diff --git a/gemla/Cargo.toml b/gemla/Cargo.toml index 4fc28e1..6119210 100644 --- a/gemla/Cargo.toml +++ b/gemla/Cargo.toml @@ -30,3 +30,4 @@ smol = "2.0.0" smol-potat = "1.1.2" num_cpus = "1.16.0" easy-parallel = "3.3.1" +fann = "0.1.8" diff --git a/gemla/build.rs b/gemla/build.rs new file mode 100644 index 0000000..e6b8ca6 --- /dev/null +++ b/gemla/build.rs @@ -0,0 +1,11 @@ +fn main() { + // Replace this with the path to the directory containing `fann.lib` + let lib_dir = "F://vandomej/Downloads/vcpkg/packages/fann_x64-windows/lib"; + + println!("cargo:rustc-link-search=native={}", lib_dir); + println!("cargo:rustc-link-lib=static=fann"); + // Use `dylib=fann` instead of `static=fann` if you're linking dynamically + + // If there are any additional directories where the compiler can find header files, you can specify them like this: + // println!("cargo:include={}", path_to_include_directory); +} diff --git a/gemla/config.json b/gemla/config.json new file mode 100644 index 0000000..6c1c941 --- /dev/null +++ b/gemla/config.json @@ -0,0 +1,3 @@ +{ + "base_dir": "F:\\\\vandomej\\Projects\\dootcamp-AI-Simulation\\Simulations" +} \ No newline at end of file diff --git a/gemla/nodes.toml b/gemla/nodes.toml deleted file mode 100644 index 976061d..0000000 --- a/gemla/nodes.toml +++ /dev/null @@ -1,15 +0,0 @@ -[[nodes]] -fabric_addr = "10.0.0.1:9999" -bridge_bind = "10.0.0.1:8888" -mem = "100 GiB" -cpu = 8 - -# [[nodes]] -# fabric_addr = "10.0.0.2:9999" -# mem = "100 GiB" -# cpu = 16 - -# [[nodes]] -# fabric_addr = "10.0.0.3:9999" -# mem = "100 GiB" -# cpu = 16 \ No newline at end of file diff --git a/gemla/src/bin/bin.rs b/gemla/src/bin/bin.rs index 2618232..1455d7c 100644 --- a/gemla/src/bin/bin.rs +++ b/gemla/src/bin/bin.rs @@ -4,6 +4,7 @@ extern crate gemla; extern crate log; mod test_state; +mod fighter_nn; use easy_parallel::Parallel; use file_linked::constants::data_format::DataFormat; @@ -13,7 +14,7 @@ use gemla::{ }; use smol::{channel, channel::RecvError, future, Executor}; use std::{path::PathBuf, time::Instant}; -use test_state::TestState; +use fighter_nn::FighterNN; use clap::Parser; #[derive(Parser)] @@ -52,7 +53,7 @@ fn main() -> anyhow::Result<()> { let args = Args::parse(); // Checking that the first argument is a valid file - let mut gemla = log_error(Gemla::::new( + let mut gemla = log_error(Gemla::::new( &PathBuf::from(args.file), GemlaConfig { generations_per_node: 3, diff --git a/gemla/src/bin/fighter_nn/mod.rs b/gemla/src/bin/fighter_nn/mod.rs new file mode 100644 index 0000000..4633ffb --- /dev/null +++ b/gemla/src/bin/fighter_nn/mod.rs @@ -0,0 +1,230 @@ +extern crate fann; + +use std::{fs::{self, File}, path::PathBuf}; +use fann::{ActivationFunc, Fann}; +use gemla::{core::genetic_node::GeneticNode, error::Error}; +use rand::prelude::*; +use serde::{Deserialize, Serialize}; +use serde_json; +use anyhow::Context; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Config { + base_dir: String, +} + +// Here is the folder structure for the FighterNN: +// base_dir/fighter_nn_{fighter_id}/{generation}/{fighter_id}_fighter_nn_{nn_id}.net + +// A neural network that utilizes the fann library to save and read nn's from files +// FighterNN contains a list of file locations for the nn's stored, all of which are stored under the same folder which is also contained. +// there is no training happening to the neural networks +// the neural networks are only used to simulate the nn's and to save and read the nn's from files +// Filenames are stored in the format of "{fighter_id}_fighter_nn_{generation}.net". +// The folder name is stored in the format of "fighter_nn_xxxxxx" where xxxxxx is an incrementing number, checking for the highest number and incrementing it by 1 +// The main folder contains a subfolder for each generation, containing a population of 10 nn's + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FighterNN { + pub id: u64, + pub folder: PathBuf, + pub generation: u64, + // A map of each nn identifier in a generation and their physics score + pub scores: Vec>, +} + +impl GeneticNode for FighterNN { + // Check for the highest number of the folder name and increment it by 1 + fn initialize() -> Result, Error> { + // Load the configuration + let config: Config = serde_json::from_reader(File::open("config.json")?) + .with_context(|| format!("Failed to read config"))?; + + let base_path = PathBuf::from(config.base_dir); + + // Ensure the base directory exists, create it if not + if !base_path.exists() { + fs::create_dir_all(&base_path)?; + } + + let mut highest = 0; + let mut folder = base_path.join(format!("fighter_nn_{:06}", highest)); + while folder.exists() { + highest += 1; + folder = base_path.join(format!("fighter_nn_{:06}", highest)); + } + + fs::create_dir(&folder)?; + + //Create a new directory for the first generation + let gen_folder = folder.join("0"); + fs::create_dir(&gen_folder)?; + + // Create the first generation in this folder + for i in 0..10 { + // Filenames are stored in the format of "xxxxxx_fighter_nn_0.net", "xxxxxx_fighter_nn_1.net", etc. Where xxxxxx is the folder name + let nn = gen_folder.join(format!("{:06}_fighter_nn_{}.net", highest, i)); + let mut fann = Fann::new(&[10, 10, 10]) + .with_context(|| format!("Failed to create nn"))?; + fann.set_activation_func_hidden(ActivationFunc::SigmoidSymmetric); + fann.set_activation_func_output(ActivationFunc::SigmoidSymmetric); + fann.save(&nn) + .with_context(|| format!("Failed to save nn"))?; + } + + Ok(Box::new(FighterNN { + id: highest, + folder, + generation: 0, + scores: vec![HashMap::new()], + })) + } + + fn simulate(&mut self) -> Result<(), Error> { + // For each nn in the current generation: + for i in 0..10 { + // load the nn + let nn = self.folder.join(format!("{}", self.generation)).join(format!("{:06}_fighter_nn_{}.net", self.id, i)); + let fann = Fann::from_file(&nn) + .with_context(|| format!("Failed to load nn"))?; + + // Simulate the nn against the random nn + let mut score = 0.0; + + // Using the same original nn, repeat the simulation with 5 random nn's from the current generation + for _ in 0..5 { + let random_nn = self.folder.join(format!("{}", self.generation)).join(format!("{:06}_fighter_nn_{}.net", self.id, thread_rng().gen_range(0..10))); + let random_fann = Fann::from_file(&random_nn) + .with_context(|| format!("Failed to load random nn"))?; + + let inputs: Vec = (0..10).map(|_| thread_rng().gen_range(-1.0..1.0)).collect(); + let outputs = fann.run(&inputs) + .with_context(|| format!("Failed to run nn"))?; + let random_outputs = random_fann.run(&inputs) + .with_context(|| format!("Failed to run random nn"))?; + + // Average the difference between the outputs of the nn and random_nn and add the result to score + let mut round_score = 0.0; + for (o, r) in outputs.iter().zip(random_outputs.iter()) { + round_score += o - r; + } + score += round_score / fann.get_num_output() as f32; + + } + + score /= 5.0; + self.scores[self.generation as usize].insert(i, score); + } + + Ok(()) + } + + + fn mutate(&mut self) -> Result<(), Error> { + // Create the new generation folder + let new_gen_folder = self.folder.join(format!("{}", self.generation + 1)); + fs::create_dir(&new_gen_folder)?; + + // Remove the 5 nn's with the lowest scores + let mut sorted_scores: Vec<_> = self.scores[self.generation as usize].iter().collect(); + sorted_scores.sort_by(|a, b| a.1.partial_cmp(b.1).unwrap()); + let to_keep = sorted_scores[5..].iter().map(|(k, _)| *k).collect::>(); + + // Save the remaining 5 nn's to the new generation folder + for i in 0..5 { + let nn_id = to_keep[i]; + let nn = self.folder.join(format!("{}", self.generation)).join(format!("{:06}_fighter_nn_{}.net", self.id, nn_id)); + let new_nn = new_gen_folder.join(format!("{:06}_fighter_nn_{}.net", self.id, i)); + fs::copy(&nn, &new_nn)?; + } + + // Take the remaining 5 nn's and create 5 new nn's by the following: + for i in 0..5 { + let nn_id = to_keep[i]; + let nn = self.folder.join(format!("{}", self.generation)).join(format!("{:06}_fighter_nn_{}.net", self.id, nn_id)); + let mut fann = Fann::from_file(&nn) + .with_context(|| format!("Failed to load nn"))?; + + // For each weight in the 5 new nn's there is a 20% chance of a minor mutation (a random number between -0.1 and 0.1 is added to the weight) + // And a 5% chance of a major mutation (a random number between -0.3 and 0.3 is added to the weight) + let mut connections = fann.get_connections(); // Vector of connections + for c in &mut connections { + if thread_rng().gen_range(0..100) < 20 { + c.weight += thread_rng().gen_range(-0.1..0.1); + } else if thread_rng().gen_range(0..100) < 5 { + c.weight += thread_rng().gen_range(-0.3..0.3); + } + } + fann.set_connections(&connections); + + // Save the new nn's to the new generation folder + let new_nn = new_gen_folder.join(format!("{:06}_fighter_nn_{}.net", self.id, i + 5)); + fann.save(&new_nn) + .with_context(|| format!("Failed to save nn"))?; + } + + self.generation += 1; + self.scores.push(HashMap::new()); + + Ok(()) + } + + fn merge(left: &FighterNN, right: &FighterNN) -> Result, Error> { + // Find next highest + // Load the configuration + let config: Config = serde_json::from_reader(File::open("config.json")?) + .with_context(|| format!("Failed to read config"))?; + + let base_path = PathBuf::from(config.base_dir); + + // Ensure the base directory exists, create it if not + if !base_path.exists() { + fs::create_dir_all(&base_path)?; + } + + let mut highest = 0; + let mut folder = base_path.join(format!("fighter_nn_{:06}", highest)); + while folder.exists() { + highest += 1; + folder = base_path.join(format!("fighter_nn_{:06}", highest)); + } + + fs::create_dir(&folder)?; + + //Create a new directory for the first generation + let gen_folder = folder.join("0"); + fs::create_dir(&gen_folder)?; + + // Take the 5 nn's with the highest scores from the left nn's and save them to the new fighter folder + let mut sorted_scores: Vec<_> = left.scores[left.generation as usize].iter().collect(); + sorted_scores.sort_by(|a, b| a.1.partial_cmp(b.1).unwrap()); + let mut remaining = sorted_scores[5..].iter().map(|(k, _)| *k).collect::>(); + for i in 0..5 { + let nn = left.folder.join(format!("{}", left.generation)).join(format!("{:06}_fighter_nn_{}.net", left.id, remaining.pop().unwrap())); + let new_nn = folder.join(format!("0")).join(format!("{:06}_fighter_nn_{}.net", highest, i)); + trace!("From: {:?}, To: {:?}", &nn, &new_nn); + fs::copy(&nn, &new_nn) + .with_context(|| format!("Failed to copy left nn"))?; + } + + // Take the 5 nn's with the highest scores from the right nn's and save them to the new fighter folder + sorted_scores = right.scores[right.generation as usize].iter().collect(); + sorted_scores.sort_by(|a, b| a.1.partial_cmp(b.1).unwrap()); + remaining = sorted_scores[5..].iter().map(|(k, _)| *k).collect::>(); + for i in 5..10 { + let nn = right.folder.join(format!("{}", right.generation)).join(format!("{:06}_fighter_nn_{}.net", right.id, remaining.pop().unwrap())); + let new_nn = folder.join(format!("0")).join(format!("{:06}_fighter_nn_{}.net", highest, i)); + trace!("From: {:?}, To: {:?}", &nn, &new_nn); + fs::copy(&nn, &new_nn) + .with_context(|| format!("Failed to copy right nn"))?; + } + + Ok(Box::new(FighterNN { + id: highest, + folder, + generation: 0, + scores: vec![HashMap::new()], + })) + } +}