GEMLA/gemla/src/core/genetic_node.rs
2025-09-06 10:56:01 -07:00

535 lines
17 KiB
Rust

//! This module defines the [`GeneticNode`] trait and related types for managing the state and behavior of nodes within a [`Gemla`] simulation.
//!
//! - [`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;
use anyhow::Context;
use async_trait::async_trait;
use log::info;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt::Debug;
use uuid::Uuid;
/// An enum representing the current state of a [`GeneticNode`] in the simulation lifecycle.
///
/// 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 its 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 its population and breeding new members.
Mutate,
/// 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,
}
/// Trait for managing the state and behavior of nodes within a [`Gemla`] simulation
///
/// 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 node implementing [`GeneticNode`].
///
/// # 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>;
/// 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.
///
/// # 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,
id: &Uuid,
context: Self::Context,
) -> Result<Box<Self>, Error>;
}
/// 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,
}
impl<T> Default for GeneticNodeWrapper<T>
where
T: Clone,
{
fn default() -> Self {
GeneticNodeWrapper {
node: None,
state: GeneticState::Initialize,
generation: 1,
id: Uuid::new_v4(),
}
}
}
impl<T> GeneticNodeWrapper<T>
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),
state: GeneticState::Simulate,
generation: 1,
id,
}
}
/// 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,
id: self.id,
gemla_context,
};
match (self.state, &mut self.node) {
(GeneticState::Initialize, _) => {
self.node = Some(*T::initialize(context.clone()).await?);
self.state = GeneticState::Simulate;
}
(GeneticState::Simulate, Some(n)) => {
let next_generation = n
.simulate(context.clone())
.await
.with_context(|| format!("Error simulating node: {:?}", self))?;
info!("Simulation complete and continuing: {:?}", next_generation);
self.state = if next_generation {
GeneticState::Mutate
} else {
GeneticState::Finish
};
}
(GeneticState::Mutate, Some(n)) => {
n.mutate(context.clone())
.await
.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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use anyhow::anyhow;
use async_trait::async_trait;
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
struct TestState {
pub score: f64,
pub max_generations: u64,
}
#[async_trait]
impl GeneticNode for TestState {
type Context = ();
async fn simulate(
&mut self,
context: GeneticNodeContext<Self::Context>,
) -> Result<bool, Error> {
self.score += 1.0;
if context.generation >= self.max_generations {
Ok(false)
} else {
Ok(true)
}
}
async fn mutate(
&mut self,
_context: GeneticNodeContext<Self::Context>,
) -> Result<(), Error> {
Ok(())
}
async fn initialize(
_context: GeneticNodeContext<Self::Context>,
) -> Result<Box<TestState>, Error> {
Ok(Box::new(TestState {
score: 0.0,
max_generations: 2,
}))
}
async fn merge(
_l: &TestState,
_r: &TestState,
_id: &Uuid,
_: Self::Context,
) -> Result<Box<TestState>, Error> {
Err(Error::Other(anyhow!("Unable to merge")))
}
}
#[test]
fn test_new() -> Result<(), Error> {
let genetic_node = GeneticNodeWrapper::<TestState>::new();
let other_genetic_node = GeneticNodeWrapper::<TestState> {
node: None,
state: GeneticState::Initialize,
generation: 1,
id: genetic_node.id(),
};
assert_eq!(genetic_node, other_genetic_node);
Ok(())
}
#[test]
fn test_from() -> Result<(), Error> {
let val = TestState {
score: 0.0,
max_generations: 10,
};
let uuid = Uuid::new_v4();
let genetic_node = GeneticNodeWrapper::from(val.clone(), uuid);
let other_genetic_node = GeneticNodeWrapper::<TestState> {
node: Some(val),
state: GeneticState::Simulate,
generation: 1,
id: genetic_node.id(),
};
assert_eq!(genetic_node, other_genetic_node);
Ok(())
}
#[test]
fn test_as_ref() -> Result<(), Error> {
let val = TestState {
score: 3.0,
max_generations: 10,
};
let uuid = Uuid::new_v4();
let genetic_node = GeneticNodeWrapper::from(val.clone(), uuid);
let ref_value = genetic_node.as_ref().unwrap();
assert_eq!(*ref_value, val);
Ok(())
}
#[test]
fn test_id() -> Result<(), Error> {
let val = TestState {
score: 3.0,
max_generations: 10,
};
let uuid = Uuid::new_v4();
let genetic_node = GeneticNodeWrapper::from(val.clone(), uuid);
let id_value = genetic_node.id();
assert_eq!(id_value, uuid);
Ok(())
}
#[test]
fn test_state() -> Result<(), Error> {
let val = TestState {
score: 3.0,
max_generations: 10,
};
let uuid = Uuid::new_v4();
let genetic_node = GeneticNodeWrapper::from(val.clone(), uuid);
let state = genetic_node.state();
assert_eq!(state, GeneticState::Simulate);
Ok(())
}
#[tokio::test]
async fn test_process_node() -> Result<(), Error> {
let mut genetic_node = GeneticNodeWrapper::<TestState>::new();
assert_eq!(genetic_node.state(), GeneticState::Initialize);
assert_eq!(genetic_node.process_node(()).await?, GeneticState::Simulate);
assert_eq!(genetic_node.process_node(()).await?, GeneticState::Mutate);
assert_eq!(genetic_node.process_node(()).await?, GeneticState::Simulate);
assert_eq!(genetic_node.process_node(()).await?, GeneticState::Finish);
assert_eq!(genetic_node.process_node(()).await?, GeneticState::Finish);
Ok(())
}
}