opendeviationbar-core 13.80.0

Core open deviation bar construction algorithm with temporal integrity guarantees
Documentation
//! Bandt–Pompe ordinal-pattern embedding constants (structural, integer-only).
//!
//! These centralize the embedding `(m = 3, τ = 1)` that is shared across the
//! Bandt–Pompe family of features — permutation entropy, weighted permutation
//! entropy, Fisher information, and SVD entropy — and is **already shipped in
//! production** via the hardcoded `ordinal_pattern_index_m3` path and the
//! ClickHouse columns `lookback_permutation_entropy` / `intra_permutation_entropy`.
//!
//! # Scope (locked — operator, 2026-06-04; updated 2026-07-21 for Issue #455)
//!
//! **Integers / structural sites only**, EXCEPT `LN_3_FACTORIAL` which is a float
//! normalizer. Originally excluded from centralization to avoid changing computed
//! values, but Issue #455 requires unification to fix a ~3e-13 typo drift between
//! two definitions. The float is now included here as the authoritative source.
//!
//! The two float normalizers that lived elsewhere in the codebase were:
//! - `interbar_math/tier3.rs:65`: `1.791_759_469_228_055` (correct)
//! - `intrabar/features.rs:103`: `1.791_759_469_228_327` (typo: last three digits differ)
//!
//! Centralizing only the integer/structural embedding (before this fix) was
//! provably value-neutral — `3usize` → [`ORDINAL_M`], `[u16; 6]` → `[u16; ORDINAL_M_FACTORIAL]`, and
//! `embed_dim == 3` → `embed_dim == ORDINAL_M` are the same integers, hence the
//! same output (proven bit-exact by the golden-snapshot gate).
//!
//! `m = 3, τ = 1` are compile-time constants with **no runtime / env / pydantic
//! knob**: a project-wide constant that can be tuned is not a constant. See
//! `docs/adr/2026-06-03-bandt-pompe-embedding-constant.md`.

/// Embedding dimension `m` for the Bandt–Pompe ordinal-pattern construction.
///
/// The production permutation-entropy path is specialized for `m = 3`
/// (`ordinal_pattern_index_m3`); this is the structural source of truth for that
/// dimension across the Bandt–Pompe feature family.
///
/// This is a **structural label, not a retargeting knob.** The shipped kernels
/// (`tier3::ordinal_pattern_index_m3`, the SIMD triplet unroll, the `n - 2`
/// triplet count, the `features.rs` `embed_dim == ORDINAL_M` decision tree and
/// its `MAX_ENTROPY_M3` normalizer) are *hardcoded* for `m = 3`. Changing this
/// value would NOT retarget those kernels — it would silently compute m=3
/// patterns under a mismatched buffer/denominator. The `ORDINAL_M == 3` invariant
/// below converts any such edit into a build failure.
pub const ORDINAL_M: usize = 3;

/// Embedding delay `τ` (gap, in samples, between embedded coordinates).
///
/// `τ = 1` uses consecutive samples.
///
/// NOTE: this constant is currently **unused in the production Rust path** — the
/// shipped permutation-entropy code (`ordinal_pattern_index_m3`) is specialized for
/// `m = 3, τ = 1` and reads three *consecutive* samples, so `τ = 1` is implicit and
/// there is no `τ` site to substitute. It is kept for completeness and as the
/// authoritative mirror of the Python eval SSoT (`entropy_candidates.ORDINAL_TAU`),
/// pinning the embedding's full `(m, τ)` definition in one place. (Surfaced by the
/// 2026-06-05 adversarial review; intentionally retained, not removed.)
pub const ORDINAL_TAU: usize = 1;

/// `m!` — the number of distinct ordinal patterns for embedding dimension
/// [`ORDINAL_M`] (`3! = 6`).
///
/// Used to size fixed-length ordinal-pattern count buffers, e.g.
/// `[u16; ORDINAL_M_FACTORIAL]`.
pub const ORDINAL_M_FACTORIAL: usize = 6;

/// Natural logarithm of 3! = ln(6)
///
/// Exact value: 1.791759469228055 (IEEE-754 f64 round-to-nearest of f64::ln(6.0))
/// Equivalent: std::f64::consts::LN_2 + std::f64::consts::LN_3
///
/// Used for Shannon entropy normalization in permutation entropy (Bandt-Pompe, M=3):
/// max_entropy = ln(3!) = ln(6), so normalized entropy = H / LN_3_FACTORIAL ∈ [0, 1]
///
/// Used by:
/// - Inter-bar: `lookback_permutation_entropy` (via interbar_math/tier3.rs)
/// - Intra-bar: `intra_permutation_entropy` (via intrabar/features.rs)
///
/// Issue #455: Unified from two definitions with ~3e-13 typo drift:
/// - `interbar_math/tier3.rs:65`: `1.791_759_469_228_055` (correct)
/// - `intrabar/features.rs:103`: `1.791_759_469_228_327` (typo)
pub const LN_3_FACTORIAL: f64 = 1.791_759_469_228_055;

// Compile-time invariant: ORDINAL_M_FACTORIAL must equal ORDINAL_M! exactly.
// Integer-only check — guards the structural relationship without touching any
// float. If ORDINAL_M ever changes, this fails the build until the buffer-sizing
// constant is updated to match.
const _: () = {
    let mut factorial = 1usize;
    let mut i = 2usize;
    while i <= ORDINAL_M {
        factorial *= i;
        i += 1;
    }
    assert!(
        factorial == ORDINAL_M_FACTORIAL,
        "ORDINAL_M_FACTORIAL must equal ORDINAL_M! (m factorial)"
    );
};

// Compile-time invariant: the shipped permutation-entropy kernels are HARDCODED
// for m = 3 (ordinal_pattern_index_m3, the SIMD triplet unroll, `n - 2`, the
// `embed_dim == ORDINAL_M` decision tree + MAX_ENTROPY_M3 normalizer). ORDINAL_M
// is the structural label for that dimension, not a knob that retargets them.
// Pinning it here turns an accidental retarget into a build error instead of
// silently-wrong output under a mismatched buffer/denominator.
const _: () = assert!(
    ORDINAL_M == 3,
    "production permutation-entropy kernels are hardcoded for m = 3; \
     retargeting m requires rewriting ordinal_pattern_index_m3 + the m=3 decision tree, \
     not just changing ORDINAL_M"
);

#[cfg(test)]
mod tests {
    use super::*;

    /// Pins the embedding constants to their ratified values. A hermetic, CI-runnable
    /// regression tripwire (needs no fixtures / `test-utils`): complements — does not
    /// replace — the golden-snapshot bit-exact gate. If any value changes, this trips
    /// here long before the 863K-trade golden is consulted.
    #[test]
    fn const_values_pinned() {
        assert_eq!(ORDINAL_M, 3, "Bandt–Pompe embedding dimension m");
        assert_eq!(ORDINAL_TAU, 1, "Bandt–Pompe embedding delay τ");
        assert_eq!(
            ORDINAL_M_FACTORIAL, 6,
            "m! (ordinal-pattern count / buffer size)"
        );
    }

    #[test]
    fn ln_3_factorial_precision() {
        // Verify the constant matches ln(6) to full f64 precision (Issue #455)
        let computed = 6.0_f64.ln();
        assert_eq!(LN_3_FACTORIAL, computed, "ln(3!) must equal ln(6)");
    }

    #[test]
    fn ln_3_factorial_via_components() {
        // Verify ln(3!) = ln(6) = ln(2) + ln(3)
        let computed = 2.0_f64.ln() + 3.0_f64.ln();
        assert_eq!(LN_3_FACTORIAL, computed, "ln(6) = ln(2) + ln(3)");
    }
}