Filling out documentation comments

This commit is contained in:
vandomej 2025-09-06 10:56:01 -07:00
parent ec68d46261
commit 38d9ea7bce
5 changed files with 364 additions and 61 deletions

View file

@ -1,6 +1,11 @@
//! A trait used to interact with the internal state of nodes within the [`Bracket`]
//! This module defines the [`GeneticNode`] trait and related types for managing the state and behavior of nodes within a [`Gemla`] simulation.
//!
//! [`Bracket`]: crate::bracket::Bracket
//! - [`GeneticNode`]: A trait for interacting with the internal state of nodes, supporting asynchronous initialization, simulation, mutation, and merging.
//! - [`GeneticState`]: Enum representing the current state of a node in the simulation lifecycle.
//! - [`GeneticNodeContext`]: Context struct passed to node methods, containing generation, node ID, and simulation context.
//! - [`GeneticNodeWrapper`]: Wrapper struct for managing state transitions and node lifecycle.
//!
//! [`Gemla`]: crate::Gemla
use crate::error::Error;
@ -11,50 +16,230 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt::Debug;
use uuid::Uuid;
/// An enum used to control the state of a [`GeneticNode`]
/// An enum representing the current state of a [`GeneticNode`] in the simulation lifecycle.
///
/// [`GeneticNode`]: crate::bracket::genetic_node
/// Used to control the processing flow of a node, such as initialization, simulation, mutation, and completion.
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
pub enum GeneticState {
/// The node and it's data have not finished initializing
/// The node and its data have not finished initializing.
Initialize,
/// The node is currently simulating a round against target data to determine the fitness of the population
/// 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
/// The node is currently mutating members of its population and breeding new members.
Mutate,
/// The node has finished processing for a given number of iterations
/// The node has finished processing for a given number of iterations.
Finish,
}
/// Context information passed to [`GeneticNode`] trait methods. Most of the data originates from the [`GeneticNodeWrapper`].
///
/// Contains the current generation, node ID, and simulation context.
#[derive(Clone, Debug)]
pub struct GeneticNodeContext<S> {
/// The current generation number. Generations start at 0 and increment after each mutation phase.
pub generation: u64,
/// The unique identifier for the node.
pub id: Uuid,
/// The user defined context specific to the simulation, such as parameters or environment data.
pub gemla_context: S,
}
/// A trait used to interact with the internal state of nodes within the [`Bracket`]
/// Trait for managing the state and behavior of nodes within a [`Gemla`] simulation
///
/// [`Bracket`]: crate::bracket::Bracket
/// Implementors define how nodes are initialized, simulated, mutated, and merged asynchronously.
///
/// # Associated Types
/// - `Context`: The type of context data passed to node methods.
///
/// # Example
/// ```ignore
/// // Example implementation for a custom node type
/// #[async_trait]
/// impl GeneticNode for MyNode {
/// type Context = MyContext;
/// // ...
/// }
/// ```
///
/// [`Gemla`]: crate::Gemla
#[async_trait]
pub trait GeneticNode: Send {
/// Custom type that provides a shared context across different nodes and simulations. Useful if you want to manage
/// conncurrency or share data between nodes.
///
/// # Example
/// ```ignore
/// pub struct SharedContext {
/// pub shared_semaphore: Arc<Semaphore>,
/// pub visible_simulations: Arc<Semaphore>,
/// }
/// ```
/// In this example, `SharedContext` could be used to limit the number of concurrent simulations
/// and control visibility of simulations to users.
type Context;
/// Initializes a new instance of a [`GeneticState`].
/// Initializes a new instance of a node implementing [`GeneticNode`].
///
/// # Examples
/// TODO
/// # Arguments
/// * `context` - The context for initialization, including generation and node ID.
///
/// # Returns
/// * A boxed instance of the initialized node.
///
/// # Example
/// ```rust
/// # use gemla::core::genetic_node::{GeneticNode, GeneticNodeContext};
/// # use async_trait::async_trait;
/// # use serde::{Serialize, Deserialize};
/// # use rand::prelude::*;
/// # use uuid::Uuid;
/// #[derive(Serialize, Deserialize, Debug, Clone)]
/// struct TestState {
/// population: Vec<i64>,
/// max_generations: u64,
/// }
///
/// #[async_trait]
/// impl GeneticNode for TestState {
/// type Context = ();
/// async fn initialize(_context: GeneticNodeContext<Self::Context>) -> Result<Box<Self>, gemla::error::Error> {
/// let mut population = vec![];
/// for _ in 0..5 {
/// population.push(thread_rng().gen_range(0..100));
/// }
/// Ok(Box::new(TestState { population, max_generations: 10 }))
/// }
/// // ...
/// }
/// ```
async fn initialize(context: GeneticNodeContext<Self::Context>) -> Result<Box<Self>, Error>;
async fn simulate(&mut self, context: GeneticNodeContext<Self::Context>)
-> Result<bool, Error>;
/// Simulates a round for the node, updating its state and returning whether to continue.
///
/// # Arguments
/// * `context` - The context for simulation, including generation and node ID.
///
/// # Returns
/// * `Ok(true)` if the node should continue to the next phase, `Ok(false)` if finished.
///
/// # Example
/// ```rust
/// # use gemla::core::genetic_node::{GeneticNode, GeneticNodeContext};
/// # use async_trait::async_trait;
/// # use serde::{Serialize, Deserialize};
/// # use rand::prelude::*;
/// # use uuid::Uuid;
/// #[derive(Serialize, Deserialize, Debug, Clone)]
/// struct TestState {
/// population: Vec<i64>,
/// max_generations: u64,
/// }
///
/// #[async_trait]
/// impl GeneticNode for TestState {
/// type Context = ();
/// async fn simulate(&mut self, context: GeneticNodeContext<Self::Context>) -> Result<bool, gemla::error::Error> {
/// let mut rng = thread_rng();
/// self.population = self.population.iter().map(|p| p.saturating_add(rng.gen_range(-1..2))).collect();
/// if context.generation >= self.max_generations {
/// Ok(false)
/// } else {
/// Ok(true)
/// }
/// }
/// // ...
/// }
/// ```
async fn simulate(&mut self, context: GeneticNodeContext<Self::Context>) -> Result<bool, Error>;
/// Mutates members in a population and/or crossbreeds them to produce new offspring.
///
/// # Examples
/// TODO
/// # Arguments
/// * `context` - The context for mutation, including generation and node ID.
///
/// # Returns
/// * `Ok(())` if mutation was successful.
/// * `Err(Error)` if an error occurred during mutation.
///
/// # Example
/// ```rust
/// # use gemla::core::genetic_node::{GeneticNode, GeneticNodeContext};
/// # use async_trait::async_trait;
/// # use serde::{Serialize, Deserialize};
/// # use rand::prelude::*;
/// # use uuid::Uuid;
/// #[derive(Serialize, Deserialize, Debug, Clone)]
/// struct TestState {
/// population: Vec<i64>,
/// max_generations: u64,
/// }
///
/// #[async_trait]
/// impl GeneticNode for TestState {
/// type Context = ();
/// async fn mutate(&mut self, _context: GeneticNodeContext<Self::Context>) -> Result<(), gemla::error::Error> {
/// let mut rng = thread_rng();
/// let mut v = self.population.clone();
/// v.sort_unstable();
/// v.reverse();
/// self.population = v[0..3].to_vec();
/// while self.population.len() < 5 {
/// let i = rng.gen_range(0..self.population.len());
/// let j = loop {
/// let idx = rng.gen_range(0..self.population.len());
/// if idx != i { break idx; }
/// };
/// let mut new_ind = self.population[i];
/// let cross = self.population[j];
/// new_ind = (new_ind.saturating_add(cross) / 2).saturating_add(rng.gen_range(-1..2));
/// self.population.push(new_ind);
/// }
/// Ok(())
/// }
/// // ...
/// }
/// ```
async fn mutate(&mut self, context: GeneticNodeContext<Self::Context>) -> Result<(), Error>;
/// Merges two nodes into a new node, using the provided context and ID. This occurs after
/// two nodes have finished simulating and the populations need to be combined.
///
/// # Arguments
/// * `left` - The left node to merge.
/// * `right` - The right node to merge.
/// * `id` - The ID for the new merged node.
/// * `context` - The context for merging.
///
/// # Example
/// ```rust
/// # use gemla::core::genetic_node::{GeneticNode, GeneticNodeContext};
/// # use async_trait::async_trait;
/// # use serde::{Serialize, Deserialize};
/// # use uuid::Uuid;
/// #[derive(Serialize, Deserialize, Debug, Clone)]
/// struct TestState {
/// population: Vec<i64>,
/// max_generations: u64,
/// }
///
/// #[async_trait]
/// impl GeneticNode for TestState {
/// type Context = ();
/// async fn merge(left: &TestState, right: &TestState, id: &Uuid, gemla_context: Self::Context) -> Result<Box<TestState>, gemla::error::Error> {
/// let mut v = left.population.clone();
/// v.append(&mut right.population.clone());
/// v.sort_by(|a, b| a.partial_cmp(b).unwrap());
/// v.reverse();
/// v = v[..3].to_vec();
/// let mut result = TestState { population: v, max_generations: 10 };
/// result.mutate(GeneticNodeContext { id: *id, generation: 0, gemla_context }).await?;
/// Ok(Box::new(result))
/// }
/// // ...
/// }
/// ```
async fn merge(
left: &Self,
right: &Self,
@ -63,16 +248,24 @@ pub trait GeneticNode: Send {
) -> 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`]
/// Wrapper for a node implementing [`GeneticNode`], managing state transitions and lifecycle.
///
/// Used externally to process state transitions and signal recovery. State transitions are managed using [`GeneticState`].
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct GeneticNodeWrapper<T>
where
T: Clone,
{
/// The wrapped node instance, if initialized.
node: Option<T>,
/// The current state of the node in the simulation lifecycle.
state: GeneticState,
/// The current generation number. Generations start at 0 and increment after each mutation phase.
generation: u64,
/// The unique identifier for the node.
id: Uuid,
}
@ -95,12 +288,14 @@ where
T: GeneticNode + Debug + Send + Clone,
T::Context: Send + Sync + Clone + Debug + Serialize + DeserializeOwned + 'static + Default,
{
/// Creates a new instance of [`GeneticNodeWrapper`]. Calls [`Default::default()`] of the inner type.
pub fn new() -> Self {
GeneticNodeWrapper::<T> {
..Default::default()
}
}
/// Creates a new instance from the given node data and ID.
pub fn from(data: T, id: Uuid) -> Self {
GeneticNodeWrapper {
node: Some(data),
@ -110,26 +305,38 @@ where
}
}
/// Returns a reference to the wrapped node, if available.
pub fn as_ref(&self) -> Option<&T> {
self.node.as_ref()
}
/// Takes the wrapped node, consuming the wrapper.
pub fn take(&mut self) -> Option<T> {
self.node.take()
}
/// Returns the ID of the node.
pub fn id(&self) -> Uuid {
self.id
}
/// Returns the current generation number.
pub fn generation(&self) -> u64 {
self.generation
}
/// Returns the current state of the node.
pub fn state(&self) -> GeneticState {
self.state
}
/// Processes the node for the current generation, updating its state and transitioning to the next phase as needed.
///
/// # Arguments
/// * `gemla_context` - The user-defined context for the simulation, passed to node methods.
///
/// # Returns
/// * The updated state of the node after processing.
pub async fn process_node(&mut self, gemla_context: T::Context) -> Result<GeneticState, Error> {
let context = GeneticNodeContext {
generation: self.generation,

View file

@ -1,5 +1,13 @@
//! Simulates a genetic algorithm on a population in order to improve the fit score and performance. The simulations
//! are performed in a tournament bracket configuration so that populations can compete against each other.
//! This module provides the core logic for simulating genetic algorithms using a tournament bracket structure.
//!
//! - Defines the [`Gemla`] struct, which manages the simulation of populations as a tree of nodes.
//! - Nodes implement the [`GeneticNode`] trait and are managed using [`GeneticNodeWrapper`].
//! - Simulations are performed in a bracket configuration, allowing populations to compete and evolve.
//! - Includes configuration, tree management, and asynchronous simulation logic.
//!
//! [`Gemla`]: crate::core::Gemla
//! [`GeneticNode`]: crate::core::genetic_node::GeneticNode
//! [`GeneticNodeWrapper`]: crate::core::genetic_node::GeneticNodeWrapper
pub mod genetic_node;
@ -19,58 +27,41 @@ use uuid::Uuid;
type SimulationTree<T> = Box<Tree<GeneticNodeWrapper<T>>>;
/// Provides configuration options for managing a [`Gemla`] object as it executes.
/// Configuration options for managing a [`Gemla`] simulation.
///
/// # Examples
/// ```rust,ignore
/// #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
/// struct TestState {
/// pub score: f64,
/// }
///
/// impl genetic_node::GeneticNode for TestState {
/// fn simulate(&mut self) -> Result<(), Error> {
/// self.score += 1.0;
/// Ok(())
/// }
///
/// fn mutate(&mut self) -> Result<(), Error> {
/// Ok(())
/// }
///
/// fn initialize() -> Result<Box<TestState>, Error> {
/// Ok(Box::new(TestState { score: 0.0 }))
/// }
///
/// fn merge(left: &TestState, right: &TestState) -> Result<Box<TestState>, Error> {
/// Ok(Box::new(if left.score > right.score {
/// left.clone()
/// } else {
/// right.clone()
/// }))
/// }
/// }
///
/// fn main() {
///
/// }
/// ```
/// - `overwrite`: If true, existing simulation data will be overwritten when initializing a new simulation.
#[derive(Serialize, Deserialize, Copy, Clone)]
pub struct GemlaConfig {
/// If true, existing simulation data will be overwritten when initializing a new simulation.
pub overwrite: bool,
}
/// 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
/// a separate tree of the same height then merging trees together. Evaluating populations between nodes and taking the strongest
/// individuals.
/// Manages a tournament-style bracket for simulating and evaluating nodes of type `T` implementing [`GeneticNode`].
///
/// [`GeneticNode`]: genetic_node::GeneticNode
/// Nodes are organized as an unbalanced binary tree, and the simulation proceeds by processing nodes, merging results,
/// and evolving populations. The simulation is asynchronous and supports concurrent node processing.
///
/// # Type Parameters
/// - `T`: The node type, which must implement [`GeneticNode`], serialization, Debug, Send, and Clone traits.
///
/// # Fields
/// - `data`: Stores the simulation tree, configuration, and context.
/// - `threads`: Tracks asynchronous tasks for node processing.
///
/// # Example
/// ```ignore
/// let config = GemlaConfig { overwrite: true };
/// let mut gemla = Gemla::<MyNodeType>::new(path, config, DataFormat::Json).await?;
/// gemla.simulate(5).await?;
/// ```
///
/// [`GeneticNode`]: crate::core::genetic_node::GeneticNode
pub struct Gemla<T>
where
T: GeneticNode + Serialize + DeserializeOwned + Debug + Send + Clone,
T::Context: Send + Sync + Clone + Debug + Serialize + DeserializeOwned + 'static + Default,
{
/// The simulation data, including the tree, configuration, and context.
pub data: FileLinked<(Option<SimulationTree<T>>, GemlaConfig, T::Context)>,
threads: HashMap<Uuid, JoinHandle<Result<GeneticNodeWrapper<T>, Error>>>,
}
@ -80,6 +71,16 @@ where
T: GeneticNode + Serialize + DeserializeOwned + Debug + Send + Sync + Clone,
T::Context: Send + Sync + Clone + Debug + Serialize + DeserializeOwned + 'static + Default,
{
/// Creates a new [`Gemla`] instance, initializing or loading the simulation data from the specified path.
///
/// # Arguments
/// - `path`: The file system path to load or create the simulation data.
/// - `config`: Configuration options for the simulation.
/// - `data_format`: The format of the data (e.g., JSON, binary).
///
/// # Returns
/// - `Ok(Self)`: A new [`Gemla`] instance.
/// - `Err(Error)`: An error occurred during initialization or loading.
pub async fn new(
path: &Path,
config: GemlaConfig,
@ -107,10 +108,19 @@ where
}
}
/// Returns a read-only reference to the simulation tree, configuration, and context.
pub fn tree_ref(&self) -> Arc<RwLock<(Option<SimulationTree<T>>, GemlaConfig, T::Context)>> {
self.data.readonly().clone()
}
/// Simulates the genetic algorithm for the specified number of steps, processing and evolving the population.
///
/// # Arguments
/// - `steps`: The number of simulation steps to perform. A simulation step increases the height of the tree by one and continues processing until all nodes are completed.
///
/// # Returns
/// - `Ok(())`: Simulation completed successfully.
/// - `Err(Error)`: An error occurred during simulation.
pub async fn simulate(&mut self, steps: u64) -> Result<(), Error> {
let tree_completed = {
// Only increase height if the tree is uninitialized or completed

View file

@ -1,16 +1,30 @@
//! Error handling utilities and error type for the Gemla crate.
//!
//! This module defines a unified [`Error`] enum for representing errors that can occur throughout the crate,
//! including I/O errors, errors from the `file_linked` crate, and general errors using `anyhow`.
//!
//! It also provides conversion implementations and a helper function for logging errors.
use log::error;
use thiserror::Error;
/// The main error type for the Gemla crate.
///
/// This enum wraps errors from dependencies and provides a unified error type for use throughout the crate.
#[derive(Error, Debug)]
pub enum Error {
/// An error originating from the `file_linked` crate.
#[error(transparent)]
FileLinked(file_linked::error::Error),
/// An I/O error.
#[error(transparent)]
IO(std::io::Error),
/// Any other error, wrapped using `anyhow`.
#[error(transparent)]
Other(#[from] anyhow::Error),
}
/// Converts a `file_linked::error::Error` into a unified [`Error`].
impl From<file_linked::error::Error> for Error {
fn from(error: file_linked::error::Error) -> Error {
match error {
@ -20,12 +34,22 @@ impl From<file_linked::error::Error> for Error {
}
}
/// Converts a standard I/O error into a unified [`Error`].
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Error {
Error::IO(error)
}
}
/// Logs an error using the `log` crate and returns the error.
///
/// This helper is useful for logging errors at the point of handling while still propagating them.
///
/// # Example
/// ```rust
/// use gemla::error::{log_error, Error};
/// let result: Result<(), Error> = log_error(Err(Error::Other(anyhow::anyhow!("Some error"))));
/// ```
pub fn log_error<T>(result: Result<T, Error>) -> Result<T, Error> {
result.map_err(|e| {
error!("{}", e);

View file

@ -1,3 +1,60 @@
//! # Gemla
//!
//! **Gemla** is a Rust library for simulating and evolving populations using genetic algorithms. It provides a flexible framework for evolutionary computation, supporting custom node types, tournament bracket simulations, and persistent state management.
//!
//! ## Features
//! - Tournament-style genetic algorithm simulation evaluating populations via nodes
//! - Asynchronous and concurrent simulation using Tokio
//! - Customizable node logic via the [`GeneticNode`] trait
//! - Persistent, file-backed simulation state with the `file_linked` crate
//! - Utilities for binary tree management
//!
//! ## Modules
//! - [`tree`]: Defines an unbalanced binary tree structure and macros for tree construction.
//! - [`core`]: Contains the main simulation logic, including the [`Gemla`] struct, configuration, and node management.
//! - [`error`]: Provides unified error types and logging utilities for the crate.
//!
//! ## Example
//! ```rust,ignore
//! # use gemla::core::{Gemla, GemlaConfig};
//! # use gemla::core::genetic_node::{GeneticNode, GeneticNodeContext};
//! # use file_linked::constants::data_format::DataFormat;
//! # use std::path::PathBuf;
//!
//! #[derive(Clone, Debug)]
//! struct MyNode { /* ... */ }
//! // Implement GeneticNode for MyNode...
//!
//! #[tokio::main]
//! async fn main() {
//! let mut gemla = Gemla::<MyNode>::new(
//! &PathBuf::from("state.json"),
//! GemlaConfig { overwrite: true },
//! DataFormat::Json,
//! ).await.unwrap();
//! // Grows simulation tree by 5 levels
//! gemla.simulate(5).await.unwrap();
//! }
//! ```
//!
//! ## Crate Organization
//! - All core simulation logic is in [`core`].
//! - Tree structures and macros are in [`tree`].
//! - Error types and helpers are in [`error`].
//!
//! ## Getting Started
//! 1. Define your node type and implement the [`GeneticNode`] trait.
//! 2. Create a [`Gemla`] instance and run simulations.
//! 3. Use the provided error handling and tree utilities as needed.
//!
//! [`Gemla`]: crate::core::Gemla
//! [`GeneticNode`]: crate::core::genetic_node::GeneticNode
//! [`tree`]: crate::tree
//! [`core`]: crate::core
//! [`error`]: crate::error
#![warn(missing_docs)]
#[macro_use]
pub mod tree;
pub mod core;

View file

@ -38,8 +38,13 @@ use std::cmp::max;
/// ```
#[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
pub struct Tree<T> {
/// The value stored in this node
pub val: T,
/// The left child node, if any
pub left: Option<Box<Tree<T>>>,
/// The right child node, if any
pub right: Option<Box<Tree<T>>>,
}