diff --git a/Cargo.lock b/Cargo.lock index 357fedb..6895997 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,7 @@ dependencies = [ "nom", "num-rational", "quickcheck", + "rand", "rayon", "serde 1.0.192", "serde_repr", diff --git a/Cargo.toml b/Cargo.toml index 522d229..d9f2514 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ serde_repr = { version = "0.1.12", optional = true} cgt-derive = { path = "cgt-derive"} rayon = {version = "1.7.0", optional = true} dashmap = { version = "5.5.3", features = ["inline"] } +rand = "0.8.5" [dev-dependencies] quickcheck = { version = "1.0", default-features = false } diff --git a/cgt-cli/src/io.rs b/cgt-cli/src/io.rs new file mode 100644 index 0000000..98c6a47 --- /dev/null +++ b/cgt-cli/src/io.rs @@ -0,0 +1,69 @@ +use std::{ + fs::File, + io::{self, stderr, Stderr}, +}; + +#[derive(Debug, Clone)] +pub enum FileOrStderr { + FilePath(String), + Stderr, +} + +impl From for FileOrStderr { + fn from(value: String) -> Self { + if &value == "-" { + Self::Stderr + } else { + Self::FilePath(value) + } + } +} + +impl FileOrStderr { + fn create_with<'a, F>(&'a self, f: F) -> io::Result + where + F: FnOnce(&'a str) -> io::Result, + { + match self { + FileOrStderr::FilePath(ref fp) => Ok(FileOrStderrWriter::File(f(fp)?)), + FileOrStderr::Stderr => Ok(FileOrStderrWriter::Stderr(stderr())), + } + } + + #[allow(dead_code)] + pub fn open(&self) -> io::Result { + self.create_with(File::open) + } + + #[allow(dead_code)] + pub fn create(&self) -> io::Result { + self.create_with(File::create) + } +} + +pub enum FileOrStderrWriter { + File(File), + Stderr(Stderr), +} + +impl io::Write for FileOrStderrWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + FileOrStderrWriter::File(f) => f.write(buf), + FileOrStderrWriter::Stderr(stderr) => { + let mut lock = stderr.lock(); + lock.write(buf) + } + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + FileOrStderrWriter::File(f) => f.flush(), + FileOrStderrWriter::Stderr(stderr) => { + let mut lock = stderr.lock(); + lock.flush() + } + } + } +} diff --git a/cgt-cli/src/main.rs b/cgt-cli/src/main.rs index 576729c..76273ed 100644 --- a/cgt-cli/src/main.rs +++ b/cgt-cli/src/main.rs @@ -5,6 +5,7 @@ mod amazons; mod anyhow_utils; mod canonical_form; mod domineering; +mod io; mod quicksort; mod snort; mod wind_up; diff --git a/cgt-cli/src/snort/common.rs b/cgt-cli/src/snort/common.rs index 640998b..e46c54a 100644 --- a/cgt-cli/src/snort/common.rs +++ b/cgt-cli/src/snort/common.rs @@ -1,5 +1,6 @@ use anyhow::{bail, Context, Result}; use cgt::{ + genetic_algorithm::Scored, graph::undirected::Graph, numeric::{dyadic_rational_number::DyadicRationalNumber, rational::Rational}, short::partizan::{ @@ -22,28 +23,13 @@ pub enum Log { temperature: DyadicRationalNumber, }, HighFitness { - position: Scored, + position: Scored, canonical_form: String, temperature: DyadicRationalNumber, degree: usize, }, } -#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] -pub struct Scored { - pub position: Snort, - pub score: Rational, -} - -impl Scored { - pub fn without_score(position: Snort) -> Self { - Scored { - position, - score: Rational::NegativeInfinity, - } - } -} - #[derive(Debug, Clone)] pub struct Edge { pub from: u32, @@ -129,7 +115,10 @@ pub fn analyze_position(position: Snort) -> Result<()> { let score = temperature.to_rational() - Rational::from(degree as i32); let log = Log::HighFitness { - position: Scored { position, score }, + position: Scored { + object: position, + score, + }, canonical_form: canonical_form.to_string(), temperature, degree, diff --git a/cgt-cli/src/snort/genetic.rs b/cgt-cli/src/snort/genetic.rs index 66113d1..29be576 100644 --- a/cgt-cli/src/snort/genetic.rs +++ b/cgt-cli/src/snort/genetic.rs @@ -1,6 +1,7 @@ -use crate::snort::common::{Log, Scored}; -use anyhow::Result; +use crate::{io::FileOrStderr, snort::common::Log}; +use anyhow::{Context, Result}; use cgt::{ + genetic_algorithm::{Algorithm, GeneticAlgorithm, Scored}, graph::undirected, numeric::rational::Rational, short::partizan::{ @@ -13,171 +14,178 @@ use clap::{self, Parser}; use rand::{seq::SliceRandom, Rng}; use std::{ cmp::min, - collections::HashSet, fs::File, - io::{stderr, BufReader, BufWriter, Write}, - path::Path, + io::{BufReader, BufWriter, Write}, + num::NonZeroUsize, }; #[derive(Parser, Debug, Clone)] pub struct Args { #[arg(long)] - generation_size: usize, + generation_size: NonZeroUsize, + /// Do not generate graphs with more that that vertices #[arg(long)] max_graph_vertices: usize, #[arg(long)] mutation_rate: f32, + /// Stop after running that many generations. Run forever otherwise #[arg(long, default_value = None)] generation_limit: Option, - /// Path to saved snapshot + /// Path to saved snapshot to be loaded #[arg(long, default_value = None)] snapshot_load_file: Option, + /// Path to save snapshot file #[arg(long)] snapshot_save_file: String, + /// Path to output logs #[arg(long)] - out_file: String, + out_file: FileOrStderr, + /// Clean up transpositon table after that many generations #[arg(long, default_value_t = 50)] cleanup_interval: usize, + /// Save if score is above that value #[arg(long, default_value_t = Rational::from(0))] save_eq_or_above: Rational, } -#[derive(serde::Serialize, serde::Deserialize)] -struct Snapshot { - specimen: Vec, -} - -struct Alg { - args: Args, - specimen: Vec, - all_time_best: HashSet, - log_writer: BufWriter>, -} - -fn random_position(max_graph_vertices: usize) -> Snort { - let mut rng = rand::thread_rng(); - let graph_size = rng.gen_range(1..=max_graph_vertices); - let graph = undirected::Graph::empty(graph_size); - let mut position = Snort::new(graph); - mutate(&mut position, 1.0); - position +struct SnortTemperatureDegreeDifference { + transposition_table: ParallelTranspositionTable, + max_graph_vertices: usize, + mutation_rate: f32, } -fn mutate(position: &mut Snort, mutation_rate: f32) { - let mut rng = rand::thread_rng(); - - // Mutate vertices - if position.graph.size() > 1 { +impl SnortTemperatureDegreeDifference { + fn mutate_with_rate( + &self, + position: &mut Snort, + rng: &mut rand::rngs::ThreadRng, + mutation_rate: f32, + ) { + // Mutate vertices + if position.graph.size() > 1 { + let mutation_roll: f32 = rng.gen(); + if mutation_roll < mutation_rate { + let to_remove = rng.gen_range(0..position.graph.size()); + position.graph.remove_vertex(to_remove); + position.vertices.remove(to_remove); + } + } + // TODO: Check for max size + // if position.graph.size() let mutation_roll: f32 = rng.gen(); if mutation_roll < mutation_rate { - let to_remove = rng.gen_range(0..position.graph.size()); - position.graph.remove_vertex(to_remove); - position.vertices.remove(to_remove); + position.graph.add_vertex(); + position + .vertices + .push(VertexKind::Single(VertexColor::Empty)); + let another_vertex = rng.gen_range(0..position.graph.size() - 1); + position + .graph + .connect(position.graph.size() - 1, another_vertex, true); } - } - // TODO: Check for max size - // if position.graph.size() - let mutation_roll: f32 = rng.gen(); - if mutation_roll < mutation_rate { - position.graph.add_vertex(); - position - .vertices - .push(VertexKind::Single(VertexColor::Empty)); - let another_vertex = rng.gen_range(0..position.graph.size() - 1); - position - .graph - .connect(position.graph.size() - 1, another_vertex, true); - } - // Mutate edges - for v in position.graph.vertices() { - for u in position.graph.vertices() { - if v == u { - continue; + // Mutate edges + for v in position.graph.vertices() { + for u in position.graph.vertices() { + if v == u { + continue; + } + + let mutation_roll: f32 = rng.gen(); + if mutation_roll < mutation_rate { + position + .graph + .connect(v, u, !position.graph.are_adjacent(v, u)); + } } + } + // Mutate colors + let available_colors = vec![ + VertexColor::Empty, + VertexColor::TintLeft, + VertexColor::TintRight, + ]; + for idx in 0..position.vertices.len() { let mutation_roll: f32 = rng.gen(); if mutation_roll < mutation_rate { - position - .graph - .connect(v, u, !position.graph.are_adjacent(v, u)); + position.vertices[idx] = VertexKind::Single(*available_colors.choose(rng).unwrap()); } } } - - // Mutate colors - let available_colors = vec![ - VertexColor::Empty, - VertexColor::TintLeft, - VertexColor::TintRight, - ]; - for idx in 0..position.vertices.len() { - let mutation_roll: f32 = rng.gen(); - if mutation_roll < mutation_rate { - position.vertices[idx] = - VertexKind::Single(*available_colors.choose(&mut rng).unwrap()); - } - } } -fn score<'pos>( - position: &'pos Snort, - transposition_table: &ParallelTranspositionTable, -) -> Rational { - let degree_sum = position.graph.degrees().iter().sum::(); - if position.vertices.is_empty() || degree_sum == 0 || !position.graph.is_connected() { - return Rational::NegativeInfinity; +impl Algorithm for SnortTemperatureDegreeDifference { + fn mutate(&self, position: &mut Snort, rng: &mut rand::rngs::ThreadRng) { + self.mutate_with_rate(position, rng, self.mutation_rate); } - temp_dif(position, transposition_table) -} + fn cross(&self, lhs: &Snort, rhs: &Snort, _rng: &mut rand::rngs::ThreadRng) -> Snort { + let mut rng = rand::thread_rng(); -fn temp_dif<'pos>( - position: &'pos Snort, - transposition_table: &ParallelTranspositionTable, -) -> Rational { - let game = position.canonical_form(transposition_table); - let temp = game.temperature(); - let degree = position.degree(); - temp.to_rational() - Rational::from(degree as i64) -} + let mut positions = [lhs, rhs]; + positions.sort_by_key(|pos| pos.graph.size()); + let [smaller, larger] = positions; -fn cross(lhs: &Snort, rhs: &Snort) -> Snort { - let mut rng = rand::thread_rng(); + let new_size = rng.gen_range(1..=larger.graph.size()); + let mut new_graph = undirected::Graph::empty(new_size); - let mut positions = [lhs, rhs]; - positions.sort_by_key(|pos| pos.graph.size()); - let [smaller, larger] = positions; + for v in 0..(min(new_size, smaller.graph.size())) { + for u in 0..(min(new_size, smaller.graph.size())) { + new_graph.connect(v, u, smaller.graph.are_adjacent(v, u)); + } + } + for v in (min(new_size, smaller.graph.size()))..(min(new_size, larger.graph.size())) { + for u in (min(new_size, smaller.graph.size()))..(min(new_size, larger.graph.size())) { + new_graph.connect(v, u, larger.graph.are_adjacent(v, u)); + } + } - let new_size = rng.gen_range(1..=larger.graph.size()); - let mut new_graph = undirected::Graph::empty(new_size); + let mut colors = smaller.vertices[0..(min(new_size, smaller.graph.size()))].to_vec(); + colors.extend( + &larger.vertices + [(min(new_size, smaller.graph.size()))..(min(new_size, larger.graph.size()))], + ); - for v in 0..(min(new_size, smaller.graph.size())) { - for u in 0..(min(new_size, smaller.graph.size())) { - new_graph.connect(v, u, smaller.graph.are_adjacent(v, u)); - } + Snort::with_colors(colors, new_graph).unwrap() + } + + fn lowest_score(&self) -> Rational { + Rational::NegativeInfinity } - for v in (min(new_size, smaller.graph.size()))..(min(new_size, larger.graph.size())) { - for u in (min(new_size, smaller.graph.size()))..(min(new_size, larger.graph.size())) { - new_graph.connect(v, u, larger.graph.are_adjacent(v, u)); + + fn score(&self, position: &Snort) -> Rational { + let degree_sum = position.graph.degrees().iter().sum::(); + if position.vertices.is_empty() || degree_sum == 0 || !position.graph.is_connected() { + return Rational::NegativeInfinity; } + + let game = position.canonical_form(&self.transposition_table); + let temp = game.temperature(); + let degree = position.degree(); + temp.to_rational() - Rational::from(degree as i64) } - let mut colors = smaller.vertices[0..(min(new_size, smaller.graph.size()))].to_vec(); - colors.extend( - &larger.vertices - [(min(new_size, smaller.graph.size()))..(min(new_size, larger.graph.size()))], - ); + fn random(&self, rng: &mut rand::rngs::ThreadRng) -> Snort { + let graph_size = rng.gen_range(1..=self.max_graph_vertices); + let graph = undirected::Graph::empty(graph_size); + let mut position = Snort::new(graph); + self.mutate_with_rate(&mut position, rng, 1.0); + position + } +} - Snort::with_colors(colors, new_graph).unwrap() +#[derive(serde::Serialize, serde::Deserialize)] +struct Snapshot { + specimen: Vec>, } fn seed_positions() -> Vec { @@ -186,7 +194,7 @@ fn seed_positions() -> Vec { // 1--3--4--10-12 // / /|\ \ // 2 7 8 9 13 - let g1 = undirected::Graph::from_edges( + let pos_1 = Snort::new(undirected::Graph::from_edges( 14, &[ (0, 3), @@ -203,8 +211,7 @@ fn seed_positions() -> Vec { (10, 12), (10, 13), ], - ); - let pos1 = Snort::new(g1); + )); // 9 // | @@ -215,7 +222,7 @@ fn seed_positions() -> Vec { // 2 6 8 -13 // | // 14 - let g2 = undirected::Graph::from_edges( + let pos_2 = Snort::new(undirected::Graph::from_edges( 15, &[ (0, 3), @@ -233,169 +240,83 @@ fn seed_positions() -> Vec { (8, 13), (8, 14), ], - ); - let pos2 = Snort::new(g2); - vec![pos1, pos2] -} - -impl Alg { - fn new_random(args: Args) -> Self { - let mut specimen = Vec::with_capacity(args.generation_size); - - // TODO: Add --no-seed flag to omit this - specimen.extend(seed_positions().into_iter().map(Scored::without_score)); - - for _ in specimen.len()..args.generation_size { - let scored = Scored { - position: random_position(args.max_graph_vertices), - score: Rational::NegativeInfinity, - }; - specimen.push(scored); - } - Alg::with_specimen(args, specimen) - } - - fn from_snapshot(args: Args, snapshot: Snapshot) -> Self { - let specimen = snapshot - .specimen - .into_iter() - .map(|position| Scored { - position, - score: Rational::NegativeInfinity, - }) - .collect(); - Alg::with_specimen(args, specimen) - } - - fn with_specimen(args: Args, specimen: Vec) -> Self { - let file = if Path::new(&args.out_file).exists() { - File::open(&args.out_file) - } else { - File::create(&args.out_file) - } - .unwrap(); - let log_writer: BufWriter> = BufWriter::new(Box::new(file)); - - Alg { - args, - specimen, - all_time_best: HashSet::new(), - log_writer, - } - } - - // TODO: parallel with rayon - fn score(&mut self, tt: &ParallelTranspositionTable) { - let specimen = &mut self.specimen; - for spec in specimen { - spec.score = score(&spec.position, tt); - if spec.score >= self.args.save_eq_or_above { - if self.all_time_best.insert(spec.clone()) { - let canonical_form = spec.position.canonical_form(tt); - let log = Log::HighFitness { - position: spec.clone(), - canonical_form: canonical_form.to_string(), - temperature: canonical_form.temperature(), - degree: spec.position.degree(), - }; - Alg::emit_log(&mut self.log_writer, &log); - } - } - } - self.specimen.sort_by_key(|spec| spec.score.clone()); - } - - fn highest_score(&self) -> Rational { - self.specimen - .last() - .expect("to have at least one score") - .score - .clone() - } - - fn snapshot(&self) -> Snapshot { - Snapshot { - specimen: self - .specimen - .iter() - .map(|spec| &spec.position) - .cloned() - .collect(), - } - } - - fn save_progress(&self, mut output: impl Write) { - writeln!( - output, - "{}", - serde_json::ser::to_string(&self.snapshot()).unwrap() - ) - .unwrap(); - } + )); - fn cross(&mut self) { - let mut rng = rand::thread_rng(); - let mid_point = self.args.generation_size / 2; - let mut new_specimen = Vec::with_capacity(self.args.generation_size); - let top_half = &self.specimen[mid_point..]; - new_specimen.extend_from_slice(top_half); - for _ in new_specimen.len()..self.args.generation_size { - let lhs = self.specimen.choose(&mut rng).unwrap(); - let rhs = self.specimen.choose(&mut rng).unwrap(); - let mut position = cross(&lhs.position, &rhs.position); - mutate(&mut position, self.args.mutation_rate); - new_specimen.push(Scored { - position, - score: Rational::NegativeInfinity, - }); - } - self.specimen = new_specimen; - } - - fn emit_log(writer: &mut BufWriter>, log: &Log) { - writeln!(writer, "{}", serde_json::ser::to_string(log).unwrap()).unwrap(); - writer.flush().unwrap(); - } + vec![pos_1, pos_2] } pub fn run(args: Args) -> Result<()> { let generation_limit = args.generation_limit; let output_file_path = args.snapshot_save_file.clone(); - let transposition_table = ParallelTranspositionTable::new(); - let mut alg = if let Some(snapshot_file) = args.snapshot_load_file.clone() { - let f = BufReader::new(File::open(snapshot_file).unwrap()); - let snapshot: Snapshot = serde_json::de::from_reader(f).unwrap(); - Alg::from_snapshot(args, snapshot) + let alg = SnortTemperatureDegreeDifference { + transposition_table: ParallelTranspositionTable::new(), + max_graph_vertices: args.max_graph_vertices, + mutation_rate: args.mutation_rate, + }; + + let specimen = if let Some(snapshot_file) = args.snapshot_load_file.clone() { + let f = BufReader::new(File::open(snapshot_file).context("Could not open snapshot file")?); + let snapshot: Snapshot = + serde_json::de::from_reader(f).context("Could not parse snapshot file")?; + snapshot.specimen.into_iter().map(|s| s.object).collect() } else { - Alg::new_random(args) + seed_positions() }; - let mut generation = 0; + let mut alg = GeneticAlgorithm::with_specimen(specimen, args.generation_size, alg); + + let mut log_writer = args.out_file.create().unwrap(); loop { - if generation_limit.map_or(false, |limit| generation >= limit) { + if generation_limit.map_or(false, |limit| alg.generation() >= limit) { break; } - alg.score(&transposition_table); - - let mut output = BufWriter::new(File::create(&output_file_path).unwrap()); - alg.save_progress(&mut output); + alg.step_generation(); + + // TODO: Save interval + { + let mut output = BufWriter::new( + File::create(&output_file_path).context("Could not create/open output file")?, + ); + writeln!( + output, + "{}", + serde_json::ser::to_string(&Snapshot { + specimen: alg.specimen().to_vec() + }) + .unwrap() + ) + .unwrap(); + } - let top = &alg.specimen.last().unwrap().position; - let top_score = alg.highest_score(); - Alg::emit_log( - &mut BufWriter::new(Box::new(stderr())), - &Log::Generation { - generation, - top_score, - temperature: top.canonical_form(&transposition_table).temperature(), - }, - ); - alg.cross(); + let best = alg.highest_score(); + let best_cf = best + .object + .canonical_form(&alg.algorithm().transposition_table); + let best_temp = best_cf.temperature(); + + { + let log = Log::Generation { + generation: alg.generation(), + top_score: best.score, + temperature: best_temp, + }; + writeln!(log_writer, "{}", serde_json::ser::to_string(&log).unwrap()).unwrap(); + log_writer.flush().unwrap(); + } - generation += 1; + { + let log = Log::HighFitness { + position: best.clone(), + canonical_form: best_cf.to_string(), + temperature: best_temp, + degree: best.object.degree(), + }; + writeln!(log_writer, "{}", serde_json::ser::to_string(&log).unwrap()).unwrap(); + log_writer.flush().unwrap(); + } } Ok(()) diff --git a/cgt-cli/src/snort/latex.rs b/cgt-cli/src/snort/latex.rs index 924fc93..e5bd03f 100644 --- a/cgt-cli/src/snort/latex.rs +++ b/cgt-cli/src/snort/latex.rs @@ -99,7 +99,7 @@ pub fn run(args: Args) -> Result<()> { .stdin .take() .context("Could not open graphviz stdin")? - .write_all(position.position.to_graphviz().as_bytes()) + .write_all(position.object.to_graphviz().as_bytes()) .context("Could not write to graphviz stdin")?; // Await result and check for errors diff --git a/src/genetic_algorithm.rs b/src/genetic_algorithm.rs new file mode 100644 index 0000000..4032999 --- /dev/null +++ b/src/genetic_algorithm.rs @@ -0,0 +1,118 @@ +//! Utilities for genetic search + +#![allow(missing_docs)] + +use rand::{rngs::ThreadRng, seq::SliceRandom}; +use std::num::NonZeroUsize; + +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Scored { + pub object: Object, + pub score: Score, +} + +pub trait Algorithm { + fn mutate(&self, object: &mut Object, rng: &mut ThreadRng); + + fn cross(&self, lhs: &Object, rhs: &Object, rng: &mut ThreadRng) -> Object; + + fn lowest_score(&self) -> Score; + + fn score(&self, object: &Object) -> Score; + + fn random(&self, rng: &mut ThreadRng) -> Object; +} + +pub struct GeneticAlgorithm { + specimen: Vec>, + generation: usize, + algorithm: Alg, +} + +impl GeneticAlgorithm +where + Alg: Algorithm, + Score: Clone + Ord, + Object: Clone, +{ + pub fn new(size: NonZeroUsize, algorithm: Alg) -> Self { + let mut rng = rand::thread_rng(); + let specimen = (0..size.get()) + .map(|_| algorithm.random(&mut rng)) + .collect::>(); + + Self::with_specimen(specimen, size, algorithm) + } + + pub fn with_specimen(mut specimen: Vec, size: NonZeroUsize, algorithm: Alg) -> Self { + let mut rng = rand::thread_rng(); + let to_generate = specimen.len().checked_sub(size.get()).unwrap_or(0); + specimen.extend((0..to_generate).map(|_| algorithm.random(&mut rng))); + let specimen = specimen + .into_iter() + .map(|object| Scored { + object, + score: algorithm.lowest_score(), + }) + .collect::>(); + + Self { + specimen, + generation: 0, + algorithm, + } + } + + pub fn highest_score(&self) -> &Scored { + self.specimen.last().unwrap() + } + + fn score(&mut self) { + self.specimen + .iter_mut() + .for_each(|spec| spec.score = self.algorithm.score(&spec.object)); + + self.specimen + .sort_unstable_by(|lhs, rhs| Ord::cmp(&lhs.score, &rhs.score)) + } + + fn cross(&mut self) { + let mut rng = rand::thread_rng(); + let generation_size = self.specimen.len(); + let mid_point = generation_size / 2; + let mut new_specimen = Vec::with_capacity(generation_size); + let top_half = &self.specimen[mid_point..]; + new_specimen.extend_from_slice(top_half); + for _ in new_specimen.len()..generation_size { + let lhs = self.specimen.choose(&mut rng).unwrap(); + let rhs = self.specimen.choose(&mut rng).unwrap(); + let mut object = self.algorithm.cross(&lhs.object, &rhs.object, &mut rng); + self.algorithm.mutate(&mut object, &mut rng); + new_specimen.push(Scored { + object, + score: self.algorithm.lowest_score(), + }); + } + self.specimen = new_specimen; + } + + pub fn step_generation(&mut self) { + self.cross(); + self.score(); + self.generation += 1; + } + + /// Get number of finished (scored) generations + pub fn generation(&self) -> usize { + self.generation + } + + pub fn algorithm(&self) -> &Alg { + &self.algorithm + } + + pub fn specimen(&self) -> &[Scored] { + &self.specimen + } +} diff --git a/src/lib.rs b/src/lib.rs index 1b5d1ba..c4fa55a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ )] pub mod drawing; +pub mod genetic_algorithm; pub mod graph; pub mod grid; pub mod loopy;