From 882925490b82474a0894d9d32e614371c639a0ed Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:30:39 +0000 Subject: [PATCH 01/11] implement basic speciation based on inverse Jaccard similarity --- Cargo.lock | 12 ++++---- Cargo.toml | 2 +- src/neuralnet.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6010d8..25b44fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,9 +125,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "genetic-rs" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda85cc7ffaa6c34c6a603de4b47a81c399b26fa825e4866de57f3c5b184b53" +checksum = "bb54b5cf12753eb0217391e15f8ae0eb388ef74acf9c145c8e826ffd168813e3" dependencies = [ "genetic-rs-common", "genetic-rs-macros", @@ -135,9 +135,9 @@ dependencies = [ [[package]] name = "genetic-rs-common" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9824e028a96d1b962aa2c300457ff3fc012066f12872db58e65475f57fa41ee" +checksum = "5d997aa6eb69d42e76d6fcb959cb33a0fc5baf443075489efe5f31a1f8c43ed5" dependencies = [ "itertools", "rand", @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "genetic-rs-macros" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41691333bc965d711879ec409130ec42c5dbb28e8cdd79947785fc6420853d6d" +checksum = "ee1b5fffd72f5cc860492bf736fedac9f8bdd5b1ffd84d90b21943c42fd31559" dependencies = [ "darling", "genetic-rs-common", diff --git a/Cargo.toml b/Cargo.toml index 94fa28e..1bce138 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ opt-level = 3 [dependencies] atomic_float = "1.1.0" bitflags = "2.11.0" -genetic-rs = { version = "1.3.0", features = ["rayon"] } +genetic-rs = { version = "1.4.0", features = ["rayon"] } lazy_static = "1.5.0" rayon = "1.11.0" replace_with = "0.1.8" diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 5dffd9b..2f97ee4 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -811,6 +811,35 @@ impl NeuralNetwork { *w += amount; }); } + + /// Gets a set of all connections in the neural network. + /// Used for things like calculating divergence between neural networks during speciation. + pub fn edges_set(&self) -> HashSet { + let mut edges = HashSet::new(); + + for (i, n) in self.input_layer.iter().enumerate() { + let from = NeuronLocation::Input(i); + for (&to, _) in &n.outputs { + edges.insert(Connection { from, to }); + } + } + + for (i, n) in self.hidden_layers.iter().enumerate() { + let from = NeuronLocation::Hidden(i); + for (&to, _) in &n.outputs { + edges.insert(Connection { from, to }); + } + } + + for (i, n) in self.output_layer.iter().enumerate() { + let from = NeuronLocation::Output(i); + for (&to, _) in &n.outputs { + edges.insert(Connection { from, to }); + } + } + + edges + } } impl Index for NeuralNetwork { @@ -1042,6 +1071,51 @@ fn output_exists(loc: NeuronLocation, hidden_len: usize, output_len: usize) -> b } } +/// The weights for calculating divergence between two neural networks. +pub struct DivergenceWeights { + /// The weight for the symmetric difference of edges + edge: f32, + + /// The weight for the difference in the number of hidden neurons. + node: f32, +} + +impl DivergenceWeights { + /// Creates a new [`DivergenceWeights`] with the specified edge and node weights. + /// Arguments must add to 1.0 and be between 0.0 and 1.0 inclusive. + pub fn new(edge: f32, node: f32) -> Self { + assert!( + (edge + node - 1.0).abs() < f32::EPSILON, + "edge and node weights must add to 1.0" + ); + assert!( + (0.0..=1.0).contains(&edge) && (0.0..=1.0).contains(&node), + "edge and node weights must be between 0.0 and 1.0 inclusive" + ); + + Self { edge, node } + } +} + +impl Speciated for NeuralNetwork { + type Context = DivergenceWeights; + + fn divergence(&self, other: &Self, ctx: &Self::Context) -> f32 { + let self_edges = self.edges_set(); + let other_edges = other.edges_set(); + let total_edges = self_edges.union(&other_edges).count(); + + let edge_diff = self_edges.symmetric_difference(&other_edges).count() as f32; + + let edge_term = ctx.edge * edge_diff / total_edges.max(1) as f32; + + let node_diff = self.hidden_layers.len().abs_diff(other.hidden_layers.len()) as f32; + let node_term = ctx.node * node_diff / self.hidden_layers.len().max(other.hidden_layers.len()).max(1) as f32; + + edge_term + node_term + } +} + /// A helper struct for operations on connections between neurons. /// It does not contain information about the weight. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] From c226ef177c183771d4e35fcd5c95e95c9bc82612 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:31:49 +0000 Subject: [PATCH 02/11] add default for DivergenceWeights --- src/neuralnet.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 2f97ee4..8946c5b 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -1097,9 +1097,16 @@ impl DivergenceWeights { } } +impl Default for DivergenceWeights { + fn default() -> Self { + Self { edge: 0.5, node: 0.5 } + } +} + impl Speciated for NeuralNetwork { type Context = DivergenceWeights; + /// Divergence based on weighted inverse Jaccard similarity. fn divergence(&self, other: &Self, ctx: &Self::Context) -> f32 { let self_edges = self.edges_set(); let other_edges = other.edges_set(); From b41c5825ebc562718832eee9c04948323f09fab8 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:26:05 +0000 Subject: [PATCH 03/11] adjust default weights --- src/neuralnet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 8946c5b..532ae40 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -1099,7 +1099,7 @@ impl DivergenceWeights { impl Default for DivergenceWeights { fn default() -> Self { - Self { edge: 0.5, node: 0.5 } + Self { edge: 0.7, node: 0.3 } } } From c02f1ced092ed8a9ed06991512203d1ec4271791 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:27:18 +0000 Subject: [PATCH 04/11] add doc comment --- src/neuralnet.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 532ae40..4600556 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -1083,6 +1083,9 @@ pub struct DivergenceWeights { impl DivergenceWeights { /// Creates a new [`DivergenceWeights`] with the specified edge and node weights. /// Arguments must add to 1.0 and be between 0.0 and 1.0 inclusive. + /// Edge weight is the weight for the symmetric difference of edges, and node weight is + /// the weight for the difference in the number of hidden neurons. + /// Default recommended values are edge = 0.7 and node = 0.3, but feel free to experiment with different values. pub fn new(edge: f32, node: f32) -> Self { assert!( (edge + node - 1.0).abs() < f32::EPSILON, From c323bf6278e8284e8dfea0afba58130e8482bae2 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:30:17 +0000 Subject: [PATCH 05/11] cargo fmt --- src/neuralnet.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 4600556..d16d8af 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -1102,7 +1102,10 @@ impl DivergenceWeights { impl Default for DivergenceWeights { fn default() -> Self { - Self { edge: 0.7, node: 0.3 } + Self { + edge: 0.7, + node: 0.3, + } } } @@ -1120,7 +1123,12 @@ impl Speciated for NeuralNetwork { let edge_term = ctx.edge * edge_diff / total_edges.max(1) as f32; let node_diff = self.hidden_layers.len().abs_diff(other.hidden_layers.len()) as f32; - let node_term = ctx.node * node_diff / self.hidden_layers.len().max(other.hidden_layers.len()).max(1) as f32; + let node_term = ctx.node * node_diff + / self + .hidden_layers + .len() + .max(other.hidden_layers.len()) + .max(1) as f32; edge_term + node_term } From f2f0dfcc622c046caaccbc92530088d24b03264a Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:31:05 +0000 Subject: [PATCH 06/11] fix clippy warnings --- src/neuralnet.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index d16d8af..2d9c857 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -819,21 +819,21 @@ impl NeuralNetwork { for (i, n) in self.input_layer.iter().enumerate() { let from = NeuronLocation::Input(i); - for (&to, _) in &n.outputs { + for &to in n.outputs.keys() { edges.insert(Connection { from, to }); } } for (i, n) in self.hidden_layers.iter().enumerate() { let from = NeuronLocation::Hidden(i); - for (&to, _) in &n.outputs { + for &to in n.outputs.keys() { edges.insert(Connection { from, to }); } } for (i, n) in self.output_layer.iter().enumerate() { let from = NeuronLocation::Output(i); - for (&to, _) in &n.outputs { + for &to in n.outputs.keys() { edges.insert(Connection { from, to }); } } From f217697facb88af06978ac2c801cf280bedfe0c2 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:43:29 +0000 Subject: [PATCH 07/11] small consistency change in divergence function --- src/neuralnet.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 2d9c857..994c23e 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -1116,11 +1116,11 @@ impl Speciated for NeuralNetwork { fn divergence(&self, other: &Self, ctx: &Self::Context) -> f32 { let self_edges = self.edges_set(); let other_edges = other.edges_set(); - let total_edges = self_edges.union(&other_edges).count(); + let total_edges = self_edges.union(&other_edges).count() as f32; let edge_diff = self_edges.symmetric_difference(&other_edges).count() as f32; - let edge_term = ctx.edge * edge_diff / total_edges.max(1) as f32; + let edge_term = ctx.edge * edge_diff / total_edges.max(1.0); let node_diff = self.hidden_layers.len().abs_diff(other.hidden_layers.len()) as f32; let node_term = ctx.node * node_diff From 1c4165599607e7dd00eb87bc9681d5e44f695459 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:44:17 +0000 Subject: [PATCH 08/11] add some QoL derives that were missing --- src/neuralnet.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 994c23e..109365b 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -960,6 +960,7 @@ impl RandomlyMutable for NeuralNetwork { } /// The settings used for [`NeuralNetwork`] reproduction. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] pub struct ReproductionSettings { /// The mutation settings to use during reproduction. @@ -1072,6 +1073,8 @@ fn output_exists(loc: NeuronLocation, hidden_len: usize, output_len: usize) -> b } /// The weights for calculating divergence between two neural networks. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize))] pub struct DivergenceWeights { /// The weight for the symmetric difference of edges edge: f32, @@ -1080,6 +1083,17 @@ pub struct DivergenceWeights { node: f32, } +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for DivergenceWeights { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (edge, node) = <(f32, f32)>::deserialize(deserializer)?; + Ok(DivergenceWeights::new(edge, node)) + } +} + impl DivergenceWeights { /// Creates a new [`DivergenceWeights`] with the specified edge and node weights. /// Arguments must add to 1.0 and be between 0.0 and 1.0 inclusive. From f777bb40c04edb479fad003b8ea8ed4a718776d4 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:20:25 +0000 Subject: [PATCH 09/11] bump genetic-rs version --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25b44fc..83c3713 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,9 +125,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "genetic-rs" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb54b5cf12753eb0217391e15f8ae0eb388ef74acf9c145c8e826ffd168813e3" +checksum = "91ff84315b42aa07d69d5d0544403f4d56791824b4e0e10420ca6c3fcaff193d" dependencies = [ "genetic-rs-common", "genetic-rs-macros", @@ -135,9 +135,9 @@ dependencies = [ [[package]] name = "genetic-rs-common" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d997aa6eb69d42e76d6fcb959cb33a0fc5baf443075489efe5f31a1f8c43ed5" +checksum = "93606635ba093802487db62ca9b3ffd9762946fd2fa2fc1461fc6f1342f8a6cd" dependencies = [ "itertools", "rand", @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "genetic-rs-macros" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee1b5fffd72f5cc860492bf736fedac9f8bdd5b1ffd84d90b21943c42fd31559" +checksum = "909a36b32f9d8361c56ef2a76dc905484698e32f4fe6ed55c36dde182be81f84" dependencies = [ "darling", "genetic-rs-common", diff --git a/Cargo.toml b/Cargo.toml index 1bce138..0d849de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ opt-level = 3 [dependencies] atomic_float = "1.1.0" bitflags = "2.11.0" -genetic-rs = { version = "1.4.0", features = ["rayon"] } +genetic-rs = { version = "1.4.1", features = ["rayon"] } lazy_static = "1.5.0" rayon = "1.11.0" replace_with = "0.1.8" From 8ba48a8e06ed1ea5ff9734106137f5e18d87f0b2 Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sun, 15 Mar 2026 17:56:56 -0400 Subject: [PATCH 10/11] remove normalization from divergence implementation (wip, needs testing to see if it's actually worth keeping) --- src/neuralnet.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 109365b..94b10c2 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -1096,10 +1096,9 @@ impl<'de> Deserialize<'de> for DivergenceWeights { impl DivergenceWeights { /// Creates a new [`DivergenceWeights`] with the specified edge and node weights. - /// Arguments must add to 1.0 and be between 0.0 and 1.0 inclusive. /// Edge weight is the weight for the symmetric difference of edges, and node weight is /// the weight for the difference in the number of hidden neurons. - /// Default recommended values are edge = 0.7 and node = 0.3, but feel free to experiment with different values. + /// Default values are edge = 0.7 and node = 0.3, but feel free to experiment with different values. pub fn new(edge: f32, node: f32) -> Self { assert!( (edge + node - 1.0).abs() < f32::EPSILON, @@ -1130,19 +1129,19 @@ impl Speciated for NeuralNetwork { fn divergence(&self, other: &Self, ctx: &Self::Context) -> f32 { let self_edges = self.edges_set(); let other_edges = other.edges_set(); - let total_edges = self_edges.union(&other_edges).count() as f32; + // let total_edges = self_edges.union(&other_edges).count() as f32; let edge_diff = self_edges.symmetric_difference(&other_edges).count() as f32; - let edge_term = ctx.edge * edge_diff / total_edges.max(1.0); + let edge_term = ctx.edge * edge_diff; // / total_edges.max(1.0); let node_diff = self.hidden_layers.len().abs_diff(other.hidden_layers.len()) as f32; - let node_term = ctx.node * node_diff - / self - .hidden_layers - .len() - .max(other.hidden_layers.len()) - .max(1) as f32; + let node_term = ctx.node * node_diff; + // / self + // .hidden_layers + // .len() + // .max(other.hidden_layers.len()) + // .max(1) as f32; edge_term + node_term } From 30fe21d015b8997d9fd6ee71f58111967853a082 Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sun, 15 Mar 2026 17:59:36 -0400 Subject: [PATCH 11/11] update DivergenceWeights implementation to compensate for normalization removal --- src/neuralnet.rs | 40 +++++----------------------------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/src/neuralnet.rs b/src/neuralnet.rs index 94b10c2..2b11f91 100644 --- a/src/neuralnet.rs +++ b/src/neuralnet.rs @@ -1074,50 +1074,20 @@ fn output_exists(loc: NeuronLocation, hidden_len: usize, output_len: usize) -> b /// The weights for calculating divergence between two neural networks. #[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct DivergenceWeights { /// The weight for the symmetric difference of edges - edge: f32, + pub edge: f32, /// The weight for the difference in the number of hidden neurons. - node: f32, -} - -#[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for DivergenceWeights { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (edge, node) = <(f32, f32)>::deserialize(deserializer)?; - Ok(DivergenceWeights::new(edge, node)) - } -} - -impl DivergenceWeights { - /// Creates a new [`DivergenceWeights`] with the specified edge and node weights. - /// Edge weight is the weight for the symmetric difference of edges, and node weight is - /// the weight for the difference in the number of hidden neurons. - /// Default values are edge = 0.7 and node = 0.3, but feel free to experiment with different values. - pub fn new(edge: f32, node: f32) -> Self { - assert!( - (edge + node - 1.0).abs() < f32::EPSILON, - "edge and node weights must add to 1.0" - ); - assert!( - (0.0..=1.0).contains(&edge) && (0.0..=1.0).contains(&node), - "edge and node weights must be between 0.0 and 1.0 inclusive" - ); - - Self { edge, node } - } + pub node: f32, } impl Default for DivergenceWeights { fn default() -> Self { Self { - edge: 0.7, - node: 0.3, + edge: 1.0, + node: 1.0, } } }