commit b323eef0e2c3a3e78c7987d95cb2dbb42f107e4c Author: Nyeogmi Date: Sun Apr 20 15:32:33 2025 -0700 Simple genetic simulator, 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..aa93cfd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,217 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "colornamer" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf1702f41ec8b3a350e44afdcbae669b761712bd9dd839299775f428b811957" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "genetics" +version = "0.1.0" +dependencies = [ + "colornamer", + "itertools", + "rand", + "serde", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..306dd95 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "genetics" +version = "0.1.0" +edition = "2021" + +[dependencies] +colornamer = "1.0.1" +itertools = "0.14.0" +rand = "0.9.1" +serde = {version = "1.0.219", features=["derive"]} diff --git a/src/choice_function.rs b/src/choice_function.rs new file mode 100644 index 0000000..c583eb3 --- /dev/null +++ b/src/choice_function.rs @@ -0,0 +1,18 @@ +use rand::{rng, seq::IndexedRandom, Rng}; + +#[derive(Clone, Copy)] +pub enum ChoiceFunction { + First, + Last, + Random, +} + +impl ChoiceFunction { + pub fn choose<'a, T>(&self, xs: &'a [T]) -> &'a T { + match self { + ChoiceFunction::First => &xs[0], + ChoiceFunction::Last => &xs[xs.len() - 1], + ChoiceFunction::Random => xs.choose(&mut rng()).unwrap(), + } + } +} diff --git a/src/creature/coloration.rs b/src/creature/coloration.rs new file mode 100644 index 0000000..9c0845a --- /dev/null +++ b/src/creature/coloration.rs @@ -0,0 +1,65 @@ +use colornamer::{ColorNamer, Colors}; + +use crate::genetics::{Genetic, Transcriber}; + +use super::stats::Stat; + +#[derive(Default)] +pub struct Coloration { + pub eyes: Color, + pub body: Color, +} + +impl Coloration { + pub fn profile(&self) -> String { + return format!( + "Eyes: {}\nBody: {}\n", + self.eyes.to_name(), + self.body.to_name() + ); + } +} + +impl Genetic for Coloration { + fn transcribe(&mut self, t: Transcriber) { + self.eyes.transcribe(t.nest("eyes")); + self.body.transcribe(t.nest("body")); + } +} + +#[derive(Clone, Copy, Default)] +pub struct Color { + pub r: Stat<'.', '!', 0, 8>, + pub g: Stat<'.', '!', 0, 8>, + pub b: Stat<'.', '!', 0, 8>, + pub pallor: Stat<'.', '!', 0, 8>, +} + +impl Genetic for Color { + fn transcribe(&mut self, t: Transcriber) { + self.r.transcribe(t.nest("r")); + self.g.transcribe(t.nest("g")); + self.b.transcribe(t.nest("b")); + self.pallor.transcribe(t.nest("pallor")); + } +} + +impl Color { + fn to_name(&self) -> String { + let mut r = self.r.value() * 255 / 8; + let mut g = self.g.value() * 255 / 8; + let mut b = self.b.value() * 255 / 8; + let pallor_amt = self.pallor.value() * 255 / 8; + r = (r * (255 - pallor_amt) + 255 * pallor_amt) / 255; + g = (g * (255 - pallor_amt) + 255 * pallor_amt) / 255; + b = (b * (255 - pallor_amt) + 255 * pallor_amt) / 255; + // let colornamer = ColorNamer::new(Colors::NTC); + let colornamer = ColorNamer::new(Colors::X11); + let hex_color = format!("#{:02x}{:02x}{:02x}", r, g, b); + return format!( + "{} ({})", + colornamer.name_hex_color(&hex_color).unwrap(), + hex_color + ); + } +} diff --git a/src/creature/mod.rs b/src/creature/mod.rs new file mode 100644 index 0000000..5831c97 --- /dev/null +++ b/src/creature/mod.rs @@ -0,0 +1,62 @@ +mod coloration; +mod species; +mod stats; + +use coloration::Coloration; +use species::Parts; +use stats::Stats; + +use crate::genetics::{Genetic, Genotype, Transcriber}; + +#[derive(Clone, Debug)] +pub struct Creature { + // pub name: String, + pub genotype: Genotype, +} + +impl Creature { + pub fn profile(&self) -> String { + let phenotype: Phenotype = self.genotype.load(); + + let mut profile = phenotype.profile(); // format!("Name: {}\n{}", self.name, phenotype.profile()); + profile.push_str("\nGenome:\n"); + for (chromosome, (lhs, rhs)) in self.genotype.chromosomes.iter() { + profile.push_str(&format!(" {}[l]: {}\n", chromosome, lhs)); + profile.push_str(&format!(" {}[r]: {}\n", chromosome, rhs)); + } + return profile; + } + + pub fn breed(&self, other: &Creature) -> Creature { + return Creature { + // name: String::from("Pyrex"), + genotype: self.genotype.breed(&other.genotype), + }; + } +} + +#[derive(Default)] +pub struct Phenotype { + pub stats: Stats, + pub coloration: Coloration, + pub parts: Parts, +} + +impl Phenotype { + pub fn profile(&self) -> String { + format!( + "{}{}{}", + self.stats.profile(), + self.coloration.profile(), + self.parts.profile() + ) + } +} + +impl Genetic for Phenotype { + fn transcribe(&mut self, t: Transcriber) { + self.stats.transcribe(t.nest("stats")); + self.coloration.transcribe(t.nest("coloration")); + self.parts.transcribe(t.nest("parts")); + } +} diff --git a/src/creature/species.rs b/src/creature/species.rs new file mode 100644 index 0000000..ed32d16 --- /dev/null +++ b/src/creature/species.rs @@ -0,0 +1,64 @@ +use crate::genetics::{Genetic, Transcriber}; + +use super::stats::Stat; + +#[derive(Default)] +pub struct Parts { + stare: Stat<'e', 'E', 0, 6>, + fangs: Stat<'f', 'F', 0, 6>, + wings: Stat<'w', 'W', 0, 6>, +} + +impl Parts { + pub fn profile(&self) -> String { + format!("Species: {:?}\n", self.species()) + } + + pub fn has_stare(&self) -> bool { + return self.stare.value() >= 5; + } + + pub fn has_fangs(&self) -> bool { + return self.fangs.value() >= 5; + } + + pub fn has_wings(&self) -> bool { + return self.wings.value() >= 5; + } + + pub fn species(&self) -> Species { + let mut ix = 0; + if self.has_stare() { + ix += 4; + } + if self.has_fangs() { + ix += 2; + } + if self.has_wings() { + ix += 1; + } + + use Species::*; + return [Bunny, Pigeon, Rat, Dragon, Hare, Crow, Snake, Bat][ix]; + } +} + +impl Genetic for Parts { + fn transcribe(&mut self, t: Transcriber) { + self.stare.transcribe(t.nest("stare")); + self.fangs.transcribe(t.nest("fangs")); + self.wings.transcribe(t.nest("wings")); + } +} + +#[derive(Clone, Copy, Debug)] +pub enum Species { + Bunny, + Pigeon, + Rat, + Dragon, + Hare, + Crow, + Snake, + Bat, +} diff --git a/src/creature/stats.rs b/src/creature/stats.rs new file mode 100644 index 0000000..32ae912 --- /dev/null +++ b/src/creature/stats.rs @@ -0,0 +1,57 @@ +use crate::genetics::{Genetic, Transcriber}; + +#[derive(Default)] +pub struct Stats { + pub iq: Stat<'.', '!', 5, 10>, // 5-15 + pub eq: Stat<'.', '!', 5, 10>, // 5-15 +} + +impl Stats { + pub fn profile(&self) -> String { + format!("IQ: {}\nEQ: {}\n", self.iq.value(), self.eq.value()) + } +} + +impl Genetic for Stats { + fn transcribe(&mut self, t: Transcriber) { + self.iq.transcribe(t.nest("IQ")); + self.eq.transcribe(t.nest("EQ")); + } +} + +#[derive(Clone, Copy)] +pub struct Stat { + values: [bool; RANGE], +} + +impl + Stat +{ + pub fn value(&self) -> usize { + let mut value = MIN; + for v in self.values.iter() { + value += *v as usize; + } + return value; + } +} + +impl Default + for Stat +{ + fn default() -> Self { + Self { + values: [false; RANGE], + } + } +} + +impl Genetic + for Stat +{ + fn transcribe(&mut self, mut t: Transcriber) { + for v in self.values.iter_mut() { + t.express(v, &[(OFF, false), (ON, true)]); + } + } +} diff --git a/src/genetics.rs b/src/genetics.rs new file mode 100644 index 0000000..d577d9a --- /dev/null +++ b/src/genetics.rs @@ -0,0 +1,199 @@ +use std::{ + cell::RefCell, + collections::{BTreeMap, HashSet}, + rc::Rc, +}; + +use itertools::{EitherOrBoth, Itertools}; +use rand::{seq::IndexedRandom, RngCore}; +use serde::{Deserialize, Serialize}; + +use crate::choice_function::ChoiceFunction; + +#[derive(Clone, Deserialize, Serialize, Debug)] +pub struct Genotype { + pub chromosomes: BTreeMap, +} +type Chromosomes = (String, String); + +fn merge(rng: &mut impl RngCore, one: &str, two: &str) -> String { + let mut results = String::new(); + for pair in one.chars().zip_longest(two.chars()) { + results.push(match pair { + EitherOrBoth::Both(l, r) => *[l, r].choose(rng).unwrap(), + EitherOrBoth::Left(l) => l, + EitherOrBoth::Right(r) => r, + }) + } + return results; +} + +impl Genotype { + pub fn breed(&self, other: &Genotype) -> Genotype { + let mut rng = rand::rng(); + let mut chromosomes: HashSet = HashSet::new(); + chromosomes.extend(self.chromosomes.keys().cloned()); + chromosomes.extend(other.chromosomes.keys().cloned()); + + let mut result: BTreeMap = BTreeMap::new(); + let placeholder = (String::new(), String::new()); + for k in chromosomes { + let mine = self.chromosomes.get(&k).unwrap_or(&placeholder); + let theirs = self.chromosomes.get(&k).unwrap_or(&placeholder); + + let mine_merged = merge(&mut rng, &mine.0, &mine.1); + let theirs_merged = merge(&mut rng, &theirs.0, &theirs.1); + result.insert(k, (mine_merged, theirs_merged)); + } + return Genotype { + chromosomes: result, + }; + } + + pub fn generate(choice_function: ChoiceFunction) -> Self { + let root = RootTranscriber::Generator(Generator { + choice_function, + progress: BTreeMap::new(), + }); + let root_cell = Rc::new(RefCell::new(root)); + let transcriber = Transcriber { + root: root_cell.clone(), + chromosome: String::from("base"), + }; + let mut base = T::default(); + base.transcribe(transcriber); + match &*root_cell.borrow() { + RootTranscriber::Generator(generator) => { + let chromosomes = generator.progress.clone(); + return Genotype { chromosomes }; + } + _ => panic!("a generator should not able to turn into a loader"), + }; + } + + pub fn load(&self) -> T { + let mut progress = BTreeMap::new(); + for (key, (lhs, rhs)) in self.chromosomes.iter() { + progress.insert( + key.clone(), + (0, Vec::from_iter(lhs.chars()), Vec::from_iter(rhs.chars())), + ); + } + let root = RootTranscriber::Loader(Loader { progress }); + let root_cell = Rc::new(RefCell::new(root)); + let transcriber = Transcriber { + root: root_cell.clone(), + chromosome: String::from("base"), + }; + let mut base = T::default(); + base.transcribe(transcriber); + return base; + } +} + +enum RootTranscriber { + Generator(Generator), + Loader(Loader), +} + +impl RootTranscriber { + pub fn express( + &mut self, + chromosome: &str, + field: &mut T, + possibilities: &[(char, T)], + ) { + match self { + RootTranscriber::Generator(generator) => { + generator.express(chromosome, field, possibilities) + } + RootTranscriber::Loader(loader) => loader.express(chromosome, field, possibilities), + } + } +} + +#[derive(Clone)] +pub struct Transcriber { + root: Rc>, + chromosome: String, +} + +impl Transcriber { + pub fn express(&mut self, field: &mut T, possibilities: &[(char, T)]) { + self.root + .borrow_mut() + .express(&self.chromosome, field, possibilities) + } + + pub fn nest(&self, suffix: &str) -> Transcriber { + return Transcriber { + root: self.root.clone(), + chromosome: format!("{}.{}", self.chromosome, suffix), + }; + } +} + +pub trait Genetic: Default { + fn transcribe(&mut self, t: Transcriber); +} + +struct Generator { + choice_function: ChoiceFunction, + progress: BTreeMap, +} + +impl Generator { + pub fn express(&mut self, chromosome: &str, _field: &mut T, possibilities: &[(char, T)]) { + let lhs = self.choice_function.choose(possibilities).0; + let rhs = self.choice_function.choose(possibilities).0; + + let progress = self + .progress + .entry(chromosome.to_string()) + .or_insert((String::new(), String::new())); + + progress.0.push(lhs); + progress.1.push(rhs); + } +} + +struct Loader { + progress: BTreeMap, Vec)>, +} + +impl Loader { + pub fn express( + &mut self, + chromosome: &str, + field: &mut T, + possibilities: &[(char, T)], + ) { + let (mut lhs, mut rhs) = ('?', '?'); + if let Some((ix, lhs_buf, rhs_buf)) = self.progress.get_mut(chromosome) { + lhs = lhs_buf.get(*ix).cloned().unwrap_or('?'); + rhs = rhs_buf.get(*ix).cloned().unwrap_or('?'); + *ix += 1; + } + + let mut expressed = lhs; // lhs wins if not dominated + if dominance(rhs) > dominance(lhs) { + expressed = rhs; + } + for (allele, value) in possibilities.iter() { + if *allele == expressed { + *field = value.clone(); + return; + } + } + } +} + +fn dominance(allele: char) -> u8 { + if allele.is_ascii_lowercase() { + return 1; + } + if allele.is_ascii_uppercase() { + return 2; + } + return 0; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ea43751 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,74 @@ +use choice_function::ChoiceFunction; +use creature::{Creature, Phenotype}; +use genetics::Genotype; +use rand::{rng, seq::SliceRandom}; + +mod choice_function; +mod creature; +mod genetics; + +fn generate_creature() -> Creature { + let genotype: Genotype = Genotype::generate::(ChoiceFunction::Random); + let creature = Creature { + // name: String::from("Pyrex"), + genotype, + }; + return creature; +} + +fn evolve() { + const N_ITERATIONS: usize = 200; + const N_CREATURES: usize = 100; + const N_SURVIVORS: usize = 90; + + let mut pool = vec![]; + for _ in 0..N_CREATURES { + pool.push(generate_creature()); + } + + for iteration in 0..N_ITERATIONS { + report(iteration, &pool); + + // kill N creatures + let mut survivors = pool.clone(); + survivors.sort_by(|x, y| fitness(x).partial_cmp(&fitness(y)).unwrap()); + survivors.reverse(); + survivors.drain(N_SURVIVORS..N_CREATURES); + + let mut pool2 = vec![]; + let mut survivors_queue = vec![]; + while survivors_queue.len() < N_CREATURES * 2 { + survivors_queue.extend(survivors.clone()); + } + survivors_queue.shuffle(&mut rng()); + + for _ in 0..N_CREATURES { + let lhs = survivors_queue.pop().unwrap(); + let rhs = survivors_queue.pop().unwrap(); + pool2.push(lhs.breed(&rhs)); + } + pool = pool2; + } +} + +fn report(iteration: usize, pool: &[Creature]) { + println!("== Iteration {} ==", iteration + 1); + println!("{}", pool[0].profile()); + println!(""); +} + +fn fitness(creature: &Creature) -> f64 { + let phenotype: Phenotype = creature.genotype.load(); + let mut value = 0.0; + if phenotype.parts.has_wings() { + value += 0.5; + } + value -= phenotype.coloration.body.r.value() as f64 / 4.0; + value += (phenotype.stats.iq.value() as f64) / 10.0; + + return value; +} + +fn main() { + evolve(); +}