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}