From 33ed5794ff6d113ea66c6a1ce95fc92dad23fc99 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 21:32:03 +0000 Subject: [PATCH 1/2] Add fun features: race mode, island model, ASCII art evolution, and enhanced GA - Enhance ga.erl with custom targets, colorized visualization with progress bar, per-generation stats dashboard (fitness, diversity), and three selection strategies (tournament, roulette wheel, rank-based) - Add race.erl: parallel population racing using Erlang concurrency to pit selection strategies against each other with a podium display - Add island.erl: island model with periodic migration of top individuals between isolated populations running as separate Erlang processes - Add creatures.erl: evolve random characters into ASCII art creatures (cat, house, heart, rocket, smiley) with colorized grid display - Update README.md with documentation for all new modules and features https://claude.ai/code/session_01DYHpEoF3iyG5LvSbKQedzp --- README.md | 112 +++++++++++++- creatures.erl | 179 ++++++++++++++++++++++ ga.erl | 405 +++++++++++++++++++++++++++++++++----------------- island.erl | 171 +++++++++++++++++++++ race.erl | 139 +++++++++++++++++ 5 files changed, 869 insertions(+), 137 deletions(-) create mode 100644 creatures.erl create mode 100644 island.erl create mode 100644 race.erl diff --git a/README.md b/README.md index d2b8948..ee535be 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,113 @@ # GeneticLearningErlang -Supervised learning algorithm written in erlang to learn the phrase "I'm Learning" +A genetic algorithm implementation in Erlang that evolves random strings toward a target phrase using selection, crossover, and mutation. Now with multiple modes that showcase Erlang's concurrency strengths. + +## Quick Start + +```bash +# Compile all modules +erlc ga.erl race.erl island.erl creatures.erl + +# Run the basic GA (evolves toward "I'm learning!") +erl -run ga start -run init stop -noshell +``` + +## Modules + +### `ga` - Core Genetic Algorithm + +The enhanced core module with colorized output, stats tracking, and multiple selection strategies. + +```erlang +%% Default: evolve "I'm learning!" with tournament selection +ga:start(). + +%% Custom target phrase +ga:start("Hello World!"). + +%% Custom target + population size +ga:start("Erlang rocks!", 2000). + +%% Full control: target, population size, selection strategy +%% Strategies: tournament | roulette | rank +ga:start("Go Erlang!", 3000, roulette). +``` + +Each generation displays: +- **Colorized best attempt** (green = matching chars, red = wrong) +- **Progress bar** showing fitness percentage +- **Stats**: best fitness, average fitness, population diversity + +### `race` - Parallel Population Racing + +Spawns three populations side by side, each using a different selection strategy (tournament, roulette, rank), and races them to see which converges first. + +```erlang +%% Race with default target +race:start(). + +%% Race with custom target +race:start("May the best win!"). + +%% Custom target + population size per racer +race:start("Speed test", 500). +``` + +Results are displayed on a podium showing placement, generation count, and elapsed time. + +### `island` - Island Model with Migration + +Runs multiple isolated populations as separate Erlang processes. Every few generations, top individuals migrate between islands, spreading good genes. A classic parallel GA technique that's a natural fit for Erlang's process model. + +```erlang +%% Default: 4 islands, 1000 individuals each +island:start(). + +%% Custom target +island:start("Island life!"). + +%% Custom target, number of islands, population per island +island:start("Archipelago", 6, 500). +``` + +Each island uses a different selection strategy. Migration happens every 10 generations, sending the 5 best individuals to a random neighbor. + +### `creatures` - ASCII Art Evolution + +Evolves random characters into ASCII art shapes. Watch chaos slowly assemble into a recognizable creature. + +```erlang +%% Evolve a cat (default) +creatures:start(). + +%% Choose a creature: cat | house | heart | rocket | smiley +creatures:start(heart). +creatures:start(rocket). + +%% List all available creatures with previews +creatures:list(). +``` + +Creatures are displayed as 2D grids with matching characters highlighted in green. + +## How It Works + +1. **Population**: Generate N random strings the same length as the target +2. **Fitness**: Sum of absolute ASCII differences between each character and the target (lower = better, 0 = perfect) +3. **Selection**: Pick the fittest individuals to be parents + - **Tournament**: Pair up individuals randomly, winner of each pair survives + - **Roulette**: Probability of selection proportional to fitness + - **Rank**: Sort by fitness, top half survives +4. **Crossover**: Split two parents at the midpoint and swap halves to create children +5. **Mutation**: Randomly shift a character's ASCII value by 1-5 (20% chance per individual) +6. **Repeat** until the target is found + +## Configuration + +Default parameters in `ga.erl`: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `POPULATION_SIZE` | 4000 | Individuals per generation | +| `MUTATION_RATE` | 2 | Numerator for mutation probability | +| `MUTATION_CAP` | 10 | Denominator for mutation probability (rate = 2/10 = 20%) | diff --git a/creatures.erl b/creatures.erl new file mode 100644 index 0000000..e7cb480 --- /dev/null +++ b/creatures.erl @@ -0,0 +1,179 @@ +-module(creatures). +-compile(export_all). + +%%% ============================================================ +%%% ASCII Art Evolution +%%% ============================================================ +%%% Evolve random characters into ASCII art creatures! +%%% Watch as chaos slowly assembles into a recognizable shape. +%%% Available creatures: cat, house, heart, rocket, smiley +%%% ============================================================ + +%% ANSI colors +-define(GREEN, "\033[32m"). +-define(RED, "\033[31m"). +-define(CYAN, "\033[36m"). +-define(YELLOW, "\033[33m"). +-define(BOLD, "\033[1m"). +-define(DIM, "\033[2m"). +-define(RESET, "\033[0m"). + +-define(POP_SIZE, 500). + +%% Evolve a cat by default +start() -> + start(cat). + +%% Evolve a named creature: cat | house | heart | rocket | smiley +start(CreatureName) -> + random:seed(now()), + Rows = getCreature(CreatureName), + Width = length(hd(Rows)), + NumRows = length(Rows), + FlatTarget = lists:flatten(Rows), + + io:format("~n~s~s=== ASCII Art Evolution ===~s~n", [?BOLD, ?CYAN, ?RESET]), + io:format(" Creature: ~s~p~s~n", [?YELLOW, CreatureName, ?RESET]), + io:format(" Grid: ~px~p (~p chars)~n~n", [Width, NumRows, length(FlatTarget)]), + + io:format(" ~sTarget:~s~n", [?BOLD, ?RESET]), + printGrid(Rows, " "), + io:format("~n"), + + Pop = [ga:rndPrintableString(length(FlatTarget)) || _ <- lists:seq(1, ?POP_SIZE)], + StartTime = erlang:monotonic_time(millisecond), + evolve(Pop, 0, FlatTarget, Width, StartTime). + +%%% ============================================================ +%%% Creature definitions (all rows same width, padded with spaces) +%%% ============================================================ + +getCreature(cat) -> + [" /\\_/\\ ", + "( o.o )", + " > ^ < "]; + +getCreature(house) -> + [" /\\ ", + " / \\ ", + "/ \\", + "| |", + "| [] |", + "|____|"]; + +getCreature(heart) -> + [" ** ** ", + "********", + "********", + " ****** ", + " **** ", + " ** "]; + +getCreature(rocket) -> + [" /\\ ", + " / \\ ", + " | ** | ", + " | ** | ", + " | ** | ", + " /| |\\ ", + "/_|__|_\\"]; + +getCreature(smiley) -> + [" ------ ", + "| o o |", + "| /\\ |", + "| \\__/ |", + " ------ "]. + +%% List available creatures +list() -> + io:format("~nAvailable creatures:~n"), + lists:foreach(fun(Name) -> + Rows = getCreature(Name), + io:format("~n ~s~p~s:~n", [?YELLOW, Name, ?RESET]), + printGrid(Rows, " ") + end, [cat, house, heart, rocket, smiley]), + io:format("~n"). + +%%% ============================================================ +%%% Evolution loop +%%% ============================================================ + +evolve(Pop, Gen, Target, Width, StartTime) -> + ParentPop = ga:tournamentSelection(Pop, Target), + NewPop = ga:breed(ParentPop), + MNewPop = ga:performMutations(NewPop), + Best = ga:getBest(MNewPop, Target), + BestFit = ga:fitness(Best, Target), + + %% Show creature every 10 generations + case Gen rem 10 of + 0 -> printProgress(Best, Target, Width, Gen, BestFit); + _ -> ok + end, + + case Best == Target of + true -> + Elapsed = erlang:monotonic_time(millisecond) - StartTime, + io:format("~n ~s~s*** CREATURE EVOLVED! ***~s~n~n", [?BOLD, ?GREEN, ?RESET]), + printSideBySide(Target, Best, Width), + io:format("~n Generations: ~p Time: ~p ms~n~n", [Gen, Elapsed]), + {ok, Best, Gen}; + false -> + evolve(MNewPop, Gen + 1, Target, Width, StartTime) + end. + +%%% ============================================================ +%%% Display functions +%%% ============================================================ + +printProgress(Current, Target, Width, Gen, Fit) -> + io:format(" ~s--- Gen ~p (fit: ~p) ---~s~n", [?CYAN, Gen, Fit, ?RESET]), + CurrentRows = splitRows(Current, Width), + TargetRows = splitRows(Target, Width), + lists:foreach(fun({CRow, TRow}) -> + io:format(" "), + printColorizedRow(CRow, TRow), + io:format("~n") + end, lists:zip(CurrentRows, TargetRows)), + io:format("~n"). + +printSideBySide(Target, Current, Width) -> + TargetRows = splitRows(Target, Width), + CurrentRows = splitRows(Current, Width), + io:format(" ~sTarget:~s~s~sEvolved:~s~n", + [?BOLD, ?RESET, lists:duplicate(Width, $\s), ?BOLD, ?RESET]), + lists:foreach(fun({TRow, CRow}) -> + io:format(" ~s~s~s", [?GREEN, TRow, ?RESET]), + io:format(" "), + io:format("~s~s~s", [?GREEN, CRow, ?RESET]), + io:format("~n") + end, lists:zip(TargetRows, CurrentRows)). + +printColorizedRow([], []) -> ok; +printColorizedRow([C|Cs], [T|Ts]) -> + case C == T of + true -> io:format("~s~c~s", [?GREEN, C, ?RESET]); + false -> io:format("~s~c~s", [?RED, C, ?RESET]) + end, + printColorizedRow(Cs, Ts); +printColorizedRow(_, _) -> ok. + +printGrid(Rows, Indent) -> + lists:foreach(fun(Row) -> + io:format("~s~s~s~s~s~n", [Indent, ?GREEN, Row, ?RESET, ""]) + end, Rows). + +%%% ============================================================ +%%% Utilities +%%% ============================================================ + +splitRows(Flat, Width) -> + splitRows(Flat, Width, []). + +splitRows([], _, Acc) -> + lists:reverse(Acc); +splitRows(Flat, Width, Acc) -> + Len = min(Width, length(Flat)), + {Row, Rest} = lists:split(Len, Flat), + splitRows(Rest, Width, [Row | Acc]). diff --git a/ga.erl b/ga.erl index 7d81330..9efe109 100644 --- a/ga.erl +++ b/ga.erl @@ -1,159 +1,292 @@ -module(ga). -compile(export_all). +%%% ============================================================ +%%% Genetic Algorithm - Supervised Learning in Erlang +%%% ============================================================ +%%% Features: +%%% 1. Custom target strings (pass any phrase to evolve) +%%% 2. Colorized terminal visualization with progress bar +%%% 3. Multiple selection strategies (tournament/roulette/rank) +%%% 4. Per-generation statistics dashboard +%%% ============================================================ -%A mutation rate of 2 with a cap of 10 would mean a 20% mutation rate +%% Defaults -define(MUTATION_CAP, 10). -define(MUTATION_RATE, 2). - -%Crossover is done from the surviving population in tournament selection. -%Half the previous generation was wiped out and so they are replaced by children of the successful. - -%It will take a fraction of the time if you give a population size of 400 or less. -%However a larger population will get the job done in less generations and is easier to watch. -define(POPULATION_SIZE, 4000). -define(TARGET, "I'm learning!"). +%% ANSI colors +-define(GREEN, "\033[32m"). +-define(RED, "\033[31m"). +-define(CYAN, "\033[36m"). +-define(YELLOW, "\033[33m"). +-define(BOLD, "\033[1m"). +-define(DIM, "\033[2m"). +-define(RESET, "\033[0m"). + +%%% ============================================================ +%%% Entry points (Feature 1: custom targets) +%%% ============================================================ + +%% Default: evolve toward "I'm learning!" with tournament selection start() -> - random:seed(now()), - random:seed(now()), - Pop = generatePop(), - start(Pop, 0). - -start(Pop, I) -> - ParentPop = tournamentSelection(Pop), - NewPop = breed(ParentPop), - MNewPop = performMutations(NewPop), - Best = getBest(MNewPop), - io:format("Generation "), - io:format("~p",[I]), - io:format("~n"), - io:format("Best attempt: " ++ [Best]++"~n"), - case Best == ?TARGET of - true -> - Best; - false -> - start(MNewPop, I+1) - end. - -generatePop() -> - generatePop(0, []). - -generatePop(I, P) when I < ?POPULATION_SIZE -> - generatePop(I+1, P ++ [rndString(string:len(?TARGET))]); -generatePop(_I, P) -> - P. - -%generates a random string the length of N -rndString(N) -> - lists:map(fun (_) -> random:uniform(90)+$\s+1 end, lists:seq(1,N)). - -%C = chromosome -fitness(C) -> - fitness(C, ?TARGET, 0). - -fitness(_, [], F) -> - F; -fitness([], _, F) -> - F; -%Basically, find the difference between the ascii values of the target and -%the chromosomes individual chars and add to the result. -%Lower fitness is better, 0 is the best fitness -fitness([C|Cs], [T|Ts], F) -> - fitness(Cs, Ts, F+abs(T - C)). - + start(?TARGET). + +%% Custom target phrase +start(Target) -> + start(Target, ?POPULATION_SIZE). + +%% Custom target + population size +start(Target, PopSize) -> + start(Target, PopSize, tournament). + +%% Full control: target, population size, selection strategy +%% SelectionType: tournament | roulette | rank +start(Target, PopSize, SelectionType) -> + random:seed(now()), + io:format("~n~s~s=== Genetic Algorithm ===~s~n", [?BOLD, ?CYAN, ?RESET]), + io:format(" Target: ~s\"~s\"~s~n", [?GREEN, Target, ?RESET]), + io:format(" Population: ~p~n", [PopSize]), + io:format(" Selection: ~p~n", [SelectionType]), + io:format(" Mutation: ~p%~n~n", [trunc(?MUTATION_RATE / ?MUTATION_CAP * 100)]), + Pop = generatePop(PopSize, length(Target)), + StartTime = erlang:monotonic_time(millisecond), + Config = #{target => Target, pop_size => PopSize, selection => SelectionType, + start_time => StartTime}, + evolve(Pop, 0, Config). + +%%% ============================================================ +%%% Core evolution loop +%%% ============================================================ + +evolve(Pop, Gen, Config) -> + Target = maps:get(target, Config), + Selection = maps:get(selection, Config), + + %% Feature 3: pluggable selection strategies + ParentPop = case Selection of + tournament -> tournamentSelection(Pop, Target); + roulette -> rouletteSelection(Pop, Target); + rank -> rankSelection(Pop, Target) + end, + NewPop = breed(ParentPop), + MNewPop = performMutations(NewPop), + + %% Feature 4: statistics + Stats = computeStats(MNewPop, Target), + Best = maps:get(best, Stats), + + %% Feature 2: visualization + printDashboard(Gen, Best, Stats, Config), + + case Best == Target of + true -> + Elapsed = erlang:monotonic_time(millisecond) - maps:get(start_time, Config), + io:format("~n ~s~s*** EVOLVED! ***~s~n", [?BOLD, ?GREEN, ?RESET]), + io:format(" Solved in ~p generations (~p ms)~n~n", [Gen, Elapsed]), + {ok, Best, Gen, Elapsed}; + false -> + evolve(MNewPop, Gen+1, Config) + end. + +%%% ============================================================ +%%% Feature 4: Statistics +%%% ============================================================ + +computeStats(Pop, Target) -> + Fitnesses = [fitness(P, Target) || P <- Pop], + Best = getBest(Pop, Target), + BestFit = fitness(Best, Target), + WorstFit = lists:max(Fitnesses), + AvgFit = lists:sum(Fitnesses) / length(Fitnesses), + %% Diversity: unique individuals as percentage of population + Unique = length(lists:usort(Pop)), + Diversity = Unique / length(Pop) * 100, + #{best => Best, best_fitness => BestFit, worst_fitness => WorstFit, + avg_fitness => AvgFit, diversity => Diversity}. + +%%% ============================================================ +%%% Feature 2: Colorized visualization +%%% ============================================================ + +printDashboard(Gen, Best, Stats, Config) -> + Target = maps:get(target, Config), + BestFit = maps:get(best_fitness, Stats), + AvgFit = maps:get(avg_fitness, Stats), + Diversity = maps:get(diversity, Stats), + + %% Progress bar + MaxFit = length(Target) * 45, + FitPct = max(0, min(100, 100 - trunc(BestFit / max(1, MaxFit) * 100))), + BarLen = 25, + Filled = max(0, min(BarLen, trunc(FitPct / 100 * BarLen))), + Empty = BarLen - Filled, + + io:format(" ~sGen ~p~s ", [?CYAN, Gen, ?RESET]), + printColorized(Best, Target), + GreenBar = ?GREEN ++ lists:duplicate(Filled, $=) ++ ?RESET, + EmptyBar = ?DIM ++ lists:duplicate(Empty, $.) ++ ?RESET, + io:format(" [~s~s] ~p%", [GreenBar, EmptyBar, FitPct]), + io:format(" ~sfit:~p avg:~.1f div:~.1f%~s~n", + [?DIM, BestFit, AvgFit, Diversity, ?RESET]). + +%% Print string with matching chars green, non-matching red +printColorized([], []) -> ok; +printColorized([C|Cs], [T|Ts]) -> + case C == T of + true -> io:format("~s~c~s", [?GREEN, C, ?RESET]); + false -> io:format("~s~c~s", [?RED, C, ?RESET]) + end, + printColorized(Cs, Ts); +printColorized(_, _) -> ok. + +%%% ============================================================ +%%% Feature 3: Selection strategies +%%% ============================================================ + +%% Tournament selection (original, now parameterized) tournamentSelection(P) -> - shuffle(P), - {P1, P2} = lists:split(length(P) div 2, P), - tournamentSelection(P1, P2, []). - -%NP = new population -tournamentSelection([], _, NP) -> - NP; -tournamentSelection([P1|P1s], [P2|P2s], NP) -> - case fitness(P1) =< fitness(P2) of - true -> - tournamentSelection(P1s, P2s, NP ++ [P1]); - false -> - tournamentSelection(P1s, P2s, NP ++ [P2]) - end. - -%splits two words in two and merges them -%"hello" and "wasup" would make the children "wallo" and "hesup" for example + tournamentSelection(P, ?TARGET). + +tournamentSelection(P, Target) -> + SP = shuffle(P), + {P1, P2} = lists:split(length(SP) div 2, SP), + tournamentSelection(P1, P2, [], Target). + +tournamentSelection([], _, NP, _) -> NP; +tournamentSelection(_, [], NP, _) -> NP; +tournamentSelection([P1|P1s], [P2|P2s], NP, Target) -> + case fitness(P1, Target) =< fitness(P2, Target) of + true -> tournamentSelection(P1s, P2s, NP ++ [P1], Target); + false -> tournamentSelection(P1s, P2s, NP ++ [P2], Target) + end. + +%% Roulette wheel selection - probability proportional to fitness +rouletteSelection(Pop, Target) -> + MaxFit = lists:max([fitness(P, Target) || P <- Pop]) + 1, + Weighted = [{MaxFit - fitness(P, Target), P} || P <- Pop], + TotalWeight = lists:sum([W || {W, _} <- Weighted]), + HalfPop = length(Pop) div 2, + rouletteSelect(Weighted, TotalWeight, HalfPop, []). + +rouletteSelect(_, _, 0, Acc) -> Acc; +rouletteSelect(Weighted, Total, N, Acc) -> + Pick = random:uniform() * Total, + Selected = roulettePick(Weighted, Pick), + rouletteSelect(Weighted, Total, N-1, Acc ++ [Selected]). + +roulettePick([{W, P}|_], Pick) when Pick =< W -> P; +roulettePick([{W, _}|Rest], Pick) -> roulettePick(Rest, Pick - W); +roulettePick([], _) -> "". + +%% Rank-based selection - top half survives, sorted by fitness +rankSelection(Pop, Target) -> + Sorted = lists:sort(fun(A, B) -> + fitness(A, Target) =< fitness(B, Target) + end, Pop), + {Top, _} = lists:split(length(Sorted) div 2, Sorted), + Top. + +%%% ============================================================ +%%% Population generation +%%% ============================================================ + +generatePop(PopSize, StrLen) -> + [rndString(StrLen) || _ <- lists:seq(1, PopSize)]. + +rndString(N) -> + [random:uniform(90) + $\s + 1 || _ <- lists:seq(1, N)]. + +%% Wider range for printable ASCII (includes space) +rndPrintableString(N) -> + [random:uniform(95) + 31 || _ <- lists:seq(1, N)]. + +%%% ============================================================ +%%% Fitness +%%% ============================================================ + +fitness(C) -> fitness(C, ?TARGET). + +fitness(C, Target) -> + fitnessCalc(C, Target, 0). + +fitnessCalc(_, [], F) -> F; +fitnessCalc([], _, F) -> F; +fitnessCalc([C|Cs], [T|Ts], F) -> + fitnessCalc(Cs, Ts, F + abs(T - C)). + +%%% ============================================================ +%%% Crossover and breeding +%%% ============================================================ + crossover(P1, P2) -> - {P11, P12} = lists:split(length(P1) div 2, P1), - {P21, P22} = lists:split(length(P2) div 2, P2), - [P11 ++ P22] ++ [P21 ++ P12]. + {P11, P12} = lists:split(length(P1) div 2, P1), + {P21, P22} = lists:split(length(P2) div 2, P2), + [P11 ++ P22, P21 ++ P12]. + +breed(P) -> + {P1, P2} = lists:split(length(P) div 2, P), + breed(P1, P2, []). + +breed([], _, NP) -> NP; +breed(_, [], NP) -> NP; +breed([P1|P1s], [P2|P2s], NP) -> + breed(P1s, P2s, NP ++ [P1, P2] ++ crossover(P1, P2)). + +%%% ============================================================ +%%% Mutation +%%% ============================================================ performMutations(P) -> - performMutations(P, []). + performMutations(P, []). -%NP = New population -performMutations([], NP) -> - NP; +performMutations([], NP) -> NP; performMutations([P|Ps], NP) -> - case random:uniform(?MUTATION_CAP) =< ?MUTATION_RATE of - true -> - performMutations(Ps, NP ++ [mutate(P)]); - false -> - performMutations(Ps, NP ++ [P]) - end. - - -%This takes a random character in a string and changes it to another -%character 5 ascii characters away from it + case random:uniform(?MUTATION_CAP) =< ?MUTATION_RATE of + true -> performMutations(Ps, NP ++ [mutate(P)]); + false -> performMutations(Ps, NP ++ [P]) + end. + mutate(P1) -> - I = random:uniform(length(P1)), - FHalf = lists:sublist(P1, I-1), - MutPoint = lists:nth(I, P1), - LHalf = lists:nthtail(I, P1), - %randomly decide whether to go 5 ascii characters up or down - case random:uniform(2) of - 1 -> - FHalf ++ [(MutPoint + random:uniform(5))] ++ LHalf; - 2 -> - FHalf ++ [(MutPoint - random:uniform(5))] ++ LHalf - end. - -getBest([P|Ps]) -> - getBest(Ps, P). - -%B = Best -getBest([], B) -> - B; -getBest([P|Ps], B) -> - case fitness(B) > fitness(P) of - true -> - getBest(Ps, P); - false -> - getBest(Ps, B) - end. + I = random:uniform(length(P1)), + FHalf = lists:sublist(P1, I-1), + MutPoint = lists:nth(I, P1), + LHalf = lists:nthtail(I, P1), + case random:uniform(2) of + 1 -> FHalf ++ [MutPoint + random:uniform(5)] ++ LHalf; + 2 -> FHalf ++ [MutPoint - random:uniform(5)] ++ LHalf + end. -breed(P) -> - {P1, P2} = lists:split(length(P) div 2, P), - breed(P1, P2, []). +%%% ============================================================ +%%% Utilities +%%% ============================================================ -breed([], _, NP) -> - NP; -breed(_, [], NP) -> - NP; -breed([P1|P1s], [P2|P2s], NP) -> - breed(P1s, P2s, NP ++ [P1] ++ [P2] ++ crossover(P1,P2)). - -%List randomiser taken from: -%https://erlangcentral.org/wiki/index.php?title=RandomShuffle +getBest(Pop) -> getBest(Pop, ?TARGET). + +getBest([P|Ps], Target) -> getBest(Ps, P, Target). + +getBest([], B, _) -> B; +getBest([P|Ps], B, Target) -> + case fitness(B, Target) > fitness(P, Target) of + true -> getBest(Ps, P, Target); + false -> getBest(Ps, B, Target) + end. + +%% List randomiser +%% https://erlangcentral.org/wiki/index.php?title=RandomShuffle shuffle(List) -> -%% Determine the log n portion then randomize the list. - randomize(round(math:log(length(List)) + 0.5), List). - + randomize(round(math:log(length(List)) + 0.5), List). + randomize(1, List) -> - randomize(List); + randomize(List); randomize(T, List) -> - lists:foldl(fun(_E, Acc) -> - randomize(Acc) - end, randomize(List), lists:seq(1, (T - 1))). - + lists:foldl(fun(_E, Acc) -> + randomize(Acc) + end, randomize(List), lists:seq(1, T - 1)). + randomize(List) -> - D = lists:map(fun(A) -> - {random:uniform(), A} end, List), - {_, D1} = lists:unzip(lists:keysort(1, D)), D1. + D = [{random:uniform(), A} || A <- List], + {_, D1} = lists:unzip(lists:keysort(1, D)), + D1. diff --git a/island.erl b/island.erl new file mode 100644 index 0000000..886f5a7 --- /dev/null +++ b/island.erl @@ -0,0 +1,171 @@ +-module(island). +-compile(export_all). + +%%% ============================================================ +%%% Island Model - Parallel GAs with Migration +%%% ============================================================ +%%% Multiple isolated populations ("islands") evolve in parallel. +%%% Periodically, top individuals migrate between islands, +%%% spreading good genes across populations. A classic parallel +%%% GA technique, and a natural fit for Erlang's process model. +%%% ============================================================ + +-define(MIGRATION_INTERVAL, 10). %% Migrate every N generations +-define(MIGRATION_SIZE, 5). %% Number of individuals to send + +%% ANSI colors +-define(GREEN, "\033[32m"). +-define(RED, "\033[31m"). +-define(CYAN, "\033[36m"). +-define(YELLOW, "\033[33m"). +-define(BOLD, "\033[1m"). +-define(DIM, "\033[2m"). +-define(RESET, "\033[0m"). + +%% Default: 4 islands, 1000 individuals each +start() -> + start("I'm learning!"). + +start(Target) -> + start(Target, 4, 1000). + +start(Target, NumIslands, PopSize) -> + Self = self(), + random:seed(now()), + + Strategies = [tournament, roulette, rank, tournament], + + io:format("~n~s~s=== ISLAND MODEL ===~s~n", [?BOLD, ?CYAN, ?RESET]), + io:format(" Target: ~s\"~s\"~s~n", [?GREEN, Target, ?RESET]), + io:format(" Islands: ~p~n", [NumIslands]), + io:format(" Pop/island:~p~n", [PopSize]), + io:format(" Migration: every ~p gens, ~p individuals~n~n", + [?MIGRATION_INTERVAL, ?MIGRATION_SIZE]), + + %% Spawn islands + Islands = lists:map(fun(I) -> + Strat = lists:nth((I rem length(Strategies)) + 1, Strategies), + Pid = spawn(fun() -> + {A, B, C} = now(), + random:seed(A + I, B, C + I * 1000), + Pop = ga:generatePop(PopSize, length(Target)), + T0 = erlang:monotonic_time(millisecond), + islandLoop(Self, I, Pop, 0, Target, Strat, [], T0) + end), + io:format(" Island #~p spawned ~s~p~s selection [~p]~n", + [I + 1, ?YELLOW, Strat, ?RESET, Pid]), + {I, Pid, Strat} + end, lists:seq(0, NumIslands - 1)), + + %% Tell each island about its neighbors (ring topology) + IslandPids = [Pid || {_, Pid, _} <- Islands], + lists:foreach(fun({_, Pid, _}) -> + Pid ! {neighbors, IslandPids} + end, Islands), + + io:format("~n ~sMigration topology: ring~s~n", [?DIM, ?RESET]), + io:format(" ~s--- Evolution started ---~s~n~n", [?BOLD, ?RESET]), + + %% Wait for any island to solve it + receive + {solved, IslandId, Strategy, Gen, Best, Elapsed} -> + io:format("~n ~s~s*** Island #~p (~p) found the solution! ***~s~n", + [?BOLD, ?GREEN, IslandId + 1, Strategy, ?RESET]), + io:format(" Solution: \"~s\"~n", [Best]), + io:format(" Generation: ~p Time: ~p ms~n~n", [Gen, Elapsed]), + %% Kill all islands + lists:foreach(fun({_, Pid, _}) -> + case is_process_alive(Pid) of + true -> exit(Pid, kill); + false -> ok + end + end, Islands), + {ok, Best, IslandId, Gen} + after 120000 -> + io:format(" ~sTimeout after 120s!~s~n", [?RED, ?RESET]), + lists:foreach(fun({_, Pid, _}) -> exit(Pid, kill) end, Islands), + timeout + end. + +%%% ============================================================ +%%% Island process loop +%%% ============================================================ + +islandLoop(Master, Id, Pop, Gen, Target, Strategy, Neighbors, T0) -> + %% Check for neighbor registration + NewNeighbors = checkNeighbors(Neighbors), + + %% Receive any incoming migrants + Pop2 = receiveMigrants(Pop, Target), + + %% Normal GA step + ParentPop = case Strategy of + tournament -> ga:tournamentSelection(Pop2, Target); + roulette -> ga:rouletteSelection(Pop2, Target); + rank -> ga:rankSelection(Pop2, Target) + end, + NewPop = ga:breed(ParentPop), + MNewPop = ga:performMutations(NewPop), + Best = ga:getBest(MNewPop, Target), + BestFit = ga:fitness(Best, Target), + + %% Print progress every 20 generations + case Gen rem 20 of + 0 -> + io:format(" ~sIsland #~p~s gen ~p \"~s\" fit:~p~n", + [?DIM, Id + 1, ?RESET, Gen, Best, BestFit]); + _ -> ok + end, + + case Best == Target of + true -> + Elapsed = erlang:monotonic_time(millisecond) - T0, + Master ! {solved, Id, Strategy, Gen, Best, Elapsed}; + false -> + %% Send migrants at intervals + case Gen rem ?MIGRATION_INTERVAL of + 0 when Gen > 0, length(NewNeighbors) > 0 -> + sendMigrants(MNewPop, Target, NewNeighbors); + _ -> ok + end, + islandLoop(Master, Id, MNewPop, Gen + 1, Target, Strategy, NewNeighbors, T0) + end. + +%%% ============================================================ +%%% Migration +%%% ============================================================ + +checkNeighbors(Current) -> + receive + {neighbors, Pids} -> Pids + after 0 -> + Current + end. + +receiveMigrants(Pop, Target) -> + receive + {migrants, Individuals} -> + %% Replace worst individuals with migrants + Sorted = lists:sort(fun(A, B) -> + ga:fitness(A, Target) =< ga:fitness(B, Target) + end, Pop), + DropCount = min(length(Individuals), length(Sorted)), + {Keep, _Drop} = lists:split(length(Sorted) - DropCount, Sorted), + Keep ++ Individuals + after 0 -> + Pop + end. + +sendMigrants(Pop, Target, Neighbors) -> + %% Send best individuals to a random neighbor (not self) + Sorted = lists:sort(fun(A, B) -> + ga:fitness(A, Target) =< ga:fitness(B, Target) + end, Pop), + Migrants = lists:sublist(Sorted, ?MIGRATION_SIZE), + OtherPids = [P || P <- Neighbors, P =/= self()], + case OtherPids of + [] -> ok; + _ -> + DestPid = lists:nth(random:uniform(length(OtherPids)), OtherPids), + DestPid ! {migrants, Migrants} + end. diff --git a/race.erl b/race.erl new file mode 100644 index 0000000..b5baccd --- /dev/null +++ b/race.erl @@ -0,0 +1,139 @@ +-module(race). +-compile(export_all). + +%%% ============================================================ +%%% Race Mode - Parallel Population Racing +%%% ============================================================ +%%% Spawns multiple populations using different selection +%%% strategies and races them to see which converges first. +%%% Leverages Erlang's lightweight concurrency! +%%% ============================================================ + +%% ANSI colors +-define(GREEN, "\033[32m"). +-define(RED, "\033[31m"). +-define(CYAN, "\033[36m"). +-define(YELLOW, "\033[33m"). +-define(BOLD, "\033[1m"). +-define(DIM, "\033[2m"). +-define(RESET, "\033[0m"). + +%% Race 3 strategies against the default target +start() -> + start("I'm learning!"). + +%% Race with custom target +start(Target) -> + start(Target, 1000). + +%% Race with custom target and population size per racer +start(Target, PopSize) -> + Self = self(), + Strategies = [tournament, roulette, rank], + NumRacers = length(Strategies), + + io:format("~n~s~s=== RACE MODE ===~s~n", [?BOLD, ?CYAN, ?RESET]), + io:format(" Target: ~s\"~s\"~s~n", [?GREEN, Target, ?RESET]), + io:format(" Population per racer: ~p~n~n", [PopSize]), + + %% Spawn one racer per strategy + Racers = lists:map(fun(I) -> + Strategy = lists:nth(I + 1, Strategies), + Pid = spawn(fun() -> + %% Stagger seeds to ensure different random streams + {A, B, C} = now(), + random:seed(A + I, B, C + I * 1000), + racer(Self, I, Target, PopSize, Strategy) + end), + io:format(" ~s[~p]~s Racer #~p: ~s~p~s selection~n", + [?CYAN, Pid, ?RESET, I + 1, ?YELLOW, Strategy, ?RESET]), + {I, Pid, Strategy} + end, lists:seq(0, NumRacers - 1)), + + io:format("~n ~s--- GO! ---~s~n~n", [?BOLD, ?RESET]), + + %% Collect results as they finish + Results = collectResults(NumRacers, []), + + %% Kill any remaining racers + lists:foreach(fun({_, Pid, _}) -> + case is_process_alive(Pid) of + true -> exit(Pid, kill); + false -> ok + end + end, Racers), + + %% Print podium + Sorted = lists:sort(fun({_,_,G1,_}, {_,_,G2,_}) -> G1 =< G2 end, Results), + io:format("~n~s~s=== RESULTS ===~s~n~n", [?BOLD, ?YELLOW, ?RESET]), + printPodium(Sorted, 1), + io:format("~n"), + Sorted. + +%%% ============================================================ +%%% Racer process +%%% ============================================================ + +racer(Master, Id, Target, PopSize, Strategy) -> + Pop = ga:generatePop(PopSize, length(Target)), + StartTime = erlang:monotonic_time(millisecond), + racerLoop(Master, Id, Pop, 0, Target, Strategy, StartTime). + +racerLoop(Master, Id, Pop, Gen, Target, Strategy, StartTime) -> + ParentPop = case Strategy of + tournament -> ga:tournamentSelection(Pop, Target); + roulette -> ga:rouletteSelection(Pop, Target); + rank -> ga:rankSelection(Pop, Target) + end, + NewPop = ga:breed(ParentPop), + MNewPop = ga:performMutations(NewPop), + Best = ga:getBest(MNewPop, Target), + + %% Progress update every 25 generations + case Gen rem 25 of + 0 -> + Fit = ga:fitness(Best, Target), + io:format(" ~sRacer #~p~s (~p) gen ~p best: \"~s\" fit: ~p~n", + [?DIM, Id + 1, ?RESET, Strategy, Gen, Best, Fit]); + _ -> ok + end, + + case Best == Target of + true -> + Elapsed = erlang:monotonic_time(millisecond) - StartTime, + io:format("~n ~s~sRacer #~p (~p) FINISHED!~s ~p gens, ~p ms~n", + [?BOLD, ?GREEN, Id + 1, Strategy, ?RESET, Gen, Elapsed]), + Master ! {finished, Id, Strategy, Gen, Elapsed}; + false -> + racerLoop(Master, Id, MNewPop, Gen + 1, Target, Strategy, StartTime) + end. + +%%% ============================================================ +%%% Result collection +%%% ============================================================ + +collectResults(0, Acc) -> Acc; +collectResults(N, Acc) -> + receive + {finished, Id, Strategy, Gen, Time} -> + collectResults(N - 1, [{Id, Strategy, Gen, Time} | Acc]) + after 120000 -> + io:format(" ~sTimeout! ~p racer(s) didn't finish.~s~n", [?RED, N, ?RESET]), + Acc + end. + +%%% ============================================================ +%%% Podium display +%%% ============================================================ + +printPodium([], _) -> ok; +printPodium([{Id, Strategy, Gen, Time} | Rest], Place) -> + {Medal, Color} = case Place of + 1 -> {"1st", ?YELLOW}; + 2 -> {"2nd", ?CYAN}; + 3 -> {"3rd", ?RED}; + _ -> {integer_to_list(Place) ++ "th", ?DIM} + end, + io:format(" ~s~s~s ~s- Racer #~p (~p): ~p generations, ~p ms~s~n", + [?BOLD, Color, Medal, ?RESET, Id + 1, Strategy, Gen, Time, ?RESET]), + printPodium(Rest, Place + 1). From f35fc6a2b216c10f7288b585044a89a7a6c51cc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 21:35:45 +0000 Subject: [PATCH 2/2] Add .gitignore to exclude compiled .beam files https://claude.ai/code/session_01DYHpEoF3iyG5LvSbKQedzp --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17278c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.beam