Increasing code coverage
This commit is contained in:
parent
95fdad1034
commit
32a6813cf4
3 changed files with 282 additions and 48 deletions
|
@ -19,5 +19,7 @@ num_cpus = "1.17.0"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.143"
|
serde_json = "1.0.143"
|
||||||
|
tempfile = "3.21.0"
|
||||||
tokio = { version = "1.47.1", features = ["full"] }
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
|
tokio-test = "0.4.4"
|
||||||
uuid = "1.18.1"
|
uuid = "1.18.1"
|
||||||
|
|
|
@ -935,6 +935,74 @@ async fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result<f32, io::
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use tokio_test::block_on;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_individual_id_format() {
|
||||||
|
let nn = FighterNN {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
folder: PathBuf::new(),
|
||||||
|
generation: 0,
|
||||||
|
population_size: 10,
|
||||||
|
scores: vec![],
|
||||||
|
nn_shapes: vec![],
|
||||||
|
crossbreed_segments: 2,
|
||||||
|
weight_initialization_range: -0.5..0.5,
|
||||||
|
minor_mutation_rate: 0.1,
|
||||||
|
major_mutation_rate: 0.05,
|
||||||
|
mutation_weight_range: -0.1..0.1,
|
||||||
|
id_mapping: vec![],
|
||||||
|
lerp_amount: 0.0,
|
||||||
|
generational_lenience: 3,
|
||||||
|
survival_rate: 0.5,
|
||||||
|
};
|
||||||
|
let id_str = nn.get_individual_id(42);
|
||||||
|
assert!(id_str.contains("fighter_nn_"));
|
||||||
|
assert!(id_str.ends_with("_42"));
|
||||||
|
assert!(id_str.starts_with(&format!("{:06}_", nn.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_score_from_file_found() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let file_path = dir.path().join("score.txt");
|
||||||
|
let nn_id = "test_nn";
|
||||||
|
let mut file = File::create(&file_path).unwrap();
|
||||||
|
writeln!(file, "{}: 123.45", nn_id).unwrap();
|
||||||
|
let score = block_on(read_score_from_file(&file_path, nn_id)).unwrap();
|
||||||
|
assert!((score - 123.45).abs() < 1e-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_score_from_file_not_found() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let file_path = dir.path().join("score.txt");
|
||||||
|
let nn_id = "not_in_file";
|
||||||
|
let mut file = File::create(&file_path).unwrap();
|
||||||
|
writeln!(file, "other_nn: 1.0").unwrap();
|
||||||
|
let result = block_on(read_score_from_file(&file_path, nn_id));
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_1v1_simulation_reads_existing_score() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let nn_id1 = "nn1";
|
||||||
|
let nn_id2 = "nn2";
|
||||||
|
let file_path = dir.path().join(format!("{}_vs_{}.txt", nn_id1, nn_id2));
|
||||||
|
let mut file = File::create(&file_path).unwrap();
|
||||||
|
writeln!(file, "{}: 10.0", nn_id1).unwrap();
|
||||||
|
writeln!(file, "{}: 20.0", nn_id2).unwrap();
|
||||||
|
let nn_path_1 = dir.path().join(format!("{}.net", nn_id1));
|
||||||
|
let nn_path_2 = dir.path().join(format!("{}.net", nn_id2));
|
||||||
|
let result = block_on(run_1v1_simulation(&nn_path_1, &nn_path_2, false)).unwrap();
|
||||||
|
assert_eq!(result, (10.0, 20.0));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_weighted_random_selection() {
|
fn test_weighted_random_selection() {
|
||||||
|
|
|
@ -409,11 +409,8 @@ pub fn crossbreed_neuron_arrays(
|
||||||
current_layer += 1;
|
current_layer += 1;
|
||||||
}
|
}
|
||||||
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
|
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
|
||||||
if is_primary {
|
// The first segment is always from the primary network, so we can set primary_last_layer here
|
||||||
primary_last_layer = current_layer;
|
primary_last_layer = current_layer;
|
||||||
} else {
|
|
||||||
secondary_last_layer = current_layer;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -452,8 +449,9 @@ pub fn crossbreed_neuron_arrays(
|
||||||
let highest_id = earlier_layer_neurons
|
let highest_id = earlier_layer_neurons
|
||||||
.iter()
|
.iter()
|
||||||
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
|
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
|
||||||
if let Some(highest_id) = highest_id {
|
if let Some(highest_id) = highest_id
|
||||||
if highest_id.1 == is_primary {
|
&& highest_id.1 == is_primary
|
||||||
|
{
|
||||||
let neurons_to_add = target_neurons
|
let neurons_to_add = target_neurons
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(id, l)| {
|
.filter(|(id, l)| {
|
||||||
|
@ -470,7 +468,6 @@ pub fn crossbreed_neuron_arrays(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
new_neurons.push((*neuron_id, is_primary, *layer, 0));
|
new_neurons.push((*neuron_id, is_primary, *layer, 0));
|
||||||
|
|
||||||
|
@ -489,19 +486,15 @@ pub fn crossbreed_neuron_arrays(
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let highest_id =
|
let highest_id =
|
||||||
current_layer_neurons.iter().max_by_key(|(id, _, _, _)| id);
|
current_layer_neurons.iter().max_by_key(|(id, _, _, _)| id);
|
||||||
if let Some(highest_id) = highest_id {
|
if let Some(highest_id) = highest_id
|
||||||
if highest_id.1 == is_primary {
|
&& highest_id.1 == is_primary
|
||||||
|
{
|
||||||
let neurons_to_add = target_neurons
|
let neurons_to_add = target_neurons
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(id, l)| id > &highest_id.0 && *l == layer - 1)
|
.filter(|(id, l)| id > &highest_id.0 && *l == layer - 1)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
for (neuron_id, _) in neurons_to_add {
|
for (neuron_id, _) in neurons_to_add {
|
||||||
new_neurons.push((
|
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
|
||||||
*neuron_id,
|
|
||||||
is_primary,
|
|
||||||
current_layer,
|
|
||||||
0,
|
|
||||||
));
|
|
||||||
|
|
||||||
if is_primary {
|
if is_primary {
|
||||||
primary_last_layer = current_layer;
|
primary_last_layer = current_layer;
|
||||||
|
@ -510,7 +503,6 @@ pub fn crossbreed_neuron_arrays(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If it's in a future layer, move to the next layer
|
// If it's in a future layer, move to the next layer
|
||||||
current_layer += 1;
|
current_layer += 1;
|
||||||
|
@ -563,7 +555,9 @@ pub fn crossbreed_neuron_arrays(
|
||||||
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
|
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
} else if *neuron_id == &segments.last().unwrap().1 + 1 {
|
}
|
||||||
|
// If the neuron id is exactly one more than the last neuron id, we need to ensure that there's at least one neuron from the same individual in the previous layer
|
||||||
|
else if *neuron_id == &segments.last().unwrap().1 + 1 {
|
||||||
let target_layer = if is_primary {
|
let target_layer = if is_primary {
|
||||||
primary_last_layer
|
primary_last_layer
|
||||||
} else {
|
} else {
|
||||||
|
@ -579,8 +573,9 @@ pub fn crossbreed_neuron_arrays(
|
||||||
let highest_id = earlier_layer_neurons
|
let highest_id = earlier_layer_neurons
|
||||||
.iter()
|
.iter()
|
||||||
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
|
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
|
||||||
if let Some(highest_id) = highest_id {
|
if let Some(highest_id) = highest_id
|
||||||
if highest_id.1 == is_primary {
|
&& highest_id.1 == is_primary
|
||||||
|
{
|
||||||
let neurons_to_add = target_neurons
|
let neurons_to_add = target_neurons
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(id, _)| id > &highest_id.0 && id < neuron_id)
|
.filter(|(id, _)| id > &highest_id.0 && id < neuron_id)
|
||||||
|
@ -589,7 +584,6 @@ pub fn crossbreed_neuron_arrays(
|
||||||
new_neurons.push((*neuron_id, is_primary, *l, 0));
|
new_neurons.push((*neuron_id, is_primary, *l, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
new_neurons.push((*neuron_id, is_primary, *layer, 0));
|
new_neurons.push((*neuron_id, is_primary, *layer, 0));
|
||||||
} else {
|
} else {
|
||||||
|
@ -808,6 +802,45 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crossbreed_basic_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Create a dummy FighterNN
|
||||||
|
let fighter_nn = FighterNN {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
folder: std::path::PathBuf::from("/tmp"),
|
||||||
|
population_size: 2,
|
||||||
|
generation: 0,
|
||||||
|
scores: vec![],
|
||||||
|
nn_shapes: vec![],
|
||||||
|
crossbreed_segments: 1,
|
||||||
|
weight_initialization_range: -0.5..0.5,
|
||||||
|
minor_mutation_rate: 0.1,
|
||||||
|
major_mutation_rate: 0.1,
|
||||||
|
mutation_weight_range: -0.5..0.5,
|
||||||
|
id_mapping: vec![],
|
||||||
|
lerp_amount: 0.5,
|
||||||
|
generational_lenience: 1,
|
||||||
|
survival_rate: 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use very small networks to avoid FANN memory/resource issues
|
||||||
|
let primary = Fann::new(&[5, 3, 3])?;
|
||||||
|
let secondary = Fann::new(&[5, 3, 3])?;
|
||||||
|
|
||||||
|
// Run crossbreed
|
||||||
|
let result = crossbreed(&fighter_nn, &primary, &secondary, 3)?;
|
||||||
|
|
||||||
|
// Check that the result has the correct input and output size
|
||||||
|
let shape = result.get_layer_sizes();
|
||||||
|
assert_eq!(shape[0], 5);
|
||||||
|
assert_eq!(*shape.last().unwrap(), 3);
|
||||||
|
// All hidden layers should have at least 1 neuron
|
||||||
|
for (i, &layer_size) in shape.iter().enumerate().skip(1).take(shape.len() - 2) {
|
||||||
|
assert!(layer_size > 0, "Hidden layer {} has zero neurons", i);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn major_mutation_test() -> Result<(), Box<dyn std::error::Error>> {
|
fn major_mutation_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Assign
|
// Assign
|
||||||
|
@ -1493,6 +1526,51 @@ mod tests {
|
||||||
assert_eq!(result_set, expected);
|
assert_eq!(result_set, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crossbreed_neuron_arrays_secondary_last_layer_final_segment() {
|
||||||
|
// Use 3 segments and larger neuron arrays to ensure the else-if branch is covered
|
||||||
|
let segments = vec![(0, 2), (3, 5), (6, 8)];
|
||||||
|
// secondary_neurons: (id, layer)
|
||||||
|
let primary_neurons = generate_neuron_datastructure(&vec![3, 3, 3, 3, 2]);
|
||||||
|
let secondary_neurons = generate_neuron_datastructure(&vec![3, 3, 3, 3, 3]);
|
||||||
|
// The last segment is (6, 8), so neuron 9 in secondary_neurons will trigger the else-if branch
|
||||||
|
|
||||||
|
let result = crossbreed_neuron_arrays(segments, primary_neurons, secondary_neurons);
|
||||||
|
|
||||||
|
// Assert: The result should contain a secondary neuron with id 9 and layer 3
|
||||||
|
let has_secondary_9_layer_3 = result
|
||||||
|
.iter()
|
||||||
|
.any(|&(id, is_primary, layer, _)| id == 9 && !is_primary && layer == 3);
|
||||||
|
assert!(
|
||||||
|
has_secondary_9_layer_3,
|
||||||
|
"Expected a secondary neuron with id 9 in layer 3, indicating secondary_last_layer was used"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crossbreed_neuron_arrays_prune_layer_exceeds_max() {
|
||||||
|
// Use the real constant from the module
|
||||||
|
let max = NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MAX;
|
||||||
|
let layer = 1;
|
||||||
|
// Create more than max neurons in layer 1, alternating primary/secondary
|
||||||
|
let primary_neurons = generate_neuron_datastructure(&vec![1, (max + 3) as u32, 1]);
|
||||||
|
let secondary_neurons = generate_neuron_datastructure(&vec![1, (max + 3) as u32, 1]);
|
||||||
|
// Segments: one for input, one for the large layer, one for output
|
||||||
|
let segments = vec![
|
||||||
|
(0, 0),
|
||||||
|
(1, (max + 3) as u32),
|
||||||
|
((max + 4) as u32, (max + 4) as u32),
|
||||||
|
];
|
||||||
|
let result = crossbreed_neuron_arrays(segments, primary_neurons, secondary_neurons);
|
||||||
|
|
||||||
|
// Count neurons in layer 1
|
||||||
|
let count = result.iter().filter(|(_, _, l, _)| *l == layer).count();
|
||||||
|
assert!(
|
||||||
|
count <= max,
|
||||||
|
"Layer should be pruned to NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MAX neurons"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_neuron_datastructure_test() {
|
fn generate_neuron_datastructure_test() {
|
||||||
// Assign
|
// Assign
|
||||||
|
@ -1866,6 +1944,92 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup for bias connection fallback
|
||||||
|
// We'll target hidden layer 3 (layer index 3), neuron 20 (first in that layer, is_primary = false)
|
||||||
|
let bias_layer = 3;
|
||||||
|
let bias_neuron_primary = get_bias_neuron_for_layer(bias_layer, &primary_shape).unwrap();
|
||||||
|
|
||||||
|
// Add a bias connection in primary if not present
|
||||||
|
let mut primary_connections = primary_fann.get_connections();
|
||||||
|
for connection in primary_connections.iter_mut() {
|
||||||
|
if connection.from_neuron == bias_neuron_primary {
|
||||||
|
// Set to a unique weight to verify it's not used
|
||||||
|
connection.weight = 12345.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primary_fann.set_connections(&primary_connections);
|
||||||
|
|
||||||
|
// Secondary network needs to have only 1 hidden layer, so it won't have a bias connection for layer 3
|
||||||
|
let secondary_shape = vec![4, 8, 6];
|
||||||
|
let mut secondary_fann = Fann::new(&secondary_shape)?;
|
||||||
|
let mut secondary_connections = secondary_fann.get_connections();
|
||||||
|
for connection in secondary_connections.iter_mut() {
|
||||||
|
connection.weight = ((connection.from_neuron * 100) + connection.to_neuron) as f32;
|
||||||
|
connection.weight = connection.weight * -1.0;
|
||||||
|
}
|
||||||
|
secondary_fann.set_connections(&secondary_connections);
|
||||||
|
|
||||||
|
let new_neurons = vec![
|
||||||
|
// Input layer: Expect 4
|
||||||
|
(0, true, 0, 0),
|
||||||
|
(1, true, 0, 1),
|
||||||
|
(2, true, 0, 2),
|
||||||
|
(3, true, 0, 3),
|
||||||
|
// Hidden Layer 1: Expect 8
|
||||||
|
(4, false, 1, 4),
|
||||||
|
(5, false, 1, 5),
|
||||||
|
(6, false, 1, 6),
|
||||||
|
(7, true, 1, 7),
|
||||||
|
(8, true, 1, 8),
|
||||||
|
(9, true, 1, 9),
|
||||||
|
(10, true, 1, 10),
|
||||||
|
(11, true, 1, 11),
|
||||||
|
// Hidden Layer 2: Expect 6
|
||||||
|
(12, true, 2, 12),
|
||||||
|
(13, true, 2, 13),
|
||||||
|
(14, true, 2, 14),
|
||||||
|
(15, true, 2, 15),
|
||||||
|
(16, true, 2, 16),
|
||||||
|
(17, true, 2, 17),
|
||||||
|
// Output Layer: Expect 4
|
||||||
|
(18, true, 3, 18),
|
||||||
|
(19, true, 3, 19),
|
||||||
|
(20, false, 3, 20),
|
||||||
|
(21, true, 3, 21),
|
||||||
|
];
|
||||||
|
let new_shape = vec![4, 8, 6, 4];
|
||||||
|
let mut new_fann = Fann::new(&new_shape)?;
|
||||||
|
// Initialize weights to 0
|
||||||
|
let mut new_connections = new_fann.get_connections();
|
||||||
|
for connection in new_connections.iter_mut() {
|
||||||
|
connection.weight = 0.0;
|
||||||
|
}
|
||||||
|
new_fann.set_connections(&new_connections);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
consolidate_old_connections(
|
||||||
|
&primary_fann,
|
||||||
|
&secondary_fann,
|
||||||
|
new_shape,
|
||||||
|
new_neurons,
|
||||||
|
&mut new_fann,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert that the fallback bias connection was used
|
||||||
|
let new_connections = new_fann.get_connections();
|
||||||
|
|
||||||
|
for connection in new_connections.iter() {
|
||||||
|
println!("{:?}", connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
let found = new_connections
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.from_neuron == bias_neuron_primary && c.weight == 12345.0);
|
||||||
|
assert!(
|
||||||
|
found,
|
||||||
|
"Expected fallback bias connection from primary network to be used"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue