Adjusting tree representation and adding scaling

This commit is contained in:
vandomej 2021-10-08 00:33:39 -07:00
parent 9b733c88f5
commit c2ce591d6b
6 changed files with 215 additions and 220 deletions

View file

@ -5,9 +5,10 @@ extern crate gemla;
mod test_state; mod test_state;
use clap::App; use clap::App;
use gemla::bracket::Gemla; use gemla::core::{Gemla, GemlaConfig};
use std::path::PathBuf; use std::path::PathBuf;
use test_state::TestState; use test_state::TestState;
// use std::io::Write;
/// Runs a simluation of a genetic algorithm against a dataset. /// Runs a simluation of a genetic algorithm against a dataset.
/// ///
@ -21,9 +22,18 @@ fn main() -> anyhow::Result<()> {
// Checking that the first argument <DIRECTORY> is a valid directory // Checking that the first argument <DIRECTORY> is a valid directory
let file_path = matches.value_of(gemla::constants::args::FILE).unwrap(); let file_path = matches.value_of(gemla::constants::args::FILE).unwrap();
let mut gemla = Gemla::<TestState>::new(&PathBuf::from(file_path), true)?; let mut gemla = Gemla::<TestState>::new(
&PathBuf::from(file_path),
GemlaConfig {
generations_per_node: 10,
overwrite: true,
},
)?;
gemla.simulate(17)?; gemla.simulate(10)?;
// let mut f = std::fs::File::create("./test")?;
// write!(f, "{}", serde_json::to_string(&gemla.data.readonly().0)?)?;
Ok(()) Ok(())
} }

View file

@ -1,4 +1,4 @@
use gemla::bracket::genetic_node::GeneticNode; use gemla::core::genetic_node::GeneticNode;
use gemla::error; use gemla::error;
use rand::prelude::*; use rand::prelude::*;
use rand::thread_rng; use rand::thread_rng;
@ -18,22 +18,20 @@ impl GeneticNode for TestState {
let mut population: Vec<i64> = vec![]; let mut population: Vec<i64> = vec![];
for _ in 0..POPULATION_SIZE { for _ in 0..POPULATION_SIZE {
population.push(thread_rng().gen_range(0..10000)) population.push(thread_rng().gen_range(0..100))
} }
Ok(Box::new(TestState { population })) Ok(Box::new(TestState { population }))
} }
fn simulate(&mut self, iterations: u64) -> Result<(), error::Error> { fn simulate(&mut self) -> Result<(), error::Error> {
let mut rng = thread_rng(); let mut rng = thread_rng();
for _ in 0..iterations { self.population = self
self.population = self .population
.population .iter()
.iter() .map(|p| p.saturating_add(rng.gen_range(-1..2)))
.map(|p| p + rng.gen_range(-10..10)) .collect();
.collect()
}
Ok(()) Ok(())
} }
@ -67,7 +65,8 @@ impl GeneticNode for TestState {
let mut new_individual = self.population.clone()[new_individual_index]; let mut new_individual = self.population.clone()[new_individual_index];
let cross_breed = self.population.clone()[cross_breed_index]; let cross_breed = self.population.clone()[cross_breed_index];
new_individual += cross_breed + rng.gen_range(-10..10); new_individual = (new_individual.saturating_add(cross_breed) / 2)
.saturating_add(rng.gen_range(-1..2));
self.population.push(new_individual); self.population.push(new_individual);
} }
@ -95,7 +94,7 @@ impl GeneticNode for TestState {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use gemla::bracket::genetic_node::GeneticNode; use gemla::core::genetic_node::GeneticNode;
#[test] #[test]
fn test_initialize() { fn test_initialize() {
@ -112,20 +111,18 @@ mod tests {
let original_population = state.population.clone(); let original_population = state.population.clone();
state.simulate(0).unwrap(); state.simulate().unwrap();
assert_eq!(original_population, state.population);
state.simulate(1).unwrap();
assert!(original_population assert!(original_population
.iter() .iter()
.zip(state.population.iter()) .zip(state.population.iter())
.all(|(&a, &b)| b >= a - 10 && b <= a + 10)); .all(|(&a, &b)| b >= a - 1 && b <= a + 2));
state.simulate(2).unwrap(); state.simulate().unwrap();
state.simulate().unwrap();
assert!(original_population assert!(original_population
.iter() .iter()
.zip(state.population.iter()) .zip(state.population.iter())
.all(|(&a, &b)| b >= a - 30 && b <= a + 30)) .all(|(&a, &b)| b >= a - 3 && b <= a + 6))
} }
#[test] #[test]

View file

@ -1,154 +0,0 @@
//! A trait used to interact with the internal state of nodes within the [`Bracket`]
//!
//! [`Bracket`]: crate::bracket::Bracket
use crate::error::Error;
use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
/// An enum used to control the state of a [`GeneticNode`]
///
/// [`GeneticNode`]: crate::bracket::genetic_node
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "enumType", content = "enumContent")]
pub enum GeneticState {
/// The node and it's data have not finished initializing
Initialize,
/// The node is currently simulating a round against target data to determine the fitness of the population
Simulate,
/// The node is currently mutating members of it's population and breeding new members
Mutate,
/// The node has finished processing for a given number of iterations
Finish,
}
/// A trait used to interact with the internal state of nodes within the [`Bracket`]
///
/// [`Bracket`]: crate::bracket::Bracket
pub trait GeneticNode {
/// Initializes a new instance of a [`GeneticState`].
///
/// # Examples
/// TODO
fn initialize() -> Result<Box<Self>, Error>;
/// Runs a simulation on the state object for the given number of `iterations` in order to guage it's fitness.
/// This will be called for every node in a bracket before evaluating it's fitness against other nodes.
///
/// # Examples
/// TODO
fn simulate(&mut self, iterations: u64) -> Result<(), Error>;
/// Mutates members in a population and/or crossbreeds them to produce new offspring.
///
/// # Examples
/// TODO
fn mutate(&mut self) -> Result<(), Error>;
fn merge(left: &Self, right: &Self) -> Result<Box<Self>, Error>;
}
/// Used externally to wrap a node implementing the [`GeneticNode`] trait. Processes state transitions for the given node as
/// well as signal recovery. Transition states are given by [`GeneticState`]
#[derive(Debug, Serialize, Deserialize)]
pub struct GeneticNodeWrapper<T> {
pub data: Option<T>,
state: GeneticState,
pub iteration: u64,
}
impl<T> GeneticNodeWrapper<T>
where
T: GeneticNode + Debug,
{
/// Initializes a wrapper around a GeneticNode. If the initialization is successful the internal state will be changed to
/// `GeneticState::Simulate` otherwise it will remain as `GeneticState::Initialize` and will attempt to be created in
/// [`process_node`](#method.process_node).
///
/// # Examples
/// TODO
pub fn new() -> Result<Self, Error> {
let mut node = GeneticNodeWrapper {
data: None,
state: GeneticState::Initialize,
iteration: 0,
};
let new_data = T::initialize()?;
node.data = Some(*new_data);
node.state = GeneticState::Simulate;
Ok(node)
}
pub fn from(data: T) -> Result<Self, Error> {
let mut node = GeneticNodeWrapper {
data: Some(data),
state: GeneticState::Initialize,
iteration: 0,
};
node.state = GeneticState::Simulate;
Ok(node)
}
/// Performs state transitions on the [`GeneticNode`] wrapped by the [`GeneticNodeWrapper`].
/// Will loop through the node training and scoring process for the given number of `iterations`.
///
/// ## Transitions
/// - `GeneticState::Initialize`: will attempt to call [`initialize`] on the node. When done successfully will change
/// the state to `GeneticState::Simulate`
/// - `GeneticState::Simulate`: Will call [`simulate`] with a number of iterations (not for `iterations`). Will change the state to `GeneticState::Score`
/// - `GeneticState::Mutate`: Will call [`mutate`] and will change the state to `GeneticState::Simulate.`
/// - `GeneticState::Finish`: Will finish processing the node and return.
///
/// [`initialize`]: crate::bracket::genetic_node::GeneticNode#tymethod.initialize
/// [`simulate`]: crate::bracket::genetic_node::GeneticNode#tymethod.simulate
/// [`mutate`]: crate::bracket::genetic_node::GeneticNode#tymethod.mutate
pub fn process_node(&mut self, iterations: u64) -> Result<(), Error> {
// Looping through each state transition until the number of iterations have been reached.
loop {
match (&self.state, &self.data) {
(GeneticState::Initialize, _) => {
self.iteration = 0;
let new_data = T::initialize()
.with_context(|| format!("Error initializing node {:?}", self))?;
self.data = Some(*new_data);
self.state = GeneticState::Simulate;
}
(GeneticState::Simulate, Some(_)) => {
self.data
.as_mut()
.unwrap()
.simulate(5)
.with_context(|| format!("Error simulating node: {:?}", self))?;
self.state = if self.iteration == iterations {
GeneticState::Finish
} else {
GeneticState::Mutate
};
}
(GeneticState::Mutate, Some(_)) => {
self.data
.as_mut()
.unwrap()
.mutate()
.with_context(|| format!("Error mutating node: {:?}", self))?;
self.iteration += 1;
self.state = GeneticState::Simulate;
}
(GeneticState::Finish, Some(_)) => {
break;
}
_ => panic!("Error processing node {:?}", self.data),
}
}
Ok(())
}
}

View file

@ -0,0 +1,119 @@
//! A trait used to interact with the internal state of nodes within the [`Bracket`]
//!
//! [`Bracket`]: crate::bracket::Bracket
use crate::error::Error;
use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
/// An enum used to control the state of a [`GeneticNode`]
///
/// [`GeneticNode`]: crate::bracket::genetic_node
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
#[serde(tag = "enumType", content = "enumContent")]
pub enum GeneticState {
/// The node and it's data have not finished initializing
Initialize,
/// The node is currently simulating a round against target data to determine the fitness of the population
Simulate,
/// The node is currently mutating members of it's population and breeding new members
Mutate,
/// The node has finished processing for a given number of iterations
Finish,
}
/// A trait used to interact with the internal state of nodes within the [`Bracket`]
///
/// [`Bracket`]: crate::bracket::Bracket
pub trait GeneticNode {
/// Initializes a new instance of a [`GeneticState`].
///
/// # Examples
/// TODO
fn initialize() -> Result<Box<Self>, Error>;
fn simulate(&mut self) -> Result<(), Error>;
/// Mutates members in a population and/or crossbreeds them to produce new offspring.
///
/// # Examples
/// TODO
fn mutate(&mut self) -> Result<(), Error>;
fn merge(left: &Self, right: &Self) -> Result<Box<Self>, Error>;
}
/// Used externally to wrap a node implementing the [`GeneticNode`] trait. Processes state transitions for the given node as
/// well as signal recovery. Transition states are given by [`GeneticState`]
#[derive(Debug, Serialize, Deserialize)]
pub struct GeneticNodeWrapper<T> {
pub node: Option<T>,
state: GeneticState,
generation: u64,
pub total_generations: u64,
}
impl<T> GeneticNodeWrapper<T>
where
T: GeneticNode + Debug,
{
pub fn new(total_generations: u64) -> Self {
GeneticNodeWrapper {
node: None,
state: GeneticState::Initialize,
generation: 0,
total_generations,
}
}
pub fn from(data: T, total_generations: u64) -> Self {
GeneticNodeWrapper {
node: Some(data),
state: GeneticState::Simulate,
generation: 0,
total_generations,
}
}
pub fn state(&self) -> &GeneticState {
&self.state
}
pub fn process_node(&mut self) -> Result<GeneticState, Error> {
match (&self.state, &self.node) {
(GeneticState::Initialize, _) => {
self.node = Some(*T::initialize()?);
self.state = GeneticState::Simulate;
}
(GeneticState::Simulate, Some(_)) => {
self.node
.as_mut()
.unwrap()
.simulate()
.with_context(|| format!("Error simulating node: {:?}", self))?;
self.state = if self.generation >= self.total_generations {
GeneticState::Finish
} else {
GeneticState::Mutate
};
}
(GeneticState::Mutate, Some(_)) => {
self.node
.as_mut()
.unwrap()
.mutate()
.with_context(|| format!("Error mutating node: {:?}", self))?;
self.generation += 1;
self.state = GeneticState::Simulate;
}
(GeneticState::Finish, Some(_)) => (),
_ => panic!("Error processing node {:?}", self.node),
}
Ok(self.state)
}
}

View file

@ -7,15 +7,23 @@ use crate::error::Error;
use crate::tree::Tree; use crate::tree::Tree;
use anyhow::anyhow; use anyhow::anyhow;
use file_linked::FileLinked; use file_linked::FileLinked;
use genetic_node::{GeneticNode, GeneticNodeWrapper}; use genetic_node::{GeneticNode, GeneticNodeWrapper, GeneticState};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
use std::fs::File; use std::fs::File;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::mem::swap; use std::mem::swap;
use std::path::Path; use std::path::Path;
type SimulationTree<T> = Tree<GeneticNodeWrapper<T>>;
#[derive(Serialize, Deserialize)]
pub struct GemlaConfig {
pub generations_per_node: u64,
pub overwrite: bool,
}
/// Creates a tournament style bracket for simulating and evaluating nodes of type `T` implementing [`GeneticNode`]. /// Creates a tournament style bracket for simulating and evaluating nodes of type `T` implementing [`GeneticNode`].
/// These nodes are built upwards as a balanced binary tree starting from the bottom. This results in `Bracket` building /// These nodes are built upwards as a balanced binary tree starting from the bottom. This results in `Bracket` building
/// a separate tree of the same height then merging trees together. Evaluating populations between nodes and taking the strongest /// a separate tree of the same height then merging trees together. Evaluating populations between nodes and taking the strongest
@ -26,91 +34,106 @@ pub struct Gemla<T>
where where
T: Serialize, T: Serialize,
{ {
data: FileLinked<Option<Tree<Option<GeneticNodeWrapper<T>>>>>, pub data: FileLinked<(Option<SimulationTree<T>>, GemlaConfig)>,
} }
impl<T> Gemla<T> impl<T> Gemla<T>
where where
T: GeneticNode + Serialize + DeserializeOwned + Debug, T: GeneticNode + Serialize + DeserializeOwned + Debug,
{ {
pub fn new(path: &Path, overwrite: bool) -> Result<Self, Error> { pub fn new(path: &Path, config: GemlaConfig) -> Result<Self, Error> {
match File::open(path) { match File::open(path) {
Ok(file) => { Ok(file) => {
drop(file); drop(file);
Ok(Gemla { Ok(Gemla {
data: if overwrite { data: if config.overwrite {
FileLinked::new(Some(btree!(None)), path)? FileLinked::new((None, config), path)?
} else { } else {
FileLinked::from_file(path)? FileLinked::from_file(path)?
}, },
}) })
} }
Err(error) if error.kind() == ErrorKind::NotFound => Ok(Gemla { Err(error) if error.kind() == ErrorKind::NotFound => Ok(Gemla {
data: FileLinked::new(Some(btree!(None)), path)?, data: FileLinked::new((None, config), path)?,
}), }),
Err(error) => Err(Error::IO(error)), Err(error) => Err(Error::IO(error)),
} }
} }
pub fn simulate(&mut self, steps: u64) -> Result<(), Error> { pub fn simulate(&mut self, steps: u64) -> Result<(), Error> {
self.data.mutate(|d| Gemla::increase_height(d, steps))?; self.data
.mutate(|(d, c)| Gemla::increase_height(d, c, steps))??;
self.data self.data
.mutate(|d| Gemla::process_tree(d.as_mut().unwrap()))??; .mutate(|(d, _c)| Gemla::process_tree(d.as_mut().unwrap()))??;
Ok(()) Ok(())
} }
fn build_empty_tree(size: usize) -> Tree<Option<GeneticNodeWrapper<T>>> { fn increase_height(
if size <= 1 { tree: &mut Option<SimulationTree<T>>,
btree!(None) config: &GemlaConfig,
} else { amount: u64,
btree!( ) -> Result<(), Error> {
None,
Gemla::build_empty_tree(size - 1),
Gemla::build_empty_tree(size - 1)
)
}
}
fn increase_height(tree: &mut Option<Tree<Option<GeneticNodeWrapper<T>>>>, amount: u64) {
for _ in 0..amount { for _ in 0..amount {
let height = tree.as_ref().unwrap().height(); if tree.is_none() {
let temp = tree.take(); swap(
swap( tree,
tree, &mut Some(btree!(GeneticNodeWrapper::new(config.generations_per_node))),
&mut Some(btree!( );
None, } else {
temp.unwrap(), let height = tree.as_mut().unwrap().height() as u64;
Gemla::build_empty_tree(height as usize) let temp = tree.take();
)), swap(
); tree,
&mut Some(btree!(
GeneticNodeWrapper::new(config.generations_per_node),
temp.unwrap(),
btree!(GeneticNodeWrapper::new(
height * config.generations_per_node
))
)),
);
}
} }
Ok(())
} }
fn process_tree(tree: &mut Tree<Option<GeneticNodeWrapper<T>>>) -> Result<(), Error> { fn process_tree(tree: &mut SimulationTree<T>) -> Result<(), Error> {
if tree.val.is_none() { if tree.val.state() == &GeneticState::Initialize {
match (&mut tree.left, &mut tree.right) { match (&mut tree.left, &mut tree.right) {
(Some(l), Some(r)) => { (Some(l), Some(r)) => {
Gemla::process_tree(&mut (*l))?; Gemla::process_tree(&mut (*l))?;
Gemla::process_tree(&mut (*r))?; Gemla::process_tree(&mut (*r))?;
let left_node = (*l).val.as_ref().unwrap().data.as_ref().unwrap(); let left_node = (*l).val.node.as_ref().unwrap();
let right_node = (*r).val.as_ref().unwrap().data.as_ref().unwrap(); let right_node = (*r).val.node.as_ref().unwrap();
let merged_node = GeneticNode::merge(left_node, right_node)?; let merged_node = GeneticNode::merge(left_node, right_node)?;
tree.val = Some(GeneticNodeWrapper::from(*merged_node)?); tree.val = GeneticNodeWrapper::from(*merged_node, tree.val.total_generations);
tree.val.as_mut().unwrap().process_node(1)?; Gemla::process_node(&mut tree.val)?;
} }
(None, None) => { (None, None) => {
tree.val = Some(GeneticNodeWrapper::new()?); Gemla::process_node(&mut tree.val)?;
tree.val.as_mut().unwrap().process_node(1)?;
} }
_ => { _ => {
return Err(Error::Other(anyhow!("unable to process tree {:?}", tree))); return Err(Error::Other(anyhow!("unable to process tree {:?}", tree)));
} }
} }
} else {
Gemla::process_node(&mut tree.val)?;
}
Ok(())
}
fn process_node(node: &mut GeneticNodeWrapper<T>) -> Result<(), Error> {
loop {
if node.process_node()? == GeneticState::Finish {
break;
}
} }
Ok(()) Ok(())
@ -119,7 +142,7 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::bracket::*; use crate::core::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -138,8 +161,8 @@ mod tests {
} }
impl genetic_node::GeneticNode for TestState { impl genetic_node::GeneticNode for TestState {
fn simulate(&mut self, iterations: u64) -> Result<(), Error> { fn simulate(&mut self) -> Result<(), Error> {
self.score += iterations as f64; self.score += 1.0;
Ok(()) Ok(())
} }

View file

@ -3,6 +3,6 @@ extern crate regex;
#[macro_use] #[macro_use]
pub mod tree; pub mod tree;
pub mod bracket;
pub mod constants; pub mod constants;
pub mod core;
pub mod error; pub mod error;