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