Skip to main content

eml_core/
operator.rs

1//! Core EML operators.
2//!
3//! The EML operator `eml(x, y) = exp(x) - ln(y)` is the continuous-mathematics
4//! analog of the NAND gate: combined with the constant 1, it can reconstruct
5//! all elementary functions (Odrzywolel 2026).
6
7/// The EML universal operator: `eml(x, y) = exp(x) - ln(y)`.
8///
9/// Combined with the constant 1, this single operator can reconstruct
10/// all elementary functions.
11#[inline]
12pub fn eml(x: f64, y: f64) -> f64 {
13    x.exp() - y.ln()
14}
15
16/// Numerically safe EML: clamps exp input to [-20, 20] and ensures
17/// a positive ln argument.
18///
19/// Use this instead of [`eml`] in evaluation trees where inputs may
20/// be out of range.
21#[inline]
22pub fn eml_safe(x: f64, y: f64) -> f64 {
23    let ex = x.clamp(-20.0, 20.0).exp();
24    let ly = if y > 0.0 {
25        y.ln()
26    } else {
27        f64::MIN_POSITIVE.ln()
28    };
29    ex - ly
30}
31
32/// Softmax over 3 values so that `alpha + beta + gamma = 1`.
33///
34/// Used as a mixing function in EML tree levels.
35#[inline]
36pub fn softmax3(a: f64, b: f64, c: f64) -> (f64, f64, f64) {
37    let max = a.max(b).max(c);
38    let ea = (a - max).exp();
39    let eb = (b - max).exp();
40    let ec = (c - max).exp();
41    let sum = ea + eb + ec;
42    (ea / sum, eb / sum, ec / sum)
43}
44
45/// Generate random parameters in [-1, 1] using a simple LCG.
46///
47/// This is a deterministic PRNG suitable for random restarts during
48/// coordinate descent training. Not cryptographically secure.
49pub(crate) fn random_params(state: &mut u64, count: usize) -> Vec<f64> {
50    let mut params = vec![0.0f64; count];
51    for p in params.iter_mut() {
52        *state = state
53            .wrapping_mul(6364136223846793005)
54            .wrapping_add(1442695040888963407);
55        *p = (*state >> 33) as f64 / (u32::MAX as f64 / 2.0) - 1.0;
56    }
57    params
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn eml_identity() {
66        // eml(0, 1) = exp(0) - ln(1) = 1 - 0 = 1
67        let result = eml(0.0, 1.0);
68        assert!(
69            (result - 1.0).abs() < 1e-12,
70            "eml(0, 1) should be 1.0, got {result}"
71        );
72    }
73
74    #[test]
75    fn eml_exp_only() {
76        // eml(1, 1) = exp(1) - ln(1) = e
77        let result = eml(1.0, 1.0);
78        assert!(
79            (result - std::f64::consts::E).abs() < 1e-12,
80            "eml(1, 1) should be e, got {result}"
81        );
82    }
83
84    #[test]
85    fn eml_ln_only() {
86        // eml(0, e) = exp(0) - ln(e) = 1 - 1 = 0
87        let result = eml(0.0, std::f64::consts::E);
88        assert!(
89            result.abs() < 1e-12,
90            "eml(0, e) should be 0.0, got {result}"
91        );
92    }
93
94    #[test]
95    fn eml_safe_does_not_panic() {
96        let _ = eml_safe(100.0, 0.0);
97        let _ = eml_safe(-100.0, -5.0);
98        let _ = eml_safe(0.0, f64::MIN_POSITIVE);
99        let _ = eml_safe(f64::NAN, 1.0);
100    }
101
102    #[test]
103    fn eml_safe_clamps_large_exp() {
104        let result = eml_safe(100.0, 1.0);
105        // Should use exp(20) not exp(100)
106        assert!(result.is_finite(), "eml_safe(100, 1) should be finite");
107        let expected = 20.0_f64.exp(); // ln(1) = 0
108        assert!(
109            (result - expected).abs() < 1e-6,
110            "eml_safe(100, 1) should be exp(20), got {result}"
111        );
112    }
113
114    #[test]
115    fn softmax3_sums_to_one() {
116        let (a, b, c) = softmax3(1.0, 2.0, 3.0);
117        let sum = a + b + c;
118        assert!(
119            (sum - 1.0).abs() < 1e-12,
120            "softmax3 should sum to 1.0, got {sum}"
121        );
122    }
123
124    #[test]
125    fn softmax3_equal_inputs() {
126        let (a, b, c) = softmax3(0.0, 0.0, 0.0);
127        assert!((a - 1.0 / 3.0).abs() < 1e-12);
128        assert!((b - 1.0 / 3.0).abs() < 1e-12);
129        assert!((c - 1.0 / 3.0).abs() < 1e-12);
130    }
131
132    #[test]
133    fn softmax3_dominated_input() {
134        let (a, _b, _c) = softmax3(100.0, 0.0, 0.0);
135        assert!(a > 0.99, "dominant input should get nearly all weight");
136    }
137
138    #[test]
139    fn random_params_deterministic() {
140        let mut s1 = 42u64;
141        let mut s2 = 42u64;
142        let p1 = random_params(&mut s1, 10);
143        let p2 = random_params(&mut s2, 10);
144        assert_eq!(p1, p2, "same seed should produce same params");
145    }
146
147    #[test]
148    fn random_params_in_range() {
149        let mut s = 0xDEAD_BEEF_u64;
150        let params = random_params(&mut s, 100);
151        for &p in &params {
152            assert!(
153                p >= -1.0 && p <= 1.0,
154                "param {p} out of [-1, 1] range"
155            );
156        }
157    }
158}