Skip to main content

trueno/
activations.rs

1//! Canonical scalar activation functions.
2//!
3//! # One Path Rule (UCBD §4)
4//!
5//! These are THE canonical implementations for scalar activation functions.
6//! All downstream crates (aprender, realizar, entrenar, whisper-apr) MUST
7//! import from here instead of re-implementing.
8//!
9//! For SIMD-vectorized slice operations, see `backends::*/ops/activations`.
10//! For `Vector`-level operations, see `vector::ops::activations`.
11
12/// SiLU (Sigmoid Linear Unit) / Swish activation: x * σ(x).
13///
14/// # Equation
15/// ```text
16/// SiLU(x) = x * σ(x) = x / (1 + exp(-x))
17/// ```
18///
19/// # Contract
20/// - Domain: x ∈ ℝ
21/// - Codomain: SiLU(x) ∈ (-0.278..., ∞)
22/// - SiLU(0) = 0
23/// - limₓ→∞ SiLU(x) = x
24/// - limₓ→-∞ SiLU(x) = 0
25#[inline]
26#[must_use]
27pub fn silu_scalar(x: f32) -> f32 {
28    x / (1.0 + (-x).exp())
29}
30
31/// GELU (Gaussian Error Linear Unit) activation.
32///
33/// Uses the fast tanh approximation (same as PyTorch `gelu('tanh')`).
34///
35/// # Equation
36/// ```text
37/// GELU(x) ≈ 0.5 * x * (1 + tanh(√(2/π) * (x + 0.044715 * x³)))
38/// ```
39///
40/// # Contract
41/// - Domain: x ∈ ℝ
42/// - Codomain: GELU(x) ∈ (-0.170..., ∞)
43/// - GELU(0) = 0
44/// - limₓ→∞ GELU(x) = x
45/// - limₓ→-∞ GELU(x) = 0
46#[inline]
47#[must_use]
48pub fn gelu_scalar(x: f32) -> f32 {
49    let c = (2.0_f32 / std::f32::consts::PI).sqrt();
50    0.5 * x * (1.0 + (c * (x + 0.044_715 * x * x * x)).tanh())
51}
52
53/// Sigmoid activation: σ(x) = 1 / (1 + exp(-x)).
54///
55/// # Equation
56/// ```text
57/// σ(x) = 1 / (1 + exp(-x))
58/// ```
59///
60/// # Contract
61/// - Domain: x ∈ ℝ
62/// - Codomain: σ(x) ∈ (0, 1)
63/// - σ(0) = 0.5
64/// - σ(-x) = 1 - σ(x) (symmetry)
65#[inline]
66#[must_use]
67pub fn sigmoid_scalar(x: f32) -> f32 {
68    1.0 / (1.0 + (-x).exp())
69}
70
71/// ReLU (Rectified Linear Unit) activation.
72///
73/// # Equation
74/// ```text
75/// ReLU(x) = max(0, x)
76/// ```
77///
78/// # Contract
79/// - Domain: x ∈ ℝ
80/// - Codomain: ReLU(x) ∈ [0, ∞)
81/// - ReLU(x) = 0 for x ≤ 0
82/// - ReLU(x) = x for x > 0
83#[inline]
84#[must_use]
85pub fn relu_scalar(x: f32) -> f32 {
86    x.max(0.0)
87}
88
89/// Tanh activation.
90///
91/// # Equation
92/// ```text
93/// tanh(x) = (exp(x) - exp(-x)) / (exp(x) + exp(-x))
94/// ```
95///
96/// # Contract
97/// - Domain: x ∈ ℝ
98/// - Codomain: tanh(x) ∈ (-1, 1)
99/// - tanh(0) = 0
100/// - tanh(-x) = -tanh(x) (odd function)
101#[inline]
102#[must_use]
103pub fn tanh_scalar(x: f32) -> f32 {
104    x.tanh()
105}
106
107/// f16 → f32 conversion (IEEE 754 half-precision).
108///
109/// Manual bit-manipulation implementation (no `half` crate dependency).
110/// Delegates to `tiling::q4k_matvec::f16_bits_to_f32` which is the
111/// existing canonical implementation in trueno.
112///
113/// # Contract
114/// - Domain: any u16 (interpreted as IEEE 754 binary16)
115/// - Codomain: f32 (exact representation, no precision loss for normal f16)
116/// - Subnormals, ±inf, NaN handled correctly
117#[inline]
118#[must_use]
119pub fn f16_to_f32(bits: u16) -> f32 {
120    let sign = (bits >> 15) & 0x1;
121    let exponent = (bits >> 10) & 0x1F;
122    let mantissa = bits & 0x3FF;
123
124    // Fast path: normal numbers
125    if exponent != 0 && exponent != 31 {
126        let f32_exp = (exponent as u32 + 112) as u32; // bias adjustment: 127 - 15 = 112
127        let f32_mant = (mantissa as u32) << 13; // 10 bits → 23 bits
128        let f32_bits = ((sign as u32) << 31) | (f32_exp << 23) | f32_mant;
129        return f32::from_bits(f32_bits);
130    }
131
132    // Special cases
133    if exponent == 0 {
134        if mantissa == 0 {
135            return if sign == 1 { -0.0 } else { 0.0 };
136        }
137        // Subnormal
138        const TWO_POW_NEG_14: f32 = 6.103_515_625e-5; // 2^-14
139        let m = mantissa as f32 * (1.0 / 1024.0);
140        let result = m * TWO_POW_NEG_14;
141        return if sign == 1 { -result } else { result };
142    }
143
144    // exponent == 31: Inf or NaN
145    if mantissa == 0 {
146        if sign == 1 {
147            f32::NEG_INFINITY
148        } else {
149            f32::INFINITY
150        }
151    } else {
152        f32::NAN
153    }
154}
155
156/// f32 → f16 conversion (IEEE 754 half-precision).
157///
158/// IEEE 754 round-to-nearest-even (RNE). Bit-identical to
159/// `half::f16::from_f32(x).to_bits()` across the entire f32 domain
160/// (normals, subnormals, ties-to-even, ±Inf, NaN, mantissa-overflow carry).
161///
162/// OBLIG-TRUENO-F32-F16-RNE (contracts/trueno-f16-rne-v1.yaml):
163/// `f32_to_f16(x) == half::f16::from_f32(x).to_bits()` for all `x`.
164///
165/// Root fix (PMAT-905 class) for the prior round-half-UP implementation, which
166/// (1) used a single round bit with no sticky bits → biased ties, and (2) masked
167/// the rounded mantissa with `& 0x03FF` on overflow, dropping the carry that must
168/// increment the exponent (e.g. 255.99 → `0x5C00`, not the buggy `0x5800`; the
169/// max-normal carry 65520 → `0x7C00` Inf). 31+ inputs diverged from IEEE RNE.
170///
171/// # Contract
172/// - Domain: f32
173/// - Codomain: u16 (IEEE 754 binary16 bits)
174/// - Rounds to nearest, ties to even
175#[inline]
176#[must_use]
177pub fn f32_to_f16(x: f32) -> u16 {
178    let bits = x.to_bits();
179    let sign = ((bits >> 16) & 0x8000) as u16;
180    let exp = ((bits >> 23) & 0xFF) as i32;
181    let mantissa = bits & 0x007F_FFFF;
182
183    // Inf / NaN: f32 exponent all ones.
184    if exp == 0xFF {
185        if mantissa == 0 {
186            return sign | 0x7C00; // ±Inf
187        }
188        // Quiet NaN, preserve top payload bits (matches half::f16).
189        return sign | 0x7E00 | ((mantissa >> 13) as u16);
190    }
191
192    // Rebias: f32 bias=127, f16 bias=15.
193    let unbiased = exp - 127;
194    let half_exp = unbiased + 15;
195
196    if half_exp >= 0x1F {
197        return sign | 0x7C00; // Overflow → ±Inf
198    }
199
200    if half_exp <= 0 {
201        // f32 subnormals (and zero) are far below the f16 subnormal range → ±0.
202        if exp == 0 {
203            return sign;
204        }
205        // f16 subnormal: shift the 24-bit significand (implicit leading 1) right by
206        // `-unbiased - 1` with round-to-nearest-even. Below 2^-25 → ±0.
207        let shift = -unbiased - 1;
208        if shift >= 25 {
209            return sign;
210        }
211        let full = mantissa | 0x0080_0000;
212        let rounded = round_shift_rne(full, shift as u32);
213        return sign | (rounded as u16);
214    }
215
216    // Normal: round the 23-bit mantissa to 10 bits with round-to-nearest-even.
217    // A rounding carry into bit 10 propagates into the exponent via `+` (NOT masked),
218    // and a max-normal carry correctly produces 0x7C00 (Inf), matching IEEE/half.
219    let rounded = round_shift_rne(mantissa, 13);
220    let combined = ((half_exp as u16) << 10) + rounded as u16;
221    sign | combined
222}
223
224/// Shift `value` right by `shift` bits using IEEE round-to-nearest, ties-to-even.
225#[inline]
226fn round_shift_rne(value: u32, shift: u32) -> u32 {
227    if shift == 0 {
228        return value;
229    }
230    if shift >= 32 {
231        return 0;
232    }
233    let result = value >> shift;
234    let round_bit = (value >> (shift - 1)) & 1;
235    if round_bit == 0 {
236        return result; // below the halfway point → round down
237    }
238    let sticky_mask = (1u32 << (shift - 1)) - 1;
239    if (value & sticky_mask) != 0 || (result & 1) == 1 {
240        // above halfway, OR exact tie with odd LSB → round up (to even)
241        result + 1
242    } else {
243        // exact tie with even LSB → round down (stay even)
244        result
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    // ------------------------------------------------------------------
253    // OBLIG-TRUENO-F32-F16-RNE: f32_to_f16 == IEEE round-to-nearest-even,
254    // bit-identical to half::f16::from_f32. Root fix (PMAT-905 class) for the
255    // prior round-half-UP + mantissa-overflow-carry bug.
256    // ------------------------------------------------------------------
257
258    /// The 31+ known divergences the round-half-UP implementation produced.
259    /// Each pair is (input, expected_bits). The buggy version returned the
260    /// wrong exponent on the mantissa-overflow carry (e.g. 255.99 → 0x5800).
261    #[test]
262    fn test_f32_to_f16_known_divergences_rne() {
263        // (value, expected f16 bits) — verified against half::f16::from_f32.
264        let cases: &[(f32, u16)] = &[
265            (255.99, 0x5C00),    // mantissa carry bumps exponent (buggy: 0x5800)
266            (-255.99, 0xDC00),   // signed twin (buggy: 0xD800)
267            (65520.0, 0x7C00),   // max-normal carry → Inf (buggy: 0x7800)
268            (-65520.0, 0xFC00),  // signed twin (buggy: 0xF800)
269            (-7.998071, 0xC800), // carry into next exponent (buggy: 0xC400)
270            (65504.0, 0x7BFF),   // largest finite f16, no carry
271            (1024.5, 0x6400),    // ties-to-even (down)
272            (2048.5, 0x6800),    // ties-to-even (down)
273            (1.0009766, 0x3C01), // smallest >1 f16 step, round up
274        ];
275        for &(x, want) in cases {
276            let got = f32_to_f16(x);
277            assert_eq!(
278                got,
279                want,
280                "f32_to_f16({x}) = {got:#06X}, want {want:#06X} (half: {:#06X})",
281                half::f16::from_f32(x).to_bits()
282            );
283            assert_eq!(got, half::f16::from_f32(x).to_bits());
284        }
285    }
286
287    #[test]
288    fn test_f32_to_f16_special_values_rne() {
289        assert_eq!(f32_to_f16(0.0), 0x0000);
290        assert_eq!(f32_to_f16(-0.0), 0x8000);
291        assert_eq!(f32_to_f16(f32::INFINITY), 0x7C00);
292        assert_eq!(f32_to_f16(f32::NEG_INFINITY), 0xFC00);
293        // NaN: exponent all ones, mantissa non-zero (quiet bit set).
294        let nan = f32_to_f16(f32::NAN);
295        assert_eq!(nan & 0x7C00, 0x7C00);
296        assert_ne!(nan & 0x03FF, 0);
297        // Overflow rounds to ±Inf, matching half.
298        assert_eq!(f32_to_f16(1.0e30), 0x7C00);
299        assert_eq!(f32_to_f16(-1.0e30), 0xFC00);
300    }
301
302    #[test]
303    fn test_f32_to_f16_ties_to_even() {
304        // Exact midpoints between two representable f16 values must round to the
305        // value with an even LSB (round-half-UP would round all of these up).
306        // 2049.0 sits exactly halfway between 2048 and 2050 in f16 spacing (step 2).
307        for &(x, want_even_down) in &[(2048.0f32, true), (2050.0f32, true)] {
308            let got = f32_to_f16(x);
309            assert_eq!(got, half::f16::from_f32(x).to_bits());
310            let _ = want_even_down;
311        }
312        // Smallest subnormal tie: 2^-25 is exactly halfway to the smallest
313        // f16 subnormal (2^-24); ties to even → 0. Just above → 0x0001.
314        let half_subnormal = f32::from_bits(0x3300_0000); // 2^-25
315        assert_eq!(f32_to_f16(half_subnormal), 0x0000);
316        assert_eq!(f32_to_f16(half_subnormal), half::f16::from_f32(half_subnormal).to_bits());
317        let just_above = f32::from_bits(0x3300_0001);
318        assert_eq!(f32_to_f16(just_above), 0x0001);
319        assert_eq!(f32_to_f16(just_above), half::f16::from_f32(just_above).to_bits());
320    }
321
322    #[test]
323    fn test_f32_to_f16_subnormals_rne() {
324        // f16 subnormal range: smallest 2^-24 = 0x0001, largest 0x03FF.
325        let smallest = f32::from_bits(0x3380_0000); // 2^-24
326        assert_eq!(f32_to_f16(smallest), 0x0001);
327        assert_eq!(f32_to_f16(smallest), half::f16::from_f32(smallest).to_bits());
328        // f32 subnormals are far below f16 range → flush to ±0.
329        assert_eq!(f32_to_f16(f32::from_bits(0x0000_0001)), 0x0000);
330        assert_eq!(f32_to_f16(f32::from_bits(0x8000_0001)), 0x8000);
331    }
332
333    /// Exhaustive-by-stride grid across the entire f32 domain (all 256 exponents,
334    /// strided mantissas, both signs). Must be bit-identical to half::f16. This is
335    /// the falsifier: RED on the round-half-UP version (thousands of divergences),
336    /// GREEN on the RNE fix. NaN bit-patterns are compared as "both NaN".
337    #[test]
338    fn test_f32_to_f16_matches_half_across_grid_rne() {
339        let mut diverged = 0u64;
340        for e in 0u32..=255 {
341            for m in (0u32..(1 << 23)).step_by(4093) {
342                for s in [0u32, 1u32] {
343                    let bits = (s << 31) | (e << 23) | m;
344                    let x = f32::from_bits(bits);
345                    let ours = f32_to_f16(x);
346                    let theirs = half::f16::from_f32(x).to_bits();
347                    if ours != theirs {
348                        let both_nan = (ours & 0x7C00) == 0x7C00
349                            && (ours & 0x03FF) != 0
350                            && (theirs & 0x7C00) == 0x7C00
351                            && (theirs & 0x03FF) != 0;
352                        if !both_nan {
353                            diverged += 1;
354                        }
355                    }
356                }
357            }
358        }
359        assert_eq!(diverged, 0, "f32_to_f16 diverged from half::f16 in {diverged} cases");
360    }
361
362    #[test]
363    fn test_silu_zero() {
364        assert!((silu_scalar(0.0)).abs() < 1e-7);
365    }
366
367    #[test]
368    fn test_silu_positive() {
369        // SiLU(x) → x for large positive x
370        let x = 10.0;
371        assert!((silu_scalar(x) - x).abs() < 0.01);
372    }
373
374    #[test]
375    fn test_silu_negative() {
376        // SiLU(x) → 0 for large negative x
377        assert!(silu_scalar(-10.0).abs() < 0.01);
378    }
379
380    #[test]
381    fn test_gelu_zero() {
382        assert!((gelu_scalar(0.0)).abs() < 1e-7);
383    }
384
385    #[test]
386    fn test_gelu_positive() {
387        let x = 10.0;
388        assert!((gelu_scalar(x) - x).abs() < 0.01);
389    }
390
391    #[test]
392    fn test_sigmoid_zero() {
393        assert!((sigmoid_scalar(0.0) - 0.5).abs() < 1e-7);
394    }
395
396    #[test]
397    fn test_sigmoid_symmetry() {
398        let x = 2.5;
399        assert!((sigmoid_scalar(x) + sigmoid_scalar(-x) - 1.0).abs() < 1e-6);
400    }
401
402    #[test]
403    fn test_relu_positive() {
404        assert!((relu_scalar(3.0) - 3.0).abs() < 1e-7);
405    }
406
407    #[test]
408    fn test_relu_negative() {
409        assert!((relu_scalar(-3.0)).abs() < 1e-7);
410    }
411
412    #[test]
413    fn test_tanh_zero() {
414        assert!((tanh_scalar(0.0)).abs() < 1e-7);
415    }
416
417    #[test]
418    fn test_tanh_odd() {
419        let x = 1.5;
420        assert!((tanh_scalar(x) + tanh_scalar(-x)).abs() < 1e-6);
421    }
422
423    #[test]
424    fn test_f16_roundtrip() {
425        let val = 1.5_f32;
426        let bits = f32_to_f16(val);
427        let back = f16_to_f32(bits);
428        assert!((val - back).abs() < 1e-3);
429    }
430
431    #[test]
432    fn test_f16_zero() {
433        assert_eq!(f16_to_f32(0), 0.0);
434    }
435
436    // =========================================================================
437    // FALSIFY-GE: gelu-kernel-v1.yaml contract (trueno gelu_scalar)
438    //
439    // Five-Whys (PMAT-354):
440    //   Why 1: trueno had basic gelu tests but zero FALSIFY-GE-* tests
441    //   Why 2: tests checked 2 values (zero, large), not mathematical invariants
442    //   Why 3: no mapping from gelu-kernel-v1.yaml to trueno test names
443    //   Why 4: trueno predates the provable-contracts YAML convention
444    //   Why 5: GELU was "obviously correct" (tanh approximation is textbook)
445    //
446    // References:
447    //   - provable-contracts/contracts/gelu-kernel-v1.yaml
448    //   - Hendrycks & Gimpel (2016) "Gaussian Error Linear Units (GELUs)"
449    // =========================================================================
450
451    /// FALSIFY-GE-001: Non-negativity — GELU(x) >= 0 for all x > 0
452    #[test]
453    fn falsify_ge_001_non_negativity() {
454        let test_values = [0.001, 0.01, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0, 100.0, 1e6];
455        for &x in &test_values {
456            let y = gelu_scalar(x);
457            assert!(y >= 0.0, "FALSIFIED GE-001: GELU({x}) = {y} < 0 for positive input");
458        }
459    }
460
461    /// FALSIFY-GE-002: Monotonicity — GELU(x) > GELU(y) when x > y > 0
462    #[test]
463    fn falsify_ge_002_positive_monotonicity() {
464        let values: Vec<f32> = vec![0.01, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0];
465        for window in values.windows(2) {
466            let (y_lo, y_hi) = (gelu_scalar(window[0]), gelu_scalar(window[1]));
467            assert!(
468                y_hi > y_lo,
469                "FALSIFIED GE-002: GELU({}) = {} not > GELU({}) = {}",
470                window[1],
471                y_hi,
472                window[0],
473                y_lo
474            );
475        }
476    }
477
478    /// FALSIFY-GE-003: Zero preservation — GELU(0) = 0
479    #[test]
480    fn falsify_ge_003_zero_preservation() {
481        let y = gelu_scalar(0.0);
482        assert!(y.abs() < 1e-7, "FALSIFIED GE-003: GELU(0) = {y}, expected 0");
483    }
484
485    /// FALSIFY-GE-005: Tanh approximation vs exact CDF — |diff| < 0.005
486    ///
487    /// Exact GELU: x * Phi(x) where Phi is the standard normal CDF.
488    /// We approximate Phi via Abramowitz & Stegun erf formula (max error 1.5e-7).
489    #[test]
490    fn falsify_ge_005_tanh_approx_accuracy() {
491        // Abramowitz & Stegun erf approximation (7.1.26), max |error| < 1.5e-7
492        fn erf_approx(x: f32) -> f32 {
493            let sign = x.signum();
494            let x = x.abs();
495            let t = 1.0 / (1.0 + 0.327_591_1 * x);
496            let t2 = t * t;
497            let t3 = t2 * t;
498            let t4 = t3 * t;
499            let t5 = t4 * t;
500            let poly = 0.254_829_592 * t - 0.284_496_736 * t2 + 1.421_413_741 * t3
501                - 1.453_152_027 * t4
502                + 1.061_405_429 * t5;
503            sign * (1.0 - poly * (-x * x).exp())
504        }
505
506        fn gelu_exact(x: f32) -> f32 {
507            let phi = 0.5 * (1.0 + erf_approx(x / std::f32::consts::SQRT_2));
508            x * phi
509        }
510
511        let test_values: Vec<f32> = (-100..=100).map(|i| i as f32 * 0.1).collect();
512        for &x in &test_values {
513            let approx = gelu_scalar(x);
514            let exact = gelu_exact(x);
515            let diff = (approx - exact).abs();
516            assert!(
517                diff < 0.005,
518                "FALSIFIED GE-005: |GELU_approx({x}) - GELU_exact({x})| = {diff} >= 0.005"
519            );
520        }
521    }
522
523    /// FALSIFY-GE-006: Large input stability — GELU(x) ≈ x for large x, ≈ 0 for large -x
524    #[test]
525    fn falsify_ge_006_large_input_stability() {
526        for &x in &[10.0_f32, 50.0, 100.0, 1000.0] {
527            let y = gelu_scalar(x);
528            assert!((y - x).abs() < 0.01, "FALSIFIED GE-006: GELU({x}) = {y}, expected ≈ {x}");
529        }
530        for &x in &[-10.0_f32, -50.0, -100.0, -1000.0] {
531            let y = gelu_scalar(x);
532            assert!(y.abs() < 0.01, "FALSIFIED GE-006: GELU({x}) = {y}, expected ≈ 0");
533        }
534    }
535
536    mod ge_proptest_falsify {
537        use super::*;
538        use proptest::prelude::*;
539
540        // GE-001-prop: non-negativity for positive x
541        proptest! {
542            #![proptest_config(ProptestConfig::with_cases(500))]
543            #[test]
544            fn falsify_ge_001_prop_non_negativity(x in 0.0_f32..1000.0) {
545                let y = gelu_scalar(x);
546                prop_assert!(y >= 0.0, "FALSIFIED GE-001-prop: gelu({x}) = {y} < 0");
547            }
548        }
549
550        // GE-002-prop: monotonicity for positive pairs
551        proptest! {
552            #![proptest_config(ProptestConfig::with_cases(300))]
553            #[test]
554            fn falsify_ge_002_prop_monotonic_positive(
555                a in 0.001_f32..100.0,
556                b in 0.001_f32..100.0,
557            ) {
558                if a != b {
559                    let (lo, hi) = if a < b { (a, b) } else { (b, a) };
560                    let y_lo = gelu_scalar(lo);
561                    let y_hi = gelu_scalar(hi);
562                    prop_assert!(
563                        y_hi > y_lo,
564                        "FALSIFIED GE-002-prop: gelu({hi})={y_hi} not > gelu({lo})={y_lo}"
565                    );
566                }
567            }
568        }
569
570        // GE-006-prop: large input stability
571        proptest! {
572            #![proptest_config(ProptestConfig::with_cases(200))]
573            #[test]
574            fn falsify_ge_006_prop_large_positive(x in 10.0_f32..500.0) {
575                let y = gelu_scalar(x);
576                prop_assert!(
577                    (y - x).abs() < 0.01,
578                    "FALSIFIED GE-006-prop: |gelu({x}) - {x}| = {}",
579                    (y - x).abs()
580                );
581            }
582        }
583    }
584}
585
586// =========================================================================
587// FALSIFY-SI: silu-kernel-v1.yaml contract (trueno silu_scalar)
588//
589// Five-Whys (PMAT-354, Phase 11):
590//   Why 1: trueno had basic silu unit tests but zero FALSIFY-SI-* tests
591//   Why 2: unit tests verify point values, not mathematical invariants
592//   Why 3: no mapping from silu-kernel-v1.yaml to trueno test names
593//   Why 4: trueno predates the provable-contracts YAML convention
594//   Why 5: SiLU was "obviously correct" (x * sigmoid(x))
595//
596// References:
597//   - provable-contracts/contracts/silu-kernel-v1.yaml
598//   - Ramachandran et al. (2017) "Searching for Activation Functions"
599// =========================================================================
600
601#[cfg(test)]
602mod silu_contract_tests {
603    use super::*;
604
605    /// FALSIFY-SI-001: Zero preservation — SiLU(0) = 0
606    #[test]
607    fn falsify_si_001_zero_preservation() {
608        let y = silu_scalar(0.0);
609        assert!(y.abs() < 1e-7, "FALSIFIED SI-001: SiLU(0) = {y}, expected 0");
610    }
611
612    /// FALSIFY-SI-002: Global lower bound — SiLU(x) > -0.279 for all x
613    #[test]
614    fn falsify_si_002_global_lower_bound() {
615        let test_values: Vec<f32> =
616            vec![-100.0, -50.0, -10.0, -5.0, -2.0, -1.278, -1.0, -0.5, 0.0, 0.5, 1.0, 5.0, 100.0];
617        for &x in &test_values {
618            let y = silu_scalar(x);
619            assert!(y > -0.28, "FALSIFIED SI-002: SiLU({x}) = {y}, expected > -0.279");
620        }
621    }
622
623    /// FALSIFY-SI-003: Monotonic for positive inputs — x > y > 0 ⟹ SiLU(x) > SiLU(y)
624    #[test]
625    fn falsify_si_003_monotonic_positive() {
626        let values: Vec<f32> = vec![0.01, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0, 100.0];
627        for i in 1..values.len() {
628            let y_prev = silu_scalar(values[i - 1]);
629            let y_curr = silu_scalar(values[i]);
630            assert!(
631                y_curr > y_prev,
632                "FALSIFIED SI-003: SiLU({}) = {y_curr} not > SiLU({}) = {y_prev}",
633                values[i],
634                values[i - 1]
635            );
636        }
637    }
638
639    /// FALSIFY-SI-005: Asymptotic linearity — |SiLU(x) - x| < 0.01 for x > 10
640    #[test]
641    fn falsify_si_005_asymptotic_linearity() {
642        for &x in &[10.0f32, 20.0, 50.0, 100.0, 500.0] {
643            let y = silu_scalar(x);
644            assert!(
645                (y - x).abs() < 0.01,
646                "FALSIFIED SI-005: |SiLU({x}) - {x}| = {} >= 0.01",
647                (y - x).abs()
648            );
649        }
650    }
651
652    /// FALSIFY-SI-006: Large negative → 0 — |SiLU(x)| < 0.01 for x < -10
653    #[test]
654    fn falsify_si_006_large_negative_vanishes() {
655        for &x in &[-10.0f32, -20.0, -50.0, -100.0, -500.0] {
656            let y = silu_scalar(x);
657            assert!(y.abs() < 0.01, "FALSIFIED SI-006: SiLU({x}) = {y}, expected ≈ 0");
658        }
659    }
660
661    mod si_proptest_falsify {
662        use super::*;
663        use proptest::prelude::*;
664
665        // SI-002-prop: global lower bound
666        proptest! {
667            #![proptest_config(ProptestConfig::with_cases(500))]
668            #[test]
669            fn falsify_si_002_prop_lower_bound(x in -1000.0_f32..1000.0) {
670                let y = silu_scalar(x);
671                prop_assert!(
672                    y > -0.28,
673                    "FALSIFIED SI-002-prop: SiLU({x}) = {y} <= -0.279"
674                );
675            }
676        }
677
678        // SI-003-prop: monotonic for positive pairs
679        proptest! {
680            #![proptest_config(ProptestConfig::with_cases(300))]
681            #[test]
682            fn falsify_si_003_prop_monotonic_positive(
683                a in 0.001_f32..100.0,
684                b in 0.001_f32..100.0,
685            ) {
686                if a != b {
687                    let (lo, hi) = if a < b { (a, b) } else { (b, a) };
688                    let y_lo = silu_scalar(lo);
689                    let y_hi = silu_scalar(hi);
690                    prop_assert!(
691                        y_hi > y_lo,
692                        "FALSIFIED SI-003-prop: SiLU({hi})={y_hi} not > SiLU({lo})={y_lo}"
693                    );
694                }
695            }
696        }
697
698        // SI-005-prop: asymptotic linearity for large positive x
699        proptest! {
700            #![proptest_config(ProptestConfig::with_cases(200))]
701            #[test]
702            fn falsify_si_005_prop_asymptotic(x in 10.0_f32..500.0) {
703                let y = silu_scalar(x);
704                prop_assert!(
705                    (y - x).abs() < 0.01,
706                    "FALSIFIED SI-005-prop: |SiLU({x}) - {x}| = {}",
707                    (y - x).abs()
708                );
709            }
710        }
711    }
712}