use std::ops::Range;
use rand::{Rng, RngExt};
use crate::function_set::{FunctionSet, Symbol};
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn function_symbol(i: usize) -> Symbol {
Symbol::from_raw(i as i32)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SymbolKind {
Function {
arity: usize,
},
Variable {
input_index: usize,
},
Constant {
value: f32,
},
}
#[derive(Debug, Clone)]
pub struct Alphabet<F: FunctionSet> {
pub functions: F,
pub n_vars: usize,
pub constants: Vec<f32>,
}
impl<F: FunctionSet> Alphabet<F> {
#[must_use]
pub fn new(functions: F, n_vars: usize, constants: Vec<f32>) -> Self {
let alphabet = Self {
functions,
n_vars,
constants,
};
debug_assert!(
alphabet.zero_arity_functions_are_trailing(),
"Alphabet: arity-0 functions must be the trailing function ids so \
terminal_range() is contiguous"
);
alphabet
}
#[must_use]
pub fn n_func(&self) -> usize {
self.functions.num_functions()
}
#[must_use]
pub fn len(&self) -> usize {
self.n_func() + self.n_vars + self.constants.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn max_arity(&self) -> usize {
self.functions.max_arity()
}
#[must_use]
pub fn arity(&self, symbol: Symbol) -> usize {
match usize::try_from(symbol.value()) {
Ok(id) if id < self.n_func() => self.functions.arity(symbol),
_ => 0,
}
}
#[must_use]
pub fn classify(&self, symbol: Symbol) -> SymbolKind {
let n_func = self.n_func();
let n_vars = self.n_vars;
let n_const = self.constants.len();
let Ok(id_u) = usize::try_from(symbol.value()) else {
return SymbolKind::Function { arity: 0 };
};
if id_u < n_func {
SymbolKind::Function {
arity: self.functions.arity(symbol),
}
} else if id_u < n_func + n_vars {
SymbolKind::Variable {
input_index: id_u - n_func,
}
} else if id_u < n_func + n_vars + n_const {
SymbolKind::Constant {
value: self.constants[id_u - n_func - n_vars],
}
} else {
SymbolKind::Function { arity: 0 }
}
}
#[must_use]
pub fn terminal_range(&self) -> Range<i32> {
let n_func = self.n_func();
let first_terminal = (0..n_func).find(|&i| self.functions.arity(function_symbol(i)) == 0);
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let start = first_terminal.unwrap_or(n_func) as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let end = self.len() as i32;
start..end
}
pub fn sample_head_symbol(&self, rng: &mut dyn Rng) -> Symbol {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let upper = self.len() as i32;
Symbol::from_raw(rng.random_range(0..upper))
}
pub fn sample_tail_symbol(&self, rng: &mut dyn Rng) -> Symbol {
let range = self.terminal_range();
Symbol::from_raw(rng.random_range(range))
}
fn zero_arity_functions_are_trailing(&self) -> bool {
let n_func = self.n_func();
let mut seen_zero = false;
for i in 0..n_func {
let arity = self.functions.arity(function_symbol(i));
if arity == 0 {
seen_zero = true;
} else if seen_zero {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::function_set::ArithmeticFunctionSet;
use crate::rng::{SeedPurpose, seed_stream};
fn alphabet(n_vars: usize, constants: Vec<f32>) -> Alphabet<ArithmeticFunctionSet> {
Alphabet::new(ArithmeticFunctionSet, n_vars, constants)
}
#[derive(Debug)]
struct BadLayoutFunctionSet;
impl FunctionSet for BadLayoutFunctionSet {
fn num_functions(&self) -> usize {
2
}
fn arity(&self, symbol: Symbol) -> usize {
match symbol.value() {
1 => 2,
_ => 0,
}
}
fn max_arity(&self) -> usize {
2
}
fn apply(&self, _symbol: Symbol, _args: &[f32]) -> f32 {
0.0
}
}
#[test]
fn id_space_layout() {
let a = alphabet(2, vec![2.5]);
assert_eq!(a.n_func(), 8);
assert_eq!(a.len(), 11);
assert_eq!(
a.classify(Symbol::from_raw(0)),
SymbolKind::Function { arity: 2 }
);
assert_eq!(
a.classify(Symbol::from_raw(7)),
SymbolKind::Function { arity: 0 }
);
assert_eq!(
a.classify(Symbol::from_raw(8)),
SymbolKind::Variable { input_index: 0 }
);
assert_eq!(
a.classify(Symbol::from_raw(9)),
SymbolKind::Variable { input_index: 1 }
);
assert_eq!(
a.classify(Symbol::from_raw(10)),
SymbolKind::Constant { value: 2.5 }
);
}
#[test]
fn terminal_range_starts_at_first_zero_arity_function() {
let a = alphabet(2, vec![1.0]);
assert_eq!(a.terminal_range(), 7..11);
}
#[test]
fn terminal_range_without_constants() {
let a = alphabet(1, vec![]);
assert_eq!(a.terminal_range(), 7..9);
}
#[test]
fn arity_is_zero_for_terminals() {
let a = alphabet(2, vec![1.0]);
assert_eq!(a.arity(Symbol::from_raw(0)), 2);
assert_eq!(a.arity(Symbol::from_raw(7)), 0);
assert_eq!(a.arity(Symbol::from_raw(8)), 0); assert_eq!(a.arity(Symbol::from_raw(10)), 0); }
#[test]
fn out_of_range_classifies_inert() {
let a = alphabet(1, vec![]);
assert_eq!(
a.classify(Symbol::from_raw(-1)),
SymbolKind::Function { arity: 0 }
);
assert_eq!(
a.classify(Symbol::from_raw(999)),
SymbolKind::Function { arity: 0 }
);
}
#[test]
#[should_panic(expected = "arity-0 functions must be the trailing")]
fn new_panics_when_zero_arity_function_precedes_a_function() {
let _ = Alphabet::new(BadLayoutFunctionSet, 1, vec![]);
}
#[test]
fn sample_tail_symbol_is_always_a_terminal() {
let a: Alphabet<ArithmeticFunctionSet> = alphabet(2, vec![1.0, 2.0]);
let range: Range<i32> = a.terminal_range();
let mut rng = seed_stream(1, 0, SeedPurpose::Mutation);
for _ in 0..1000 {
let s: Symbol = a.sample_tail_symbol(&mut rng);
assert_eq!(a.arity(s), 0, "tail symbol {s} is not a terminal");
assert!(range.contains(&s.value()), "tail symbol {s} outside range");
}
}
#[test]
fn sample_head_symbol_stays_in_id_space() {
let a: Alphabet<ArithmeticFunctionSet> = alphabet(2, vec![1.0]);
let len: i32 = i32::try_from(a.len()).unwrap();
let mut rng = seed_stream(2, 0, SeedPurpose::Init);
for _ in 0..1000 {
let s: Symbol = a.sample_head_symbol(&mut rng);
assert!(
(0..len).contains(&s.value()),
"head symbol {s} outside 0..{len}"
);
}
}
#[test]
fn non_finite_constant_is_a_terminal() {
let a: Alphabet<ArithmeticFunctionSet> = alphabet(1, vec![f32::NAN, f32::INFINITY]);
let range: Range<i32> = a.terminal_range();
assert_eq!(a.arity(Symbol::from_raw(9)), 0);
assert_eq!(a.arity(Symbol::from_raw(10)), 0);
assert!(range.contains(&9) && range.contains(&10));
match a.classify(Symbol::from_raw(9)) {
SymbolKind::Constant { value } => assert!(value.is_nan()),
other => panic!("expected Constant, got {other:?}"),
}
match a.classify(Symbol::from_raw(10)) {
SymbolKind::Constant { value } => assert!(value.is_infinite()),
other => panic!("expected Constant, got {other:?}"),
}
}
}