From 92c258e4bc64e9b3bf69362c1d2dd9612173a077 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Mon, 12 Feb 2024 12:49:57 +0000 Subject: [PATCH 01/11] begin preparing neat feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 67f215f..639ae41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ license = "MIT" [features] default = ["max-index"] -#crossover = ["genetic-rs/crossover"] +crossover = ["genetic-rs/crossover"] rayon = ["genetic-rs/rayon", "dep:rayon"] max-index = [] From eab121b4b5a7e05dfb62120043cb59006467ccb3 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:31:45 +0000 Subject: [PATCH 02/11] implement basic crossover reproduction algo (untested) --- src/topology.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/src/topology.rs b/src/topology.rs index ef7df33..eb8815e 100644 --- a/src/topology.rs +++ b/src/topology.rs @@ -258,7 +258,7 @@ impl NeuralNetworkTopology { (self.output_layer[i].clone(), NeuronLocation::Output(i)) } } - } + } fn delete_neuron(&mut self, loc: NeuronLocation) -> NeuronTopology { if !loc.is_hidden() { @@ -491,6 +491,93 @@ impl From> } } +#[cfg(feature = "crossover")] +impl CrossoverReproduction for NeuralNetworkTopology { + // TODO deal with cyclic connection hell + fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self { + let input_layer = self.input_layer + .map(|n| n.read().unwrap().clone()) + .collect::>() + .try_into() + .unwrap(); + + let mut hidden_layers = Vec::with_capacity(self.hidden_layers.len().max(other.hidden_layers.len())); + + for i in 0..hidden_layers.len() { + if rng.gen::() <= 0.5 { + if let Some(n) = self.hidden_layers.get(i) { + let mut n = n.read().unwrap().clone(); + + n.inputs = n.inputs + .into_iter() + .filter(|(l, _)| input_exists(l, &input_layer, &hidden_layers)) + .collect(); + hidden_layers[i] = n; + + continue; + } + } + + let mut n = other.hidden_layers[i]; + + n.inputs = n.inputs + .into_iter() + .filter(|(l, _)| input_exists(l, &input_layer, &hidden_layers)) + .collect(); + hidden_layers[i] = n; + } + + let mut output_layer = self.output_layer; + + for i in 0..O { + if rng.gen::() <= 0.5 { + let mut n = self.output_layer[i].read().unwrap().clone(); + + n.inputs = n.inputs + .into_iter() + .filter(|(l, _)| input_exists(l, &input_layer, &hidden_layers)) + .collect(); + output_layer[i] = n; + + continue; + } + + let mut n = other.output_layer[i].read().unwrap().clone(); + + n.inputs = n.inputs + .into_iter() + .filter(|(l, _)| input_exists(l, &input_layer, &hidden_layers)) + .collect(); + output_layer[i] = n; + } + + let mut child = Self { + input_layer, + hidden_layers, + output_layer, + mutation_rate: self.mutation_rate, + mutation_passes: self.mutation_passes, + }; + + child.mutate(self.mutation_rate); + + child + } +} + +#[cfg(feature = "crossover")] +fn input_exists( + loc: NeuronLocation, + input: &[Arc>], + hidden: &[Arc>], +) -> bool { + match loc { + NeuronLocation::Input(i) => i < input.len(), + NeuronLocation::Hidden(i) => i < hidden.len(), + NueronLocation::Output(_) => false, + } +} + /// An activation function object that implements [`fmt::Debug`] and is [`Send`] #[derive(Clone)] pub struct ActivationFn { From d02ec6b20482d62a7706d4d51db9e291cecf1816 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:27:38 +0000 Subject: [PATCH 03/11] fix comptime errors --- src/topology.rs | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/topology.rs b/src/topology.rs index eb8815e..29a080d 100644 --- a/src/topology.rs +++ b/src/topology.rs @@ -459,22 +459,19 @@ impl From> for NeuralNetworkTopology { fn from(value: nnt_serde::NNTSerde) -> Self { - let input_layer = value - .input_layer + let input_layer = value.input_layer .into_iter() .map(|n| Arc::new(RwLock::new(n))) .collect::>() .try_into() .unwrap(); - let hidden_layers = value - .hidden_layers + let hidden_layers = value.hidden_layers .into_iter() .map(|n| Arc::new(RwLock::new(n))) .collect(); - let output_layer = value - .output_layer + let output_layer = value.output_layer .into_iter() .map(|n| Arc::new(RwLock::new(n))) .collect::>() @@ -496,7 +493,8 @@ impl CrossoverReproduction for NeuralNetworkTopo // TODO deal with cyclic connection hell fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self { let input_layer = self.input_layer - .map(|n| n.read().unwrap().clone()) + .iter() + .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone()))) .collect::>() .try_into() .unwrap(); @@ -510,24 +508,29 @@ impl CrossoverReproduction for NeuralNetworkTopo n.inputs = n.inputs .into_iter() - .filter(|(l, _)| input_exists(l, &input_layer, &hidden_layers)) + .filter(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)) .collect(); - hidden_layers[i] = n; + hidden_layers[i] = Arc::new(RwLock::new(n)); continue; } } - let mut n = other.hidden_layers[i]; + let mut n = other.hidden_layers[i].read().unwrap().clone(); n.inputs = n.inputs .into_iter() - .filter(|(l, _)| input_exists(l, &input_layer, &hidden_layers)) + .filter(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)) .collect(); - hidden_layers[i] = n; + hidden_layers[i] = Arc::new(RwLock::new(n)); } - let mut output_layer = self.output_layer; + let mut output_layer: [Arc>; O] = self.output_layer + .iter() + .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone()))) + .collect::>() + .try_into() + .unwrap(); for i in 0..O { if rng.gen::() <= 0.5 { @@ -535,9 +538,9 @@ impl CrossoverReproduction for NeuralNetworkTopo n.inputs = n.inputs .into_iter() - .filter(|(l, _)| input_exists(l, &input_layer, &hidden_layers)) + .filter(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)) .collect(); - output_layer[i] = n; + output_layer[i] = Arc::new(RwLock::new(n)); continue; } @@ -546,9 +549,9 @@ impl CrossoverReproduction for NeuralNetworkTopo n.inputs = n.inputs .into_iter() - .filter(|(l, _)| input_exists(l, &input_layer, &hidden_layers)) + .filter(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)) .collect(); - output_layer[i] = n; + output_layer[i] = Arc::new(RwLock::new(n)); } let mut child = Self { @@ -559,22 +562,22 @@ impl CrossoverReproduction for NeuralNetworkTopo mutation_passes: self.mutation_passes, }; - child.mutate(self.mutation_rate); + child.mutate(self.mutation_rate, rng); child } } #[cfg(feature = "crossover")] -fn input_exists( +fn input_exists( loc: NeuronLocation, - input: &[Arc>], + input: &[Arc>; I], hidden: &[Arc>], ) -> bool { match loc { NeuronLocation::Input(i) => i < input.len(), NeuronLocation::Hidden(i) => i < hidden.len(), - NueronLocation::Output(_) => false, + NeuronLocation::Output(_) => false, } } From 1c91bc7d20e9d92dc60fe84c38873797f3c7c0eb Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:52:57 +0000 Subject: [PATCH 04/11] add crossover example (and fix stuff) --- Cargo.toml | 5 ++ examples/basic.rs | 2 + examples/crossover.rs | 143 ++++++++++++++++++++++++++++++++++++++++++ src/topology.rs | 36 ++++++++++- 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 examples/crossover.rs diff --git a/Cargo.toml b/Cargo.toml index d302e64..324b9c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,3 +34,8 @@ serde-big-array = { version = "0.5.1", optional = true } [dev-dependencies] bincode = "1.3.3" + + +[[example]] +name = "crossover" +required-features = ["crossover"] \ No newline at end of file diff --git a/examples/basic.rs b/examples/basic.rs index aa67020..c299ac1 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,3 +1,5 @@ +//! A basic example of NEAT division reproduction with this crate. + use neat::*; use rand::prelude::*; diff --git a/examples/crossover.rs b/examples/crossover.rs new file mode 100644 index 0000000..d739e70 --- /dev/null +++ b/examples/crossover.rs @@ -0,0 +1,143 @@ +//! Essentially the same as the `basic` example, but it uses crossover reproduction instead of division reproduction. + +use neat::*; +use rand::prelude::*; + +#[derive(Clone, Debug, PartialEq)] +struct AgentDNA { + network: NeuralNetworkTopology<2, 4>, +} + +impl RandomlyMutable for AgentDNA { + fn mutate(&mut self, rate: f32, rng: &mut impl Rng) { + self.network.mutate(rate, rng); + } +} + +impl Prunable for AgentDNA {} + +impl CrossoverReproduction for AgentDNA { + fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { + Self { + network: self.network.crossover(&other.network, rng) + } + } +} + +impl GenerateRandom for AgentDNA { + fn gen_random(rng: &mut impl rand::Rng) -> Self { + Self { + network: NeuralNetworkTopology::new(0.01, 3, rng), + } + } +} + +#[derive(Debug)] +struct Agent { + network: NeuralNetwork<2, 4>, +} + +impl From<&AgentDNA> for Agent { + fn from(value: &AgentDNA) -> Self { + Self { + network: (&value.network).into(), + } + } +} + +fn fitness(dna: &AgentDNA) -> f32 { + let agent = Agent::from(dna); + + let mut fitness = 0.; + let mut rng = rand::thread_rng(); + + for _ in 0..10 { + // 10 games + + // set up game + let mut agent_pos: (i32, i32) = (rng.gen_range(0..10), rng.gen_range(0..10)); + let mut food_pos: (i32, i32) = (rng.gen_range(0..10), rng.gen_range(0..10)); + + while food_pos == agent_pos { + food_pos = (rng.gen_range(0..10), rng.gen_range(0..10)); + } + + let mut step = 0; + + loop { + // perform actions in game + let action = agent.network.predict([ + (food_pos.0 - agent_pos.0) as f32, + (food_pos.1 - agent_pos.1) as f32, + ]); + let action = action.iter().max_index(); + + match action { + 0 => agent_pos.0 += 1, + 1 => agent_pos.0 -= 1, + 2 => agent_pos.1 += 1, + _ => agent_pos.1 -= 1, + } + + step += 1; + + if agent_pos == food_pos { + fitness += 10.; + break; // new game + } else { + // lose fitness for being slow and far away + fitness -= + (food_pos.0 - agent_pos.0 + food_pos.1 - agent_pos.1).abs() as f32 * 0.001; + } + + // 50 steps per game + if step == 50 { + break; + } + } + } + + fitness +} + +#[cfg(not(feature = "rayon"))] +fn main() { + let mut rng = rand::thread_rng(); + + let mut sim = GeneticSim::new( + Vec::gen_random(&mut rng, 100), + fitness, + crossover_pruning_nextgen, + ); + + for _ in 0..100 { + sim.next_generation(); + } + + let fits: Vec<_> = sim.genomes.iter().map(fitness).collect(); + + let maxfit = fits + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap(); + + dbg!(&fits, maxfit); +} + +#[cfg(feature = "rayon")] +fn main() { + let mut sim = GeneticSim::new(Vec::gen_random(100), fitness, crossover_pruning_nextgen); + + for _ in 0..100 { + sim.next_generation(); + } + + let fits: Vec<_> = sim.genomes.iter().map(fitness).collect(); + + let maxfit = fits + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap(); + + dbg!(&fits, maxfit); +} diff --git a/src/topology.rs b/src/topology.rs index 29a080d..ab1522d 100644 --- a/src/topology.rs +++ b/src/topology.rs @@ -454,6 +454,34 @@ impl DivisionReproduction for NeuralNetworkTopol } } +impl PartialEq for NeuralNetworkTopology { + fn eq(&self, other: &Self) -> bool { + if self.mutation_rate != other.mutation_rate || self.mutation_passes != other.mutation_passes { + return false; + } + + for i in 0..I { + if *self.input_layer[i].read().unwrap() != *other.input_layer[i].read().unwrap() { + return false; + } + } + + for i in 0..self.hidden_layers.len().min(other.hidden_layers.len()) { + if *self.hidden_layers[i].read().unwrap() != *other.hidden_layers[i].read().unwrap() { + return false; + } + } + + for i in 0..O { + if *self.output_layer[i].read().unwrap() != *other.output_layer[i].read().unwrap() { + return false; + } + } + + true + } +} + #[cfg(feature = "serde")] impl From> for NeuralNetworkTopology @@ -595,6 +623,12 @@ impl fmt::Debug for ActivationFn { } } +impl PartialEq for ActivationFn { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + #[cfg(feature = "serde")] impl Serialize for ActivationFn { fn serialize(&self, serializer: S) -> Result { @@ -643,7 +677,7 @@ pub fn linear_activation(n: f32) -> f32 { } /// A stateless version of [`Neuron`][crate::Neuron]. -#[derive(Debug, Clone)] +#[derive(PartialEq, Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct NeuronTopology { /// The input locations and weights. From 29e5fe0823dbe77a73e77751f057ce75a9cca9b0 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:56:26 +0000 Subject: [PATCH 05/11] merge crossover and basic examples --- Cargo.toml | 7 +-- examples/basic.rs | 57 ++++++++++++++++- examples/crossover.rs | 143 ------------------------------------------ 3 files changed, 55 insertions(+), 152 deletions(-) delete mode 100644 examples/crossover.rs diff --git a/Cargo.toml b/Cargo.toml index 324b9c9..e08a721 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,9 +33,4 @@ serde = { version = "1.0.197", features = ["derive"], optional = true } serde-big-array = { version = "0.5.1", optional = true } [dev-dependencies] -bincode = "1.3.3" - - -[[example]] -name = "crossover" -required-features = ["crossover"] \ No newline at end of file +bincode = "1.3.3" \ No newline at end of file diff --git a/examples/basic.rs b/examples/basic.rs index c299ac1..76c891b 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,4 +1,4 @@ -//! A basic example of NEAT division reproduction with this crate. +//! A basic example of NEAT with this crate. Enable the `crossover` feature for it to use crossover reproduction use neat::*; use rand::prelude::*; @@ -24,6 +24,15 @@ impl DivisionReproduction for AgentDNA { } } +#[cfg(feature = "crossover")] +impl CrossoverReproduction for AgentDNA { + fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { + Self { + network: self.network.crossover(&other.network, rng) + } + } +} + impl GenerateRandom for AgentDNA { fn gen_random(rng: &mut impl rand::Rng) -> Self { Self { @@ -100,7 +109,7 @@ fn fitness(dna: &AgentDNA) -> f32 { fitness } -#[cfg(not(feature = "rayon"))] +#[cfg(all(not(feature = "crossover"), not(feature = "rayon")))] fn main() { let mut rng = rand::thread_rng(); @@ -124,7 +133,7 @@ fn main() { dbg!(&fits, maxfit); } -#[cfg(feature = "rayon")] +#[cfg(all(not(feature = "crossover"), feature = "rayon"))] fn main() { let mut sim = GeneticSim::new(Vec::gen_random(100), fitness, division_pruning_nextgen); @@ -141,3 +150,45 @@ fn main() { dbg!(&fits, maxfit); } + +#[cfg(all(eature = "crossover", not(feature = "rayon")))] +fn main() { + let mut rng = rand::thread_rng(); + + let mut sim = GeneticSim::new( + Vec::gen_random(&mut rng, 100), + fitness, + crossover_pruning_nextgen, + ); + + for _ in 0..100 { + sim.next_generation(); + } + + let fits: Vec<_> = sim.genomes.iter().map(fitness).collect(); + + let maxfit = fits + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap(); + + dbg!(&fits, maxfit); +} + +#[cfg(all(feature = "crossover", feature = "rayon"))] +fn main() { + let mut sim = GeneticSim::new(Vec::gen_random(100), fitness, crossover_pruning_nextgen); + + for _ in 0..100 { + sim.next_generation(); + } + + let fits: Vec<_> = sim.genomes.iter().map(fitness).collect(); + + let maxfit = fits + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap(); + + dbg!(&fits, maxfit); +} diff --git a/examples/crossover.rs b/examples/crossover.rs deleted file mode 100644 index d739e70..0000000 --- a/examples/crossover.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! Essentially the same as the `basic` example, but it uses crossover reproduction instead of division reproduction. - -use neat::*; -use rand::prelude::*; - -#[derive(Clone, Debug, PartialEq)] -struct AgentDNA { - network: NeuralNetworkTopology<2, 4>, -} - -impl RandomlyMutable for AgentDNA { - fn mutate(&mut self, rate: f32, rng: &mut impl Rng) { - self.network.mutate(rate, rng); - } -} - -impl Prunable for AgentDNA {} - -impl CrossoverReproduction for AgentDNA { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { - Self { - network: self.network.crossover(&other.network, rng) - } - } -} - -impl GenerateRandom for AgentDNA { - fn gen_random(rng: &mut impl rand::Rng) -> Self { - Self { - network: NeuralNetworkTopology::new(0.01, 3, rng), - } - } -} - -#[derive(Debug)] -struct Agent { - network: NeuralNetwork<2, 4>, -} - -impl From<&AgentDNA> for Agent { - fn from(value: &AgentDNA) -> Self { - Self { - network: (&value.network).into(), - } - } -} - -fn fitness(dna: &AgentDNA) -> f32 { - let agent = Agent::from(dna); - - let mut fitness = 0.; - let mut rng = rand::thread_rng(); - - for _ in 0..10 { - // 10 games - - // set up game - let mut agent_pos: (i32, i32) = (rng.gen_range(0..10), rng.gen_range(0..10)); - let mut food_pos: (i32, i32) = (rng.gen_range(0..10), rng.gen_range(0..10)); - - while food_pos == agent_pos { - food_pos = (rng.gen_range(0..10), rng.gen_range(0..10)); - } - - let mut step = 0; - - loop { - // perform actions in game - let action = agent.network.predict([ - (food_pos.0 - agent_pos.0) as f32, - (food_pos.1 - agent_pos.1) as f32, - ]); - let action = action.iter().max_index(); - - match action { - 0 => agent_pos.0 += 1, - 1 => agent_pos.0 -= 1, - 2 => agent_pos.1 += 1, - _ => agent_pos.1 -= 1, - } - - step += 1; - - if agent_pos == food_pos { - fitness += 10.; - break; // new game - } else { - // lose fitness for being slow and far away - fitness -= - (food_pos.0 - agent_pos.0 + food_pos.1 - agent_pos.1).abs() as f32 * 0.001; - } - - // 50 steps per game - if step == 50 { - break; - } - } - } - - fitness -} - -#[cfg(not(feature = "rayon"))] -fn main() { - let mut rng = rand::thread_rng(); - - let mut sim = GeneticSim::new( - Vec::gen_random(&mut rng, 100), - fitness, - crossover_pruning_nextgen, - ); - - for _ in 0..100 { - sim.next_generation(); - } - - let fits: Vec<_> = sim.genomes.iter().map(fitness).collect(); - - let maxfit = fits - .iter() - .max_by(|a, b| a.partial_cmp(b).unwrap()) - .unwrap(); - - dbg!(&fits, maxfit); -} - -#[cfg(feature = "rayon")] -fn main() { - let mut sim = GeneticSim::new(Vec::gen_random(100), fitness, crossover_pruning_nextgen); - - for _ in 0..100 { - sim.next_generation(); - } - - let fits: Vec<_> = sim.genomes.iter().map(fitness).collect(); - - let maxfit = fits - .iter() - .max_by(|a, b| a.partial_cmp(b).unwrap()) - .unwrap(); - - dbg!(&fits, maxfit); -} From 41e78b365607578944784cdbaa656c607d0efeae Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:58:43 +0000 Subject: [PATCH 06/11] fix example --- examples/basic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index 76c891b..1dc8602 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -3,7 +3,7 @@ use neat::*; use rand::prelude::*; -#[derive(Clone, Debug)] +#[derive(PartialEq, Clone, Debug)] struct AgentDNA { network: NeuralNetworkTopology<2, 4>, } @@ -151,7 +151,7 @@ fn main() { dbg!(&fits, maxfit); } -#[cfg(all(eature = "crossover", not(feature = "rayon")))] +#[cfg(all(feature = "crossover", not(feature = "rayon")))] fn main() { let mut rng = rand::thread_rng(); From b1a57f1c8a235d64e4d7fbb86df4a7dc47947be6 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:03:26 +0000 Subject: [PATCH 07/11] fix clippy warnings --- src/topology.rs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/topology.rs b/src/topology.rs index ab1522d..46b3d89 100644 --- a/src/topology.rs +++ b/src/topology.rs @@ -534,10 +534,7 @@ impl CrossoverReproduction for NeuralNetworkTopo if let Some(n) = self.hidden_layers.get(i) { let mut n = n.read().unwrap().clone(); - n.inputs = n.inputs - .into_iter() - .filter(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)) - .collect(); + n.inputs.retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); hidden_layers[i] = Arc::new(RwLock::new(n)); continue; @@ -546,10 +543,7 @@ impl CrossoverReproduction for NeuralNetworkTopo let mut n = other.hidden_layers[i].read().unwrap().clone(); - n.inputs = n.inputs - .into_iter() - .filter(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)) - .collect(); + n.inputs.retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); hidden_layers[i] = Arc::new(RwLock::new(n)); } @@ -560,14 +554,11 @@ impl CrossoverReproduction for NeuralNetworkTopo .try_into() .unwrap(); - for i in 0..O { + for (i, n) in self.output_layer.iter().enumerate() { if rng.gen::() <= 0.5 { - let mut n = self.output_layer[i].read().unwrap().clone(); + let mut n = n.read().unwrap().clone(); - n.inputs = n.inputs - .into_iter() - .filter(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)) - .collect(); + n.inputs.retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); output_layer[i] = Arc::new(RwLock::new(n)); continue; @@ -575,10 +566,7 @@ impl CrossoverReproduction for NeuralNetworkTopo let mut n = other.output_layer[i].read().unwrap().clone(); - n.inputs = n.inputs - .into_iter() - .filter(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)) - .collect(); + n.inputs.retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); output_layer[i] = Arc::new(RwLock::new(n)); } From 68cd7be8e6593287346def7f9f28c571aa851054 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:04:05 +0000 Subject: [PATCH 08/11] cargo fmt --- examples/basic.rs | 2 +- src/topology.rs | 44 ++++++++++++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/examples/basic.rs b/examples/basic.rs index 1dc8602..93f5cf9 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -28,7 +28,7 @@ impl DivisionReproduction for AgentDNA { impl CrossoverReproduction for AgentDNA { fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { Self { - network: self.network.crossover(&other.network, rng) + network: self.network.crossover(&other.network, rng), } } } diff --git a/src/topology.rs b/src/topology.rs index 46b3d89..f88b975 100644 --- a/src/topology.rs +++ b/src/topology.rs @@ -258,7 +258,7 @@ impl NeuralNetworkTopology { (self.output_layer[i].clone(), NeuronLocation::Output(i)) } } - } + } fn delete_neuron(&mut self, loc: NeuronLocation) -> NeuronTopology { if !loc.is_hidden() { @@ -456,7 +456,9 @@ impl DivisionReproduction for NeuralNetworkTopol impl PartialEq for NeuralNetworkTopology { fn eq(&self, other: &Self) -> bool { - if self.mutation_rate != other.mutation_rate || self.mutation_passes != other.mutation_passes { + if self.mutation_rate != other.mutation_rate + || self.mutation_passes != other.mutation_passes + { return false; } @@ -487,19 +489,22 @@ impl From> for NeuralNetworkTopology { fn from(value: nnt_serde::NNTSerde) -> Self { - let input_layer = value.input_layer + let input_layer = value + .input_layer .into_iter() .map(|n| Arc::new(RwLock::new(n))) .collect::>() .try_into() .unwrap(); - let hidden_layers = value.hidden_layers + let hidden_layers = value + .hidden_layers .into_iter() .map(|n| Arc::new(RwLock::new(n))) .collect(); - let output_layer = value.output_layer + let output_layer = value + .output_layer .into_iter() .map(|n| Arc::new(RwLock::new(n))) .collect::>() @@ -520,21 +525,24 @@ impl From> impl CrossoverReproduction for NeuralNetworkTopology { // TODO deal with cyclic connection hell fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self { - let input_layer = self.input_layer + let input_layer = self + .input_layer .iter() .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone()))) .collect::>() .try_into() .unwrap(); - let mut hidden_layers = Vec::with_capacity(self.hidden_layers.len().max(other.hidden_layers.len())); + let mut hidden_layers = + Vec::with_capacity(self.hidden_layers.len().max(other.hidden_layers.len())); for i in 0..hidden_layers.len() { if rng.gen::() <= 0.5 { if let Some(n) = self.hidden_layers.get(i) { let mut n = n.read().unwrap().clone(); - n.inputs.retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); + n.inputs + .retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); hidden_layers[i] = Arc::new(RwLock::new(n)); continue; @@ -542,12 +550,14 @@ impl CrossoverReproduction for NeuralNetworkTopo } let mut n = other.hidden_layers[i].read().unwrap().clone(); - - n.inputs.retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); + + n.inputs + .retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); hidden_layers[i] = Arc::new(RwLock::new(n)); } - let mut output_layer: [Arc>; O] = self.output_layer + let mut output_layer: [Arc>; O] = self + .output_layer .iter() .map(|n| Arc::new(RwLock::new(n.read().unwrap().clone()))) .collect::>() @@ -557,16 +567,18 @@ impl CrossoverReproduction for NeuralNetworkTopo for (i, n) in self.output_layer.iter().enumerate() { if rng.gen::() <= 0.5 { let mut n = n.read().unwrap().clone(); - - n.inputs.retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); + + n.inputs + .retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); output_layer[i] = Arc::new(RwLock::new(n)); continue; } let mut n = other.output_layer[i].read().unwrap().clone(); - - n.inputs.retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); + + n.inputs + .retain(|(l, _)| input_exists(*l, &input_layer, &hidden_layers)); output_layer[i] = Arc::new(RwLock::new(n)); } @@ -587,7 +599,7 @@ impl CrossoverReproduction for NeuralNetworkTopo #[cfg(feature = "crossover")] fn input_exists( loc: NeuronLocation, - input: &[Arc>; I], + input: &[Arc>; I], hidden: &[Arc>], ) -> bool { match loc { From 264a74cb7fc35d9d61f19b2254ff9eea6caddd0a Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:05:48 +0000 Subject: [PATCH 09/11] main docs change --- README.md | 1 + src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40fad04..140a66c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Implementation of the NEAT algorithm using `genetic-rs` ### Features - rayon - Uses parallelization on the `NeuralNetwork` struct and adds the `rayon` feature to the `genetic-rs` re-export. - serde - Adds the NNTSerde struct and allows for serialization of `NeuralNetworkTopology` +- crossover - Implements the `CrossoverReproduction` trait on `NeuralNetworkTopology` ### How To Use When working with this crate, you'll want to use the `NeuralNetworkTopology` struct in your agent's DNA and diff --git a/src/lib.rs b/src/lib.rs index 7bdd42d..ee9f769 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ //! - [x] base (single-core) crate //! - [x] rayon //! - [x] serde -//! - [ ] crossover +//! - [x] crossover //! //! You can get started by looking at [genetic-rs docs](https://docs.rs/genetic-rs) and checking the examples for this crate. From e4bcdbc50049d1fa636f7eac2375e947c38ef930 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:09:35 +0000 Subject: [PATCH 10/11] remove TODO comment (i guess it just worked without me implementing) --- src/topology.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/topology.rs b/src/topology.rs index f88b975..70357d5 100644 --- a/src/topology.rs +++ b/src/topology.rs @@ -523,7 +523,6 @@ impl From> #[cfg(feature = "crossover")] impl CrossoverReproduction for NeuralNetworkTopology { - // TODO deal with cyclic connection hell fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self { let input_layer = self .input_layer From 80d3f68b3db833a315c7c6d528f2037b1eaef94d Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+inflectrix@users.noreply.github.com> Date: Fri, 23 Feb 2024 16:46:01 +0000 Subject: [PATCH 11/11] change version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e08a721..58f4808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "neat" description = "Crate for working with NEAT in rust" -version = "0.2.1" +version = "0.3.0" edition = "2021" authors = ["Inflectrix"] repository = "https://github.com/inflectrix/neat"