Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
vandomej
868472ff5c Final cleanup 2025-09-06 11:32:59 -07:00
vandomej
c6ae9b71cd Cleaning up code and adding more documentation 2025-09-06 11:20:25 -07:00
vandomej
38d9ea7bce Filling out documentation comments 2025-09-06 10:56:01 -07:00
11 changed files with 487 additions and 63 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "file_linked"
version = "0.1.41"
version = "0.1.42"
authors = ["vandomej <jacob.vandome15@gmail.com>"]
edition = "2018"
license = "MIT"

View file

@ -1,5 +1,11 @@
//! Constants used in the `file_linked` crate.
//! This module defines enums and constants that are used throughout the crate, such as data formats for serialization.
/// Data formats supported for serialization and deserialization.
#[derive(Debug)]
pub enum DataFormat {
/// Use the `bincode` format for serialization.
Bincode,
/// Use the `json` format for serialization.
Json,
}

View file

@ -1,11 +1,21 @@
//! Error types for the `file_linked` crate.
//! This module defines a unified error type for the crate, wrapping errors from dependencies and providing conversions
//! from common error types.
use thiserror::Error;
/// The main error type for the `file_linked` crate.
#[derive(Error, Debug)]
pub enum Error {
/// An error originating from the `bincode` crate.
#[error(transparent)]
Serialization(bincode::Error),
/// An IO error, such as file not found or permission denied.
#[error(transparent)]
IO(std::io::Error),
/// Any other error, wrapped using `anyhow`.
#[error(transparent)]
Other(#[from] anyhow::Error),
}

View file

@ -1,5 +1,61 @@
//! A wrapper around an object that ties it to a physical file
//! # file_linked
//!
//! **file_linked** is a Rust library for binding objects directly to files, providing persistent, file-backed state management for any serializable type. It is designed for use cases where you want to transparently synchronize in-memory data with a file, supporting both synchronous and asynchronous mutation patterns.
//!
//! ## Features
//! - Generic wrapper [`FileLinked<T>`] for any serializable type
//! - Automatic file synchronization on mutation or replacement
//! - Asynchronous and thread-safe access using `tokio::RwLock`
//! - Support for multiple serialization formats (JSON, Bincode)
//! - Simple API for reading, mutating, and replacing data
//! - Error handling with unified [`Error`] type
//!
//! ## Modules
//! - [`constants`]: Data format selection and related constants
//! - [`error`]: Error types for serialization, I/O, and general errors
//!
//! ## Example
//! ```rust,ignore
//! # use file_linked::*;
//! # use file_linked::constants::data_format::DataFormat;
//! # use serde::{Deserialize, Serialize};
//! # use std::path::PathBuf;
//! # use tokio;
//!
//! #[derive(Deserialize, Serialize)]
//! struct Test {
//! pub a: u32,
//! pub b: String,
//! pub c: f64
//! }
//!
//! #[tokio::main]
//! async fn main() {
//! let test = Test { a: 1, b: String::from("two"), c: 3.0 };
//! let mut linked = FileLinked::new(test, &PathBuf::from("./file"), DataFormat::Json).await.unwrap();
//! linked.mutate(|x| x.a += 1).await.unwrap();
//! assert_eq!(linked.readonly().read().await.a, 2);
//! }
//! ```
//!
//! ## Usage
//! 1. Create a serializable struct and instantiate a [`FileLinked`] object.
//! 2. Use `.readonly()` for shared access, `.mutate()` or `.replace()` for mutation (which automatically syncs to file).
//! 3. Use `.from_file()` to restore from disk.
//!
//! ## Crate Organization
//! - All core logic is in [`FileLinked`].
//! - Data format selection is in [`constants`].
//! - Error types and helpers are in [`error`].
//!
//! [`FileLinked`]: crate::FileLinked
//! [`Error`]: crate::error::Error
//! [`constants`]: crate::constants
//! [`error`]: crate::error
#![warn(missing_docs)]
/// Constants used in the `file_linked` crate.
pub mod constants;
pub mod error;
@ -236,12 +292,14 @@ where
/// let mut linked_test = FileLinked::new(test, &PathBuf::from("./temp"), DataFormat::Bincode).await
/// .expect("Unable to create file linked object");
///
/// // Scope to limit the lifetime of the read lock
/// {
/// let readonly = linked_test.readonly();
/// let readonly_ref = readonly.read().await;
/// assert_eq!(readonly_ref.a, 1);
/// }
///
/// // After mutate closure completes the object will be written to a file
/// linked_test.mutate(|t| t.a = 2).await?;
///
/// let readonly = linked_test.readonly();
@ -332,6 +390,52 @@ where
T: Serialize + DeserializeOwned + Send + 'static,
{
/// Asynchronously modifies the data contained in a `FileLinked` object using an async callback `op`.
/// The callback `op` is given an `Arc<RwLock<T>>` to the underlying data, allowing for concurrent access patterns.
/// After the async operation completes the data is written to a file to synchronize the state.
///
/// # Examples
/// ```rust
/// # use file_linked::*;
/// # use file_linked::error::Error;
/// # use file_linked::constants::data_format::DataFormat;
/// # use serde::{Deserialize, Serialize};
/// # use std::path::PathBuf;
/// # use tokio;
/// #
/// # #[derive(Deserialize, Serialize)]
/// # struct Test {
/// # pub a: u32,
/// # pub b: String,
/// # pub c: f64
/// # }
/// # #[tokio::main]
/// # async fn main() -> Result<(), Error> {
/// let test = Test {
/// a: 1,
/// b: String::from(""),
/// c: 0.0
/// };
/// let mut linked_test = FileLinked::new(test, &PathBuf::from("./temp"), DataFormat::Bincode).await
/// .expect("Unable to create file linked object");
///
/// // Scope to limit the lifetime of the read lock
/// {
/// let readonly = linked_test.readonly();
/// let readonly_ref = readonly.read().await;
/// assert_eq!(readonly_ref.a, 1);
/// }
///
/// // After mutate_async closure completes the object will be written to a file
/// linked_test.mutate_async(|t| async move { t.write().await.a = 2 }).await?;
///
/// let readonly = linked_test.readonly();
/// let readonly_ref = readonly.read().await;
/// assert_eq!(readonly_ref.a, 2);
/// # drop(linked_test);
/// # std::fs::remove_file("./temp").expect("Unable to remove file");
/// # Ok(())
/// # }
/// ```
pub async fn mutate_async<F, Fut, U>(&mut self, op: F) -> Result<U, Error>
where
F: FnOnce(Arc<RwLock<T>>) -> Fut,

View file

@ -1,6 +1,6 @@
[package]
name = "gemla"
version = "0.1.31"
version = "0.1.32"
authors = ["Jacob VanDomelen <Jacob.Vandome15@gmail.com>"]
edition = "2018"
license = "MIT"

View file

@ -7,13 +7,13 @@ mod test_state;
use anyhow::Result;
use clap::Parser;
use test_state::TestState;
use file_linked::constants::data_format::DataFormat;
use gemla::{
core::{Gemla, GemlaConfig},
error::log_error,
};
use std::{path::PathBuf, time::Instant};
use test_state::TestState;
// const NUM_THREADS: usize = 2;

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::core::Gemla
use crate::error::Error;
@ -11,50 +16,231 @@ 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::core::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,ignore
/// # 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>;
/// 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,ignore
/// # 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,ignore
/// # 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,ignore
/// # 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 +249,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 +289,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 +306,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 [`enum@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 [`enum@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 [`enum@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>>>,
}