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"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
tempfile = "3.21.0"
|
||||
tokio = { version = "1.47.1", features = ["full"] }
|
||||
tokio-test = "0.4.4"
|
||||
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)]
|
||||
pub mod test {
|
||||
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]
|
||||
fn test_weighted_random_selection() {
|
||||
|
|
|
@ -409,11 +409,8 @@ pub fn crossbreed_neuron_arrays(
|
|||
current_layer += 1;
|
||||
}
|
||||
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
|
||||
if is_primary {
|
||||
primary_last_layer = current_layer;
|
||||
} else {
|
||||
secondary_last_layer = current_layer;
|
||||
}
|
||||
// The first segment is always from the primary network, so we can set primary_last_layer here
|
||||
primary_last_layer = current_layer;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
@ -452,22 +449,22 @@ pub fn crossbreed_neuron_arrays(
|
|||
let highest_id = earlier_layer_neurons
|
||||
.iter()
|
||||
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
|
||||
if let Some(highest_id) = highest_id {
|
||||
if highest_id.1 == is_primary {
|
||||
let neurons_to_add = target_neurons
|
||||
.iter()
|
||||
.filter(|(id, l)| {
|
||||
id > &highest_id.0 && id < neuron_id && l == layer
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (neuron_id, layer) in neurons_to_add {
|
||||
new_neurons.push((*neuron_id, is_primary, *layer, 0));
|
||||
if let Some(highest_id) = highest_id
|
||||
&& highest_id.1 == is_primary
|
||||
{
|
||||
let neurons_to_add = target_neurons
|
||||
.iter()
|
||||
.filter(|(id, l)| {
|
||||
id > &highest_id.0 && id < neuron_id && l == layer
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (neuron_id, layer) in neurons_to_add {
|
||||
new_neurons.push((*neuron_id, is_primary, *layer, 0));
|
||||
|
||||
if is_primary {
|
||||
primary_last_layer = *layer;
|
||||
} else {
|
||||
secondary_last_layer = *layer;
|
||||
}
|
||||
if is_primary {
|
||||
primary_last_layer = *layer;
|
||||
} else {
|
||||
secondary_last_layer = *layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -489,25 +486,20 @@ pub fn crossbreed_neuron_arrays(
|
|||
.collect::<Vec<_>>();
|
||||
let highest_id =
|
||||
current_layer_neurons.iter().max_by_key(|(id, _, _, _)| id);
|
||||
if let Some(highest_id) = highest_id {
|
||||
if highest_id.1 == is_primary {
|
||||
let neurons_to_add = target_neurons
|
||||
.iter()
|
||||
.filter(|(id, l)| id > &highest_id.0 && *l == layer - 1)
|
||||
.collect::<Vec<_>>();
|
||||
for (neuron_id, _) in neurons_to_add {
|
||||
new_neurons.push((
|
||||
*neuron_id,
|
||||
is_primary,
|
||||
current_layer,
|
||||
0,
|
||||
));
|
||||
if let Some(highest_id) = highest_id
|
||||
&& highest_id.1 == is_primary
|
||||
{
|
||||
let neurons_to_add = target_neurons
|
||||
.iter()
|
||||
.filter(|(id, l)| id > &highest_id.0 && *l == layer - 1)
|
||||
.collect::<Vec<_>>();
|
||||
for (neuron_id, _) in neurons_to_add {
|
||||
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
|
||||
|
||||
if is_primary {
|
||||
primary_last_layer = current_layer;
|
||||
} else {
|
||||
secondary_last_layer = current_layer;
|
||||
}
|
||||
if is_primary {
|
||||
primary_last_layer = current_layer;
|
||||
} else {
|
||||
secondary_last_layer = current_layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -563,7 +555,9 @@ pub fn crossbreed_neuron_arrays(
|
|||
new_neurons.push((*neuron_id, is_primary, current_layer, 0));
|
||||
}
|
||||
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 {
|
||||
primary_last_layer
|
||||
} else {
|
||||
|
@ -579,15 +573,15 @@ pub fn crossbreed_neuron_arrays(
|
|||
let highest_id = earlier_layer_neurons
|
||||
.iter()
|
||||
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)));
|
||||
if let Some(highest_id) = highest_id {
|
||||
if highest_id.1 == is_primary {
|
||||
let neurons_to_add = target_neurons
|
||||
.iter()
|
||||
.filter(|(id, _)| id > &highest_id.0 && id < neuron_id)
|
||||
.collect::<Vec<_>>();
|
||||
for (neuron_id, l) in neurons_to_add {
|
||||
new_neurons.push((*neuron_id, is_primary, *l, 0));
|
||||
}
|
||||
if let Some(highest_id) = highest_id
|
||||
&& highest_id.1 == is_primary
|
||||
{
|
||||
let neurons_to_add = target_neurons
|
||||
.iter()
|
||||
.filter(|(id, _)| id > &highest_id.0 && id < neuron_id)
|
||||
.collect::<Vec<_>>();
|
||||
for (neuron_id, l) in neurons_to_add {
|
||||
new_neurons.push((*neuron_id, is_primary, *l, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -808,6 +802,45 @@ mod tests {
|
|||
|
||||
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]
|
||||
fn major_mutation_test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Assign
|
||||
|
@ -1493,6 +1526,51 @@ mod tests {
|
|||
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]
|
||||
fn generate_neuron_datastructure_test() {
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue