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    Pair(&'static str, Arity, Param<(T, T)>, fn(&[T], &(T, T)) -> T),
54}
55
56impl<T> Op<T> {
57    pub fn name(&self) -> &str {
58        match self {
59            Op::Fn(name, _, _) => name,
60            Op::Var(name, _, _) => name,
61            Op::Const(name, _) => name,
62            Op::Value(name, _, _, _) => name,
63            Op::Pair(name, _, _, _) => name,
64        }
65    }
66
67    pub fn arity(&self) -> Arity {
68        match self {
69            Op::Fn(_, arity, _) => *arity,
70            Op::Var(_, _, _) => Arity::Zero,
71            Op::Const(_, _) => Arity::Zero,
72            Op::Value(_, arity, _, _) => *arity,
73            Op::Pair(_, arity, _, _) => *arity,
74        }
75    }
76
77    pub fn is_fn(&self) -> bool {
78        matches!(self, Op::Fn(_, _, _))
79    }
80
81    pub fn is_var(&self) -> bool {
82        matches!(self, Op::Var(_, _, _))
83    }
84
85    pub fn is_const(&self) -> bool {
86        matches!(self, Op::Const(_, _))
87    }
88
89    pub fn is_value(&self) -> bool {
90        matches!(self, Op::Value(_, _, _, _))
91    }
92}
93
94impl<T> Eval<[T], T> for Op<T>
95where
96    T: Clone,
97{
98    fn eval(&self, inputs: &[T]) -> T {
99        match self {
100            Op::Fn(_, _, op) => op(inputs),
101            Op::Var(_, index, _) => inputs[*index].clone(),
102            Op::Const(_, value) => value.clone(),
103            Op::Value(_, _, value, operation) => operation(inputs, value.data()),
104            Op::Pair(_, _, value, operation) => operation(inputs, value.data()),
105        }
106    }
107}
108
109impl<T> Factory<(), Op<T>> for Op<T>
110where
111    T: Clone,
112{
113    fn new_instance(&self, _: ()) -> Op<T> {
114        match self {
115            Op::Fn(name, arity, op) => Op::Fn(name, *arity, *op),
116            Op::Var(name, index, domain) => Op::Var(name, *index, *domain),
117            Op::Const(name, value) => Op::Const(name, value.clone()),
118            Op::Value(name, arity, value, operation) => {
119                Op::Value(name, *arity, value.new_instance(()), *operation)
120            }
121            Op::Pair(name, arity, value, operation) => {
122                Op::Pair(name, *arity, value.new_instance(()), *operation)
123            }
124        }
125    }
126}
127
128impl<T> Clone for Op<T>
129where
130    T: Clone,
131{
132    fn clone(&self) -> Self {
133        match self {
134            Op::Fn(name, arity, op) => Op::Fn(name, *arity, *op),
135            Op::Var(name, index, domain) => Op::Var(name, *index, *domain),
136            Op::Const(name, value) => Op::Const(name, value.clone()),
137            Op::Value(name, arity, value, operation) => {
138                Op::Value(name, *arity, value.clone(), *operation)
139            }
140            Op::Pair(name, arity, value, operation) => {
141                Op::Pair(name, *arity, value.clone(), *operation)
142            }
143        }
144    }
145}
146
147impl<T> PartialEq for Op<T>
148where
149    T: PartialEq,
150{
151    fn eq(&self, other: &Self) -> bool {
152        self.name() == other.name()
153            && self.arity() == other.arity()
154            && match (self, other) {
155                (Op::Fn(_, _, _), Op::Fn(_, _, _)) => true,
156                (Op::Var(_, idx_a, card_a), Op::Var(_, idx_b, card_b)) => {
157                    idx_a == idx_b && card_a == card_b
158                }
159                (Op::Const(_, val_a), Op::Const(_, val_b)) => val_a == val_b,
160                (Op::Value(_, _, val_a, _), Op::Value(_, _, val_b, _)) => val_a == val_b,
161                (Op::Pair(_, _, val_a, _), Op::Pair(_, _, val_b, _)) => val_a == val_b,
162                _ => false,
163            }
164    }
165}
166
167impl Hash for Op<f32> {
168    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
169        self.name().hash(state);
170        self.arity().hash(state);
171        match self {
172            Op::Fn(_, _, op) => {
173                let op_ptr = *op as usize;
174                op_ptr.hash(state);
175            }
176            Op::Var(_, index, domain) => {
177                index.hash(state);
178                domain.hash(state);
179            }
180            Op::Const(_, value) => {
181                value.to_bits().hash(state);
182            }
183            Op::Value(_, _, value, operation) => {
184                (*value).data().to_bits().hash(state);
185                let op_ptr = *operation as usize;
186                op_ptr.hash(state);
187            }
188            Op::Pair(_, _, value, operation) => {
189                let data = (*value).data();
190                let b_one = data.0.to_bits();
191                let b_two = data.1.to_bits();
192                b_one.hash(state);
193                b_two.hash(state);
194                let op_ptr = *operation as usize;
195                op_ptr.hash(state);
196            }
197        }
198    }
199}
200
201impl<T> Display for Op<T> {
202    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
203        write!(f, "{}", self.name())
204    }
205}
206
207impl<T> Default for Op<T>
208where
209    T: Default,
210{
211    fn default() -> Self {
212        Op::Fn("default", Arity::Zero, |_: &[T]| T::default())
213    }
214}
215
216impl<T> Debug for Op<T>
217where
218    T: Debug,
219{
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        match self {
222            Op::Fn(name, _, _) => write!(f, "Fn:  {}", name),
223            Op::Var(name, index, card) => match card {
224                Some(k) => write!(f, "Var: {}({},{})", name, index, k),
225                None => write!(f, "Var: {}({})", name, index),
226            },
227            Op::Const(name, value) => match f.precision() {
228                Some(p) => write!(f, "Con: {}({:.*?})", name, p, value),
229                None => write!(f, "Con: {}({:?})", name, value),
230            },
231            Op::Value(name, _, value, _) => match f.precision() {
232                Some(p) => write!(f, "Val: {}({:.*?})", name, p, value),
233                None => write!(f, "Val: {}({:?})", name, value),
234            },
235            Op::Pair(name, _, value, _) => match f.precision() {
236                Some(p) => write!(f, "Pair: {}{:.*?}", name, p, value),
237                None => write!(f, "Pair: {}{:?}", name, value),
238            },
239        }
240    }
241}
242
243impl<T: Clone> From<Op<T>> for NodeValue<Op<T>> {
244    fn from(value: Op<T>) -> Self {
245        let arity = value.arity();
246        NodeValue::Bounded(value, arity)
247    }
248}
249
250impl<T> From<Op<T>> for TreeNode<Op<T>> {
251    fn from(value: Op<T>) -> Self {
252        let arity = value.arity();
253        TreeNode::with_arity(value, arity)
254    }
255}
256
257impl<T> From<Op<T>> for Vec<TreeNode<Op<T>>> {
258    fn from(value: Op<T>) -> Self {
259        vec![TreeNode::from(value)]
260    }
261}
262
263#[cfg(test)]
264mod test {
265    use super::*;
266
267    #[test]
268    fn test_ops() {
269        let op = Op::add();
270        assert_eq!(op.name(), "add");
271        assert_eq!(op.arity(), Arity::Exact(2));
272        assert_eq!(op.eval(&[1_f32, 2_f32]), 3_f32);
273        assert_eq!(op.new_instance(()), op);
274    }
275
276    #[test]
277    fn test_op_clone() {
278        let op = Op::add();
279        let op2 = op.clone();
280
281        let result = op.eval(&[1_f32, 2_f32]);
282        let result2 = op2.eval(&[1_f32, 2_f32]);
283
284        assert_eq!(op, op2);
285        assert_eq!(result, result2);
286    }
287}