//! 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 { /// 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, /// pub visible_simulations: Arc, /// } /// ``` /// 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, /// max_generations: u64, /// } /// /// #[async_trait] /// impl GeneticNode for TestState { /// type Context = (); /// async fn initialize(_context: GeneticNodeContext) -> Result, 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) -> Result, 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, /// max_generations: u64, /// } /// /// #[async_trait] /// impl GeneticNode for TestState { /// type Context = (); /// async fn simulate(&mut self, context: GeneticNodeContext) -> Result { /// 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) -> Result; /// 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, /// max_generations: u64, /// } /// /// #[async_trait] /// impl GeneticNode for TestState { /// type Context = (); /// async fn mutate(&mut self, _context: GeneticNodeContext) -> 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) -> 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, /// 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, 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, 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 where T: Clone, { /// The wrapped node instance, if initialized. node: Option, /// 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 Default for GeneticNodeWrapper where T: Clone, { fn default() -> Self { GeneticNodeWrapper { node: None, state: GeneticState::Initialize, generation: 1, id: Uuid::new_v4(), } } } impl GeneticNodeWrapper 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:: { ..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 { 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 { 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, ) -> Result { self.score += 1.0; if context.generation >= self.max_generations { Ok(false) } else { Ok(true) } } async fn mutate( &mut self, _context: GeneticNodeContext, ) -> Result<(), Error> { Ok(()) } async fn initialize( _context: GeneticNodeContext, ) -> Result, Error> { Ok(Box::new(TestState { score: 0.0, max_generations: 2, })) } async fn merge( _l: &TestState, _r: &TestState, _id: &Uuid, _: Self::Context, ) -> Result, Error> { Err(Error::Other(anyhow!("Unable to merge"))) } } #[test] fn test_new() -> Result<(), Error> { let genetic_node = GeneticNodeWrapper::::new(); let other_genetic_node = GeneticNodeWrapper:: { 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:: { 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::::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(()) } }