Skip to main content

radiate_gp/ops/
operation.rs

1use crate::{Arity, Eval, Factory, NodeValue, TreeNode, ops::Param};
2use std::{
3    fmt::{Debug, Display},
4    hash::Hash,
5};
6
7/// [Op] is an enumeration that represents the different types of operations
8/// that can be performed within the genetic programming framework. Each variant
9/// of the enum encapsulates a different kind of operation, allowing for a flexible
10/// and extensible way to define the behavior of nodes within trees and graphs.
11///
12/// The [Op] heavilty depends on it's [Arity] to define how many inputs it expects.
13/// This is crucial for ensuring that the operations receive the correct number of inputs
14/// and that the structures built using these operations are built in ways that respect
15/// these input requirements. For example, an addition operation would typically have an arity of 2,
16/// while a constant operation would have an arity of 0. This is the _base_ level of the GP system, meaning
17
18/// that everything built on top of it (trees, graphs, etc.) will relies *heavily* on how these
19/// operations are defined and used.
20pub enum Op<T> {
21    /// 1) A stateless function operation:
22    ///
23    /// # Arguments
24    /// - A `&'static str` name (e.g., "Add", "Sigmoid")
25    /// - Arity (how many inputs it takes)
26    /// - Arc<dyn Fn(&`\[`T`\]`) -> T> for the actual function logic
27    Fn(&'static str, Arity, fn(&[T]) -> T),
28    /// 2) A variable-like operation:
29    ///
30    /// # Arguments
31    /// - `String` = a name or identifier
32    /// - `usize` = an index to retrieve from some external context
33    /// - `Option<usize>` = an optional domain size for categorical variables
34    Var(&'static str, usize, Option<usize>),
35    /// 3) A compile-time constant: e.g., 1, 2, 3, etc.
36    ///
37    /// # Arguments
38    /// - `&'static str` name
39    /// - `T` the actual constant value
40    Const(&'static str, T),
41    /// 4) A value-based operation that encapsulates data and an operation to process it.
42    ///
43    /// This allows for operations that can hold state or data, such as weights in a neural
44    /// network, and apply a specific function to that data when evaluated.
45    ///
46    /// # Arguments
47    /// - `&'static str` name
48    /// - `Arity` of how many inputs it might read
49    /// - `Param<T>` the actual data/value associated with this operation
50    /// - An `fn(&[T], &T) -> T` for the function logic that uses the inputs and the value to produce an output.
51    Value(&'static str, Arity, Param<T>, fn(&[T], &T) -> T),
52}
53
54impl<T> Op<T> {
55    pub fn name(&self) -> &str {
56        match self {
57            Op::Fn(name, _, _) => name,
58            Op::Var(name, _, _) => name,
59            Op::Const(name, _) => name,
60            Op::Value(name, _, _, _) => name,
61        }
62    }
63
64    pub fn arity(&self) -> Arity {
65        match self {
66            Op::Fn(_, arity, _) => *arity,
67            Op::Var(_, _, _) => Arity::Zero,
68            Op::Const(_, _) => Arity::Zero,
69            Op::Value(_, arity, _, _) => *arity,
70        }
71    }
72
73    pub fn value(&self) -> Option<&T> {
74        match self {
75            Op::Value(_, _, value, _) => Some(value.data()),
76            _ => None,
77        }
78    }
79
80    pub fn is_fn(&self) -> bool {
81        matches!(self, Op::Fn(_, _, _))
82    }
83
84    pub fn is_var(&self) -> bool {
85        matches!(self, Op::Var(_, _, _))
86    }
87
88    pub fn is_const(&self) -> bool {
89        matches!(self, Op::Const(_, _))
90    }
91
92    pub fn is_value(&self) -> bool {
93        matches!(self, Op::Value(_, _, _, _))
94    }
95}
96
97impl<T> Eval<[T], T> for Op<T>
98where
99    T: Clone,
100{
101    fn eval(&self, inputs: &[T]) -> T {
102        match self {
103            Op::Fn(_, _, op) => op(inputs),
104            Op::Var(_, index, _) => inputs[*index].clone(),
105            Op::Const(_, value) => value.clone(),
106            Op::Value(_, _, value, operation) => operation(inputs, value.data()),
107        }
108    }
109}
110
111impl<T> Factory<(), Op<T>> for Op<T>
112where
113    T: Clone,
114{
115    fn new_instance(&self, _: ()) -> Op<T> {
116        match self {
117            Op::Fn(name, arity, op) => Op::Fn(name, *arity, *op),
118            Op::Var(name, index, domain) => Op::Var(name, *index, domain.clone()),
119            Op::Const(name, value) => Op::Const(name, value.clone()),
120            Op::Value(name, arity, value, operation) => {
121                Op::Value(name, *arity, value.new_instance(()), *operation)
122            }
123        }
124    }
125}
126
127impl<T> Clone for Op<T>
128where
129    T: Clone,
130{
131    fn clone(&self) -> Self {
132        match self {
133            Op::Fn(name, arity, op) => Op::Fn(name, *arity, *op),
134            Op::Var(name, index, domain) => Op::Var(name, *index, domain.clone()),
135            Op::Const(name, value) => Op::Const(name, value.clone()),
136            Op::Value(name, arity, value, operation) => {
137                Op::Value(name, *arity, value.clone(), *operation)
138            }
139        }
140    }
141}
142
143impl<T> PartialEq for Op<T>
144where
145    T: PartialEq,
146{
147    fn eq(&self, other: &Self) -> bool {
148        self.name() == other.name()
149            && self.arity() == other.arity()
150            && match (self, other) {
151                (Op::Fn(_, _, _), Op::Fn(_, _, _)) => true,
152                (Op::Var(_, idx_a, card_a), Op::Var(_, idx_b, card_b)) => {
153                    idx_a == idx_b && card_a == card_b
154                }
155                (Op::Const(_, val_a), Op::Const(_, val_b)) => val_a == val_b,
156                (Op::Value(_, _, val_a, _), Op::Value(_, _, val_b, _)) => val_a == val_b,
157                _ => false,
158            }
159    }
160}
161
162impl Hash for Op<f32> {
163    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
164        self.name().hash(state);
165        self.arity().hash(state);
166        match self {
167            Op::Fn(_, _, op) => {
168                let op_ptr = *op as usize;
169                op_ptr.hash(state);
170            }
171            Op::Var(_, index, domain) => {
172                index.hash(state);
173                domain.hash(state);
174            }
175            Op::Const(_, value) => {
176                value.to_bits().hash(state);
177            }
178            Op::Value(_, _, value, operation) => {
179                (*value).data().to_bits().hash(state);
180                let op_ptr = *operation as usize;
181                op_ptr.hash(state);
182            }
183        }
184    }
185}
186
187impl<T> Display for Op<T> {
188    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
189        write!(f, "{}", self.name())
190    }
191}
192
193impl<T> Default for Op<T>
194where
195    T: Default,
196{
197    fn default() -> Self {
198        Op::Fn("default", Arity::Zero, |_: &[T]| T::default())
199    }
200}
201
202impl<T> Debug for Op<T>
203where
204    T: Debug,
205{
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        match self {
208            Op::Fn(name, _, _) => write!(f, "Fn: {}", name),
209            Op::Var(name, index, card) => match card {
210                Some(k) => {
211                    write!(f, "Var: {}({}, Cat:{})", name, index, k)
212                }
213                None => write!(f, "Var: {}({})", name, index),
214            },
215            Op::Const(name, value) => write!(f, "C: {}({:?})", name, value),
216            Op::Value(name, _, value, _) => {
217                write!(f, "Val: {}({:?})", name, value)
218            }
219        }
220    }
221}
222
223impl<T: Clone> From<Op<T>> for NodeValue<Op<T>> {
224    fn from(value: Op<T>) -> Self {
225        let arity = value.arity();
226        NodeValue::Bounded(value, arity)
227    }
228}
229
230impl<T> From<Op<T>> for TreeNode<Op<T>> {
231    fn from(value: Op<T>) -> Self {
232        let arity = value.arity();
233        TreeNode::with_arity(value, arity)
234    }
235}
236
237impl<T> From<Op<T>> for Vec<TreeNode<Op<T>>> {
238    fn from(value: Op<T>) -> Self {
239        vec![TreeNode::from(value)]
240    }
241}
242
243#[cfg(test)]
244mod test {
245    use super::*;
246
247    #[test]
248    fn test_ops() {
249        let op = Op::add();
250        assert_eq!(op.name(), "add");
251        assert_eq!(op.arity(), Arity::Exact(2));
252        assert_eq!(op.eval(&[1_f32, 2_f32]), 3_f32);
253        assert_eq!(op.new_instance(()), op);
254    }
255
256    #[test]
257    fn test_op_clone() {
258        let op = Op::add();
259        let op2 = op.clone();
260
261        let result = op.eval(&[1_f32, 2_f32]);
262        let result2 = op2.eval(&[1_f32, 2_f32]);
263
264        assert_eq!(op, op2);
265        assert_eq!(result, result2);
266    }
267}