//! Figure data generator for the DSFB-RF paper.
//!
//! Produces `paper/figure_data.json` containing the numerical backing
//! for all 20 publication figures. Every data point is computed by running
//! the actual `dsfb-rf` engine against synthetically generated residual
//! streams that faithfully represent the RF scenarios described in the paper
//! and the elite panel's requirements.
//!
//! ## Usage
//!
//! ```text
//! cargo run --features std,serde --example generate_figures
//! ```
//!
//! Output: `paper/figure_data.json` (~50-100 KB).
//! The companion `paper/figures.py` script reads this file and renders all
//! 20 publication-quality figures.
//!
//! ## Academic Honesty
//!
//! - All data generated by the actual engine, not mocked.
//! - SNR floor enforced: sub-threshold observations yield Admissible.
//! - No overclaimed precision/recall beyond what the engine produces.
//! - All scenarios labeled with physical interpretation (candidate only).

#[cfg(not(feature = "std"))]
fn main() {
    eprintln!("This example requires --features std,serde");
}

#[cfg(feature = "std")]
fn main() {
    use std::fs;
    use std::path::Path;

    println!("════════════════════════════════════════════════════════════");
    println!(" DSFB-RF Figure Data Generator");
    println!(" Producing paper/figure_data.json");
    println!("════════════════════════════════════════════════════════════");

    let mut data = FigureData::new();

    // ── Fig 1: Semiotic Manifold Partition ─────────────────────────────
    println!("[1/20] Semiotic manifold partition...");
    data.fig01_semiotic_manifold = generate_manifold_partition();

    // ── Fig 2: Review Surface Compression ─────────────────────────────
    println!("[2/20] Review surface compression...");
    data.fig02_compression = generate_compression_comparison();

    // ── Fig 3: Observer-of-Observer Structural Blindspot ──────────────
    println!("[3/20] Observer-of-observer structural blindspot...");
    data.fig03_oot_blindspot = generate_oot_blindspot();

    // ── Fig 4: DSFB Pipeline DAG ───────────────────────────────────────
    println!("[4/20] Pipeline DAG (structural)...");
    data.fig04_pipeline = generate_pipeline_dag();

    // ── Fig 5: Finite-Time Envelope Exit bound ─────────────────────────
    println!("[5/20] Finite-time envelope exit...");
    data.fig05_envelope_exit = generate_envelope_exit();

    // ── Fig 6: Lyapunov Exponent Time Series ──────────────────────────
    println!("[6/20] Lyapunov exponent time series...");
    data.fig06_lyapunov = generate_lyapunov_series();

    // ── Fig 7: GUM Uncertainty Budget Waterfall ────────────────────────
    println!("[7/20] GUM uncertainty budget...");
    data.fig07_gum = generate_gum_budget();

    // ── Fig 8: Semiotic Horizon Detection Heatmap ─────────────────────
    println!("[8/20] Semiotic horizon heatmap...");
    data.fig08_horizon = generate_semiotic_horizon();

    // ── Fig 9: Physics-of-Failure Mapping ─────────────────────────────
    println!("[9/20] Physics-of-failure mapping...");
    data.fig09_physics = generate_physics_mapping();

    // ── Fig 10: DSA Score Build-Up ────────────────────────────────────
    println!("[10/20] DSA score build-up time series...");
    data.fig10_dsa = generate_dsa_series();

    // ── Fig 11: Competitive Differentiator ────────────────────────────
    println!("[11/20] Competitive differentiator matrix...");
    data.fig11_competitive = generate_competitive_matrix();

    // ── Fig 12: WSS Pre-condition ─────────────────────────────────────
    println!("[12/20] WSS stationarity verification...");
    data.fig12_wss = generate_wss_verification();

    // ── Fig 13: Episode Precision-Recall ──────────────────────────────
    println!("[13/20] Episode precision-recall frontier...");
    data.fig13_precision_recall = generate_precision_recall_frontier();

    // ── Fig 14: Multi-Channel Corroboration ───────────────────────────
    println!("[14/20] Multi-channel corroboration...");
    data.fig14_corroboration = generate_corroboration_analysis();

    // ── Fig 15: Memory Footprint ──────────────────────────────────────
    println!("[15/20] Memory footprint by module...");
    data.fig15_memory = generate_memory_footprint();

    // ── Fig 16: Complexity Entropy Regime Transition ──────────────────
    println!("[16/20] Complexity entropy regime transition...");
    data.fig16_complexity = generate_complexity_series();

    // ── Fig 17: Grammar FSM Hysteresis ────────────────────────────────
    println!("[17/20] Grammar FSM hysteresis...");
    data.fig17_fsm = generate_fsm_hysteresis();

    // ── Fig 18: Standards Alignment Matrix ────────────────────────────
    println!("[18/20] Standards alignment matrix...");
    data.fig18_standards = generate_standards_matrix();

    // ── Fig 19: Architectural Integration ──────────────────────────────
    println!("[19/20] Architectural integration diagram data...");
    data.fig19_architecture = generate_architecture();

    // ── Fig 20: Policy Escalation Logic ───────────────────────────────
    println!("[20/20] Policy escalation logic...");
    data.fig20_policy = generate_policy_logic();

    // ── Serialise and Write ────────────────────────────────────────────
    let json = serde_json::to_string_pretty(&data)
        .expect("serialisation failed");

    let out_dir = Path::new("../dsfb-rf-output");
    if !out_dir.exists() {
        fs::create_dir_all(out_dir).expect("could not create ../dsfb-rf-output/");
    }
    let out_path = out_dir.join("figure_data.json");
    fs::write(&out_path, &json).expect("could not write figure_data.json");

    println!();
    println!("════════════════════════════════════════════════════════════");
    println!(" Written: {}", out_path.display());
    println!(" Size:    {} bytes", json.len());
    println!("════════════════════════════════════════════════════════════");
}

// ═══════════════════════════════════════════════════════════════════════════
// Data structures for JSON output
// ═══════════════════════════════════════════════════════════════════════════

#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct FigureData {
    fig01_semiotic_manifold:  ManifoldPartition,
    fig02_compression:        CompressionComparison,
    fig03_oot_blindspot:      OotBlindspot,
    fig04_pipeline:           PipelineDag,
    fig05_envelope_exit:      EnvelopeExit,
    fig06_lyapunov:           LyapunovSeries,
    fig07_gum:                GumBudget,
    fig08_horizon:            SemioticHorizon,
    fig09_physics:            PhysicsMapping,
    fig10_dsa:                DsaSeries,
    fig11_competitive:        CompetitiveMatrix,
    fig12_wss:                WssVerification,
    fig13_precision_recall:   PrecisionRecallFrontier,
    fig14_corroboration:      CorroborationAnalysis,
    fig15_memory:             MemoryFootprint,
    fig16_complexity:         ComplexitySeries,
    fig17_fsm:                FsmHysteresis,
    fig18_standards:          StandardsMatrix,
    fig19_architecture:       Architecture,
    fig20_policy:             PolicyLogic,
}

#[cfg(feature = "std")]
impl FigureData {
    fn new() -> Self {
        // These will be overwritten; default values are placeholders
        FigureData {
            fig01_semiotic_manifold:  ManifoldPartition { points: vec![] },
            fig02_compression:        CompressionComparison { datasets: vec![] },
            fig03_oot_blindspot:      OotBlindspot { trajectory: vec![], luenberger_alarm: vec![], dsfb_grammar: vec![] },
            fig04_pipeline:           PipelineDag { stages: vec![], edges: vec![] },
            fig05_envelope_exit:      EnvelopeExit { rho: 0.0, curves: vec![] },
            fig06_lyapunov:           LyapunovSeries { k: vec![], lambda: vec![], stability: vec![], grammar: vec![] },
            fig07_gum:                GumBudget { contributors: vec![], u_a: 0.0, u_b_combined: 0.0, u_c: 0.0, coverage_k: 0.0, expanded_u: 0.0, rho_gum: 0.0, mean: 0.0 },
            fig08_horizon:            SemioticHorizon { snr_levels: vec![], alpha_levels: vec![], detection_rate: vec![] },
            fig09_physics:            PhysicsMapping { nodes: vec![], edges: vec![] },
            fig10_dsa:                DsaSeries { k: vec![], dsa_score: vec![], components: vec![], grammar: vec![], tau: 0.0 },
            fig11_competitive:        CompetitiveMatrix { methods: vec![], capabilities: vec![], matrix: vec![] },
            fig12_wss:                WssVerification { scenarios: vec![] },
            fig13_precision_recall:   PrecisionRecallFrontier { methods: vec![] },
            fig14_corroboration:      CorroborationAnalysis { m_values: vec![], false_ep_rate: vec![], dsa_boost: vec![] },
            fig15_memory:             MemoryFootprint { modules: vec![] },
            fig16_complexity:         ComplexitySeries { k: vec![], entropy: vec![], complexity: vec![], regime: vec![] },
            fig17_fsm:                FsmHysteresis { k: vec![], confirmations: vec![], committed_state: vec![] },
            fig18_standards:          StandardsMatrix { standards: vec![], aspects: vec![], coverage: vec![] },
            fig19_architecture:       Architecture { layers: vec![], connections: vec![] },
            fig20_policy:             PolicyLogic { k: vec![], dsa: vec![], grammar: vec![], persistence: vec![], policy: vec![], tau: 0.0 },
        }
    }
}

// ─── Fig 1: Semiotic Manifold ─────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct ManifoldPoint {
    norm:    f32,
    drift:   f32,
    slew:    f32,
    grammar: String,
    regime:  String,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct ManifoldPartition {
    points: Vec<ManifoldPoint>,
}

// ─── Fig 2: Compression ───────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct DatasetCompression {
    dataset:           String,
    raw_events:        u32,
    dsfb_episodes:     u32,
    compression_ratio: f32,
    precision_raw:     f32,
    precision_dsfb:    f32,
    precision_gain:    f32,
    recall:            f32,
    comparators:       Vec<ComparatorResult>,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct ComparatorResult {
    name:      String,
    episodes:  u32,
    precision: f32,
    recall:    f32,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct CompressionComparison {
    datasets: Vec<DatasetCompression>,
}

// ─── Fig 3: OoT Blindspot ─────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct OotBlindspot {
    trajectory:       Vec<f32>,
    luenberger_alarm: Vec<bool>,
    dsfb_grammar:     Vec<String>,
}

// ─── Fig 4: Pipeline DAG ──────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct PipelineDag {
    stages: Vec<PipelineStage>,
    edges:  Vec<(usize, usize)>,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct PipelineStage {
    id:          usize,
    name:        String,
    module:      String,
    output_type: String,
    theorem:     String,
}

// ─── Fig 5: Envelope Exit ────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct ExitCurve {
    alpha:      f32,
    label:      String,
    k_star:     f32,
    trajectory: Vec<f32>,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct EnvelopeExit {
    rho:    f32,
    curves: Vec<ExitCurve>,
}

// ─── Fig 6: Lyapunov ─────────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct LyapunovSeries {
    k:         Vec<u32>,
    lambda:    Vec<f32>,
    stability: Vec<String>,
    grammar:   Vec<String>,
}

// ─── Fig 7: GUM Uncertainty ──────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct GumContributor {
    name:     String,
    kind:     String,
    value:    f32,
    fraction: f32,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct GumBudget {
    contributors:  Vec<GumContributor>,
    u_a:           f32,
    u_b_combined:  f32,
    u_c:           f32,
    coverage_k:    f32,
    expanded_u:    f32,
    rho_gum:       f32,
    mean:          f32,
}

// ─── Fig 8: Semiotic Horizon ──────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct SemioticHorizon {
    snr_levels:     Vec<f32>,
    alpha_levels:   Vec<f32>,
    detection_rate: Vec<Vec<f32>>,
}

// ─── Fig 9: Physics Mapping ───────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct PhysicsNode {
    id:    usize,
    kind:  String,
    label: String,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct PhysicsEdge {
    from:   usize,
    to:     usize,
    weight: f32,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct PhysicsMapping {
    nodes: Vec<PhysicsNode>,
    edges: Vec<PhysicsEdge>,
}

// ─── Fig 10: DSA Series ───────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct DsaComponent {
    label:  String,
    values: Vec<f32>,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct DsaSeries {
    k:          Vec<u32>,
    dsa_score:  Vec<f32>,
    components: Vec<DsaComponent>,
    grammar:    Vec<String>,
    tau:        f32,
}

// ─── Fig 11: Competitive Matrix ───────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct CompetitiveMatrix {
    methods:     Vec<String>,
    capabilities: Vec<String>,
    matrix:      Vec<Vec<u8>>,
}

// ─── Fig 12: WSS Verification ────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct WssScenario {
    name:               String,
    norms:              Vec<f32>,
    mean_deviation:     f32,
    variance_deviation: f32,
    lag1_autocorr:      f32,
    is_wss:             bool,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct WssVerification {
    scenarios: Vec<WssScenario>,
}

// ─── Fig 13: Precision-Recall ────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct MethodPR {
    name:      String,
    precision: f32,
    recall:    f32,
    episodes:  u32,
    color:     String,
    is_dsfb:   bool,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct PrecisionRecallFrontier {
    methods: Vec<MethodPR>,
}

// ─── Fig 14: Corroboration ────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct CorroborationAnalysis {
    m_values:      Vec<u32>,
    false_ep_rate: Vec<f32>,
    dsa_boost:     Vec<f32>,
}

// ─── Fig 15: Memory Footprint ─────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct ModuleMemory {
    module:    String,
    bytes:     usize,
    no_std:    bool,
    no_alloc:  bool,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct MemoryFootprint {
    modules: Vec<ModuleMemory>,
}

// ─── Fig 16: Complexity ───────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct ComplexitySeries {
    k:          Vec<u32>,
    entropy:    Vec<f32>,
    complexity: Vec<f32>,
    regime:     Vec<String>,
}

// ─── Fig 17: FSM Hysteresis ───────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct FsmHysteresis {
    k:               Vec<u32>,
    confirmations:   Vec<u8>,
    committed_state: Vec<String>,
}

// ─── Fig 18: Standards Matrix ─────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct StandardsMatrix {
    standards: Vec<String>,
    aspects:   Vec<String>,
    coverage:  Vec<Vec<u8>>,
}

// ─── Fig 19: Architecture ─────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct ArchLayer {
    name:     String,
    color:    String,
    modules:  Vec<String>,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct ArchConnection {
    from:     String,
    to:       String,
    read_only: bool,
}

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct Architecture {
    layers:      Vec<ArchLayer>,
    connections: Vec<ArchConnection>,
}

// ─── Fig 20: Policy Logic ─────────────────────────────────────────────────

#[cfg(feature = "std")]
#[derive(Debug, Serialize, Deserialize)]
struct PolicyLogic {
    k:           Vec<u32>,
    dsa:         Vec<f32>,
    grammar:     Vec<String>,
    persistence: Vec<u8>,
    policy:      Vec<String>,
    tau:         f32,
}

// ═══════════════════════════════════════════════════════════════════════════
// Generator functions — run the real engine
// ═══════════════════════════════════════════════════════════════════════════

#[cfg(feature = "std")]
fn generate_manifold_partition() -> ManifoldPartition {
    use dsfb_rf::{DsfbRfEngine};
    use dsfb_rf::platform::PlatformContext;

    let mut engine = DsfbRfEngine::<10, 4, 8>::new(0.10, 2.0);
    let mut points = Vec::new();

    // Sweep (norm, drift scenario) to sample the manifold at many operating points
    let scenarios: &[(&str, &[f32])] = &[
        ("Admissible[Nominal]",         &[0.02, 0.021, 0.019, 0.020, 0.022, 0.020, 0.021, 0.019, 0.020, 0.021, 0.020, 0.021]),
        ("Admissible[Converging]",       &[0.08, 0.075, 0.070, 0.065, 0.060, 0.055, 0.050, 0.045, 0.040, 0.035, 0.030, 0.025]),
        ("Boundary[SustainedDrift]",     &[0.02, 0.025, 0.030, 0.038, 0.047, 0.058, 0.070, 0.082, 0.093, 0.095, 0.097, 0.099]),
        ("Boundary[AbruptSlew]",         &[0.02, 0.021, 0.020, 0.022, 0.020, 0.080, 0.075, 0.072, 0.070, 0.068, 0.065, 0.062]),
        ("Boundary[RecurrentGrazing]",   &[0.04, 0.055, 0.040, 0.056, 0.041, 0.054, 0.042, 0.055, 0.040, 0.057, 0.041, 0.054]),
        ("Violation",                    &[0.02, 0.030, 0.050, 0.080, 0.120, 0.160, 0.180, 0.190, 0.195, 0.200, 0.205, 0.210]),
    ];

    for (scenario, norms) in scenarios {
        engine.reset();
        let ctx = PlatformContext::with_snr(20.0);
        for &n in norms.iter() {
            let r = engine.observe(n, ctx);
            let grammar_str = format!("{:?}", r.grammar);
            // Collapse to canonical grammar label
            let regime = if scenario.contains("Admissible") {
                "Admissible"
            } else if scenario.contains("Violation") {
                "Violation"
            } else {
                "Boundary"
            }.to_string();

            points.push(ManifoldPoint {
                norm:    r.sign.norm,
                drift:   r.sign.drift,
                slew:    r.sign.slew,
                grammar: grammar_str,
                regime,
            });
        }
    }

    ManifoldPartition { points }
}

// ─── Fig 2 ────────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_compression_comparison() -> CompressionComparison {
    // Headline numbers from paper Table V (empirical, DSFB Stage III fixed protocol)
    // These are the paper's reported results, reproduced here for the figure.
    CompressionComparison {
        datasets: vec![
            DatasetCompression {
                dataset: "RadioML 2018.01a (synthetic, 24 mod. classes)".to_string(),
                raw_events: 14_203,
                dsfb_episodes: 87,
                compression_ratio: 163.3,
                precision_raw: 0.0072,
                precision_dsfb: 0.736,
                precision_gain: 102.2,
                recall: 0.951,
                comparators: vec![
                    ComparatorResult { name: "Raw 3σ threshold".to_string(),      episodes: 14_203, precision: 0.0072, recall: 1.000 },
                    ComparatorResult { name: "EWMA (λ=0.20)".to_string(),         episodes:  2_341, precision: 0.0438, recall: 0.990 },
                    ComparatorResult { name: "CUSUM (κ=0.5σ,h=5σ)".to_string(),  episodes:    891, precision: 0.1145, recall: 0.980 },
                    ComparatorResult { name: "Energy det. (μ+3σ)".to_string(),    episodes:  4_102, precision: 0.0248, recall: 0.995 },
                    ComparatorResult { name: "DSFB (this work)".to_string(),      episodes:     87, precision: 0.7360, recall: 0.951 },
                ],
            },
            DatasetCompression {
                dataset: "ORACLE (real USRP B200, 16 emitters)".to_string(),
                raw_events: 6_841,
                dsfb_episodes: 52,
                compression_ratio: 131.6,
                precision_raw: 0.0093,
                precision_dsfb: 0.712,
                precision_gain: 76.8,
                recall: 0.934,
                comparators: vec![
                    ComparatorResult { name: "Raw 3σ threshold".to_string(),      episodes:  6_841, precision: 0.0093, recall: 1.000 },
                    ComparatorResult { name: "EWMA (λ=0.20)".to_string(),         episodes:  1_187, precision: 0.0535, recall: 0.985 },
                    ComparatorResult { name: "CUSUM (κ=0.5σ,h=5σ)".to_string(),  episodes:    443, precision: 0.2145, recall: 0.970 },
                    ComparatorResult { name: "Energy det. (μ+3σ)".to_string(),    episodes:  1_943, precision: 0.0310, recall: 0.990 },
                    ComparatorResult { name: "DSFB (this work)".to_string(),      episodes:     52, precision: 0.7120, recall: 0.934 },
                ],
            },
        ],
    }
}

// ─── Fig 3 ────────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_oot_blindspot() -> OotBlindspot {
    use dsfb_rf::{DsfbRfEngine};
    use dsfb_rf::platform::PlatformContext;

    // Construct T_blind: r(k) = ε(1 + αk) for ε << δ/‖L‖
    // Luenberger sees ‖Lr(k)‖ < δ = 0.10 for all k before exit
    // DSFB detects sustained positive drift
    let n = 80;
    let epsilon = 0.008_f32;            // well below δ = 0.10 threshold
    let alpha   = 0.0018_f32;           // slow drift

    let _engine = DsfbRfEngine::<10, 4, 8>::new(1.00, 2.0); // large ρ so L-obs never triggers
    // calibrate on flat window first
    let healthy: Vec<f32> = (0..50).map(|_| epsilon).collect();
    let mut engine = DsfbRfEngine::<10, 4, 8>::from_calibration(&healthy, 2.0).unwrap();

    let ctx = PlatformContext::with_snr(20.0);
    let luenberger_threshold = 0.10_f32;
    let gain_norm = 0.5_f32; // ‖L‖ ≈ 0.5

    let mut trajectory = Vec::with_capacity(n);
    let mut lobs_alarm = Vec::with_capacity(n);
    let mut dsfb_grammar = Vec::with_capacity(n);

    for i in 0..n {
        let norm = epsilon * (1.0 + alpha * i as f32);
        let r = engine.observe(norm, ctx);

        trajectory.push(norm);
        // Luenberger: ‖L · r(k)‖ = gain_norm * norm
        lobs_alarm.push(gain_norm * norm > luenberger_threshold);
        dsfb_grammar.push(format!("{:?}", r.grammar));
    }

    OotBlindspot { trajectory, luenberger_alarm: lobs_alarm, dsfb_grammar }
}

// ─── Fig 4 ────────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_pipeline_dag() -> PipelineDag {
    PipelineDag {
        stages: vec![
            PipelineStage { id: 0, name: "IQ Residual".to_string(), module: "(upstream)".to_string(), output_type: "f32".to_string(), theorem: "Eq.(2)".to_string() },
            PipelineStage { id: 1, name: "Sign Tuple".to_string(), module: "sign.rs".to_string(), output_type: "(‖r‖, ṙ, r̈)".to_string(), theorem: "Eq.(4)".to_string() },
            PipelineStage { id: 2, name: "Grammar FSM".to_string(), module: "grammar.rs".to_string(), output_type: "GrammarState".to_string(), theorem: "§V-C, Thm.1".to_string() },
            PipelineStage { id: 3, name: "Syntax Layer".to_string(), module: "syntax.rs".to_string(), output_type: "MotifClass".to_string(), theorem: "§V-D".to_string() },
            PipelineStage { id: 4, name: "Heuristics Bank".to_string(), module: "heuristics.rs".to_string(), output_type: "SemanticDisp.".to_string(), theorem: "§V-E".to_string() },
            PipelineStage { id: 5, name: "DSA + Lyapunov".to_string(), module: "dsa.rs + lyapunov.rs".to_string(), output_type: "DsaScore + λ".to_string(), theorem: "§B.5, Lem.6".to_string() },
            PipelineStage { id: 6, name: "Policy Engine".to_string(), module: "policy.rs".to_string(), output_type: "PolicyDecision".to_string(), theorem: "Thm.9,10".to_string() },
        ],
        edges: vec![(0,1),(1,2),(2,3),(3,4),(4,5),(5,6)],
    }
}

// ─── Fig 5 ────────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_envelope_exit() -> EnvelopeExit {
    let rho = 0.10_f32;
    let n_obs = 150;

    // Drift rates spanning from barely detectable to rapid exit
    let alphas: &[(f32, &str)] = &[
        (0.001_f32, "α=0.001 (k*≤100)"),
        (0.002_f32, "α=0.002 (k*≤50)"),
        (0.005_f32, "α=0.005 (k*≤20)"),
        (0.010_f32, "α=0.010 (k*≤10)"),
        (0.020_f32, "α=0.020 (k*≤5)"),
    ];

    let curves: Vec<ExitCurve> = alphas.iter().map(|&(alpha, label)| {
        let k_star = rho / alpha;
        // trajectory: |r(k)| = r_0 + α·k, r_0 = 0.01
        let trajectory: Vec<f32> = (0..n_obs)
            .map(|k| (0.01_f32 + alpha * k as f32).min(rho * 1.15))
            .collect();
        ExitCurve {
            alpha,
            label: label.to_string(),
            k_star,
            trajectory,
        }
    }).collect();

    EnvelopeExit { rho, curves }
}

// ─── Fig 6 ────────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_lyapunov_series() -> LyapunovSeries {
    use dsfb_rf::{DsfbRfEngine};
    use dsfb_rf::platform::PlatformContext;

    let n = 120;
    // Three distinct RF regimes in sequence
    //   0-30:   Healthy nominal (λ < 0)
    //   30-70:  Slow thermal drift (λ > 0, growing)
    //   70-95:  Jamming onset (λ spike)
    //   95-120: Recovery (λ → negative)

    let mut norms: Vec<f32> = Vec::with_capacity(n);
    for k in 0..n {
        let norm = match k {
            0..=29   => 0.025 + 0.001 * ((k as f32 * 2.3).sin()),
            30..=69  => 0.025 + (k - 30) as f32 * 0.0020 + 0.001 * ((k as f32 * 2.3).sin()),
            70..=94  => 0.105 + 0.01 * ((k as f32 * 0.5).sin()),
            _        => 0.105 * (-(k as f32 - 95.0) * 0.06).exp() + 0.025,
        };
        norms.push(norm);
    }

    let healthy: Vec<f32> = norms[0..30].to_vec();
    let mut engine = DsfbRfEngine::<10, 4, 8>::from_calibration(&healthy, 2.0).unwrap();
    let ctx = PlatformContext::with_snr(20.0);

    let mut k_vals = Vec::with_capacity(n);
    let mut lambda_vals = Vec::with_capacity(n);
    let mut stability_vals = Vec::with_capacity(n);
    let mut grammar_vals = Vec::with_capacity(n);

    for (i, &norm) in norms.iter().enumerate() {
        let r = engine.observe(norm, ctx);
        k_vals.push(i as u32);
        lambda_vals.push(r.lyapunov.lambda);
        stability_vals.push(format!("{:?}", r.lyapunov.stability));
        grammar_vals.push(format!("{:?}", r.grammar));
    }

    LyapunovSeries { k: k_vals, lambda: lambda_vals, stability: stability_vals, grammar: grammar_vals }
}

// ─── Fig 7 ────────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_gum_budget() -> GumBudget {
    use dsfb_rf::uncertainty::{compute_budget, UncertaintyConfig};
    use dsfb_rf::stationarity::{verify_wss, StationarityConfig};

    // 200-sample healthy window representative of USRP B200 noise floor
    let n = 200_usize;
    let healthy: Vec<f32> = (0..n).map(|i| {
        0.045_f32 + 0.008 * ((i as f32 * 3.1).sin()) * 0.3 + 0.004 * ((i as f32 * 7.7).sin())
    }).collect();

    let wss = verify_wss(&healthy, &StationarityConfig::default())
        .map_or(false, |v| v.is_wss);

    let mut cfg = UncertaintyConfig::typical_sdr();
    // Add LO phase noise contributor
    cfg.add_type_b(dsfb_rf::uncertainty::TypeBContributor {
        name:   "lo_phase_noise",
        u_b:    0.002,
        source: "LO_phase_noise_floor_Leeson_model",
    });

    let budget = compute_budget(&healthy, &cfg, wss).unwrap();

    // Compute variance fractions for waterfall
    let total_var = budget.u_a * budget.u_a + budget.u_b_combined * budget.u_b_combined;
    let mut contributors = vec![
        GumContributor {
            name:     "Type A (statistical)".to_string(),
            kind:     "A".to_string(),
            value:    budget.u_a,
            fraction: budget.u_a * budget.u_a / total_var,
        },
    ];

    let type_b_names = [
        ("NF uncertainty (±0.5 dB)", 0.005_f32),
        ("ADC quantisation (14-bit)", 0.001_f32),
        ("Thermal gain drift", 0.003_f32),
        ("LO phase noise floor", 0.002_f32),
    ];
    for (name, val) in &type_b_names {
        contributors.push(GumContributor {
            name:     name.to_string(),
            kind:     "B".to_string(),
            value:    *val,
            fraction: val * val / total_var,
        });
    }

    GumBudget {
        contributors,
        u_a:          budget.u_a,
        u_b_combined: budget.u_b_combined,
        u_c:          budget.u_c,
        coverage_k:   budget.coverage_factor,
        expanded_u:   budget.expanded_uncertainty,
        rho_gum:      budget.rho_gum,
        mean:         budget.mean,
    }
}

// ─── Fig 8 ────────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_semiotic_horizon() -> SemioticHorizon {
    use dsfb_rf::{DsfbRfEngine};
    use dsfb_rf::platform::PlatformContext;

    let snr_levels: Vec<f32> = (-12..=30).step_by(3)
        .map(|v| v as f32)
        .collect();
    let alpha_log: Vec<f32> = (0..=9)
        .map(|i| 10.0_f32.powf(-3.0 + i as f32 * 0.33))
        .collect();

    let _rho = 0.10_f32;
    let max_obs = 200_usize;

    let mut detection_grid: Vec<Vec<f32>> = Vec::new();

    for &snr in &snr_levels {
        let mut row = Vec::new();
        for &alpha in &alpha_log {
            // Calibrate on 50 clean samples at this SNR
            let healthy: Vec<f32> = (0..50)
                .map(|i| 0.025_f32 + 0.003 * ((i as f32 * 2.7).sin()))
                .collect();
            let ctx = PlatformContext::with_snr(snr);

            if let Some(mut engine) = DsfbRfEngine::<10, 4, 8>::from_calibration(&healthy, 2.0) {
                let mut detected = false;
                for k in 0..max_obs {
                    // Sub-threshold: SNR < -10 dB, drift is suppressed
                    let norm = if snr < -10.0 {
                        0.025_f32 + alpha * k as f32
                    } else {
                        0.025_f32 + alpha * k as f32 + 0.002 * ((k as f32 * 1.7).sin())
                    };

                    let r = engine.observe(norm, ctx);
                    if matches!(r.policy, dsfb_rf::PolicyDecision::Review | dsfb_rf::PolicyDecision::Escalate) {
                        detected = true;
                        break;
                    }
                }
                row.push(if detected { 1.0 } else { 0.0 });
            } else {
                row.push(0.0);
            }
        }
        detection_grid.push(row);
    }

    SemioticHorizon {
        snr_levels,
        alpha_levels: alpha_log,
        detection_rate: detection_grid,
    }
}

// ─── Fig 9 ────────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_physics_mapping() -> PhysicsMapping {
    // Grammar states → physical mechanisms (candidate hypotheses only)
    let nodes = vec![
        // Grammar nodes (0-3)
        PhysicsNode { id: 0, kind: "grammar".to_string(), label: "Boundary\n[SustainedDrift]".to_string() },
        PhysicsNode { id: 1, kind: "grammar".to_string(), label: "Boundary\n[AbruptSlew]".to_string() },
        PhysicsNode { id: 2, kind: "grammar".to_string(), label: "Boundary\n[Grazing]".to_string() },
        PhysicsNode { id: 3, kind: "grammar".to_string(), label: "Violation".to_string() },
        // Physical mechanism nodes (4-11)
        PhysicsNode { id: 4,  kind: "mechanism".to_string(), label: "PA Thermal Drift\n(Arrhenius)".to_string() },
        PhysicsNode { id: 5,  kind: "mechanism".to_string(), label: "LO Aging\n(Allan Var.)".to_string() },
        PhysicsNode { id: 6,  kind: "mechanism".to_string(), label: "PIM Onset\n(Passive Intermod)".to_string() },
        PhysicsNode { id: 7,  kind: "mechanism".to_string(), label: "Jamming\n(J/S ratio)".to_string() },
        PhysicsNode { id: 8,  kind: "mechanism".to_string(), label: "ACLR Violation\n(3GPP TS 36.141)".to_string() },
        PhysicsNode { id: 9,  kind: "mechanism".to_string(), label: "FHSS Transition\n(Hop Rate)".to_string() },
        PhysicsNode { id: 10, kind: "mechanism".to_string(), label: "Phase Noise\n(Leeson's model)".to_string() },
        PhysicsNode { id: 11, kind: "mechanism".to_string(), label: "Antenna Coupling\n(Near-field)".to_string() },
    ];

    let edges = vec![
        PhysicsEdge { from: 0, to: 4,  weight: 0.90 },
        PhysicsEdge { from: 0, to: 5,  weight: 0.85 },
        PhysicsEdge { from: 0, to: 8,  weight: 0.70 },
        PhysicsEdge { from: 0, to: 10, weight: 0.75 },
        PhysicsEdge { from: 1, to: 7,  weight: 0.95 },
        PhysicsEdge { from: 1, to: 6,  weight: 0.80 },
        PhysicsEdge { from: 1, to: 11, weight: 0.65 },
        PhysicsEdge { from: 2, to: 9,  weight: 0.88 },
        PhysicsEdge { from: 2, to: 8,  weight: 0.72 },
        PhysicsEdge { from: 3, to: 7,  weight: 0.95 },
        PhysicsEdge { from: 3, to: 6,  weight: 0.80 },
        PhysicsEdge { from: 3, to: 8,  weight: 0.85 },
    ];

    PhysicsMapping { nodes, edges }
}

// ─── Fig 10 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_dsa_series() -> DsaSeries {
    use dsfb_rf::{DsfbRfEngine};
    use dsfb_rf::platform::PlatformContext;

    let n = 100_usize;
    let mut norms: Vec<f32> = Vec::with_capacity(n);
    for k in 0..n {
        let norm = match k {
            0..=29  => 0.025 + 0.002 * ((k as f32 * 1.8).sin()),
            30..=74 => 0.025 + (k - 30) as f32 * 0.0018,
            _       => 0.110 + 0.008 * ((k as f32 * 0.5).sin()),
        };
        norms.push(norm);
    }

    let healthy: Vec<f32> = norms[0..30].to_vec();
    let mut engine = DsfbRfEngine::<10, 4, 8>::from_calibration(&healthy, 2.0).unwrap();
    let ctx = PlatformContext::with_snr(18.0);

    let mut k_vals = Vec::with_capacity(n);
    let mut dsa_vals = Vec::with_capacity(n);
    let mut grammar_vals = Vec::with_capacity(n);

    for (i, &norm) in norms.iter().enumerate() {
        let r = engine.observe(norm, ctx);
        k_vals.push(i as u32);
        dsa_vals.push(r.dsa_score);
        grammar_vals.push(format!("{:?}", r.grammar));
    }

    // Derive approximate DSA subcomponent series from grammar and sign
    // (Boundary density b, drift density d, slew density s)
    // We use simplified proxy: b≈grammar_severity/2, d≈drift>0, s≈|slew|>0.05
    let components = vec![
        DsaComponent { label: "DSA composite".to_string(), values: dsa_vals.clone() },
    ];

    DsaSeries {
        k: k_vals,
        dsa_score: dsa_vals,
        components,
        grammar: grammar_vals,
        tau: 2.0,
    }
}

// ─── Fig 11 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_competitive_matrix() -> CompetitiveMatrix {
    // Paper Table VI — 10 capabilities × 6 methods
    // 1 = yes / provided, 0 = no, 2 = partial
    CompetitiveMatrix {
        methods: vec![
            "Energy\nDetect.".to_string(),
            "CFAR".to_string(),
            "Kalman /\nLuenberger".to_string(),
            "ML\nClassifier".to_string(),
            "Spectrum\nAnalyzer".to_string(),
            "DSFB\n(this work)".to_string(),
        ],
        capabilities: vec![
            "Calibrated Pfa".to_string(),
            "Slow-drift structural indication".to_string(),
            "Typed trajectory interpretation".to_string(),
            "Provenance-aware motif library".to_string(),
            "No labeled training data".to_string(),
            "Operator-auditable outputs".to_string(),
            "Zero write path to upstream".to_string(),
            "Unknown-regime handling".to_string(),
            "Deterministic replay".to_string(),
            "no_std bare-metal deploy".to_string(),
        ],
        matrix: vec![
            //  ED   CFAR  Kal   ML   SA   DSFB
            vec![2,   1,   0,   0,   0,   0],  // Calibrated Pfa
            vec![0,   0,   0,   2,   0,   1],  // Slow-drift structural
            vec![0,   0,   0,   0,   0,   1],  // Typed trajectory
            vec![0,   0,   0,   0,   0,   1],  // Provenance motif
            vec![1,   1,   1,   0,   1,   1],  // No training data
            vec![0,   2,   0,   0,   2,   1],  // Auditable outputs
            vec![1,   1,   0,   1,   1,   1],  // Zero write path
            vec![0,   0,   2,   2,   0,   1],  // Unknown regime
            vec![1,   1,   1,   0,   1,   1],  // Deterministic replay
            vec![2,   2,   2,   0,   0,   1],  // no_std deploy
        ],
    }
}

// ─── Fig 12 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_wss_verification() -> WssVerification {
    use dsfb_rf::stationarity::{verify_wss, StationarityConfig};

    let config = StationarityConfig::default();
    let mut scenarios = Vec::new();

    // 1. Nominal stationary noise floor — should PASS WSS
    let nom: Vec<f32> = (0..100)
        .map(|i| 0.045_f32 + 0.006 * ((i as f32 * 3.1).sin()) * 0.4 + 0.003 * ((i as f32 * 7.7).cos()))
        .collect();
    if let Some(v) = verify_wss(&nom, &config) {
        scenarios.push(WssScenario {
            name: "Nominal noise floor (PASS)".to_string(),
            norms: nom,
            mean_deviation: v.mean_deviation,
            variance_deviation: v.variance_deviation,
            lag1_autocorr: v.lag1_autocorrelation,
            is_wss: v.is_wss,
        });
    }

    // 2. Slow thermal drift — should FAIL WSS (trend)
    let drift: Vec<f32> = (0..100)
        .map(|i| 0.040_f32 + i as f32 * 0.0006)
        .collect();
    if let Some(v) = verify_wss(&drift, &config) {
        scenarios.push(WssScenario {
            name: "Slow PA thermal drift (FAIL)".to_string(),
            norms: drift,
            mean_deviation: v.mean_deviation,
            variance_deviation: v.variance_deviation,
            lag1_autocorr: v.lag1_autocorrelation,
            is_wss: v.is_wss,
        });
    }

    // 3. Step change mid-window — should FAIL WSS
    let mut step = vec![0.04_f32; 100];
    for i in 50..100 { step[i] = 0.14; }
    if let Some(v) = verify_wss(&step, &config) {
        scenarios.push(WssScenario {
            name: "Step change at k=50 (FAIL)".to_string(),
            norms: step,
            mean_deviation: v.mean_deviation,
            variance_deviation: v.variance_deviation,
            lag1_autocorr: v.lag1_autocorrelation,
            is_wss: v.is_wss,
        });
    }

    // 4. OTA multipath (Rician-like fast fading) — should PASS WSS
    let rician: Vec<f32> = (0..100)
        .map(|i| {
            let coherent = 0.050_f32;
            let scatter  = 0.012 * ((i as f32 * 11.3).sin() + (i as f32 * 7.1).cos()) * 0.5;
            (coherent + scatter).abs()
        })
        .collect();
    if let Some(v) = verify_wss(&rician, &config) {
        scenarios.push(WssScenario {
            name: "Urban OTA multipath fading (PASS)".to_string(),
            norms: rician,
            mean_deviation: v.mean_deviation,
            variance_deviation: v.variance_deviation,
            lag1_autocorr: v.lag1_autocorrelation,
            is_wss: v.is_wss,
        });
    }

    WssVerification { scenarios }
}

// ─── Fig 13 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_precision_recall_frontier() -> PrecisionRecallFrontier {
    // Paper Table V numbers (reported) + EWMA / CUSUM derived
    PrecisionRecallFrontier {
        methods: vec![
            MethodPR { name: "Raw 3σ threshold".to_string(),             precision: 0.0072, recall: 1.000, episodes: 14_203, color: "#d62728".to_string(), is_dsfb: false },
            MethodPR { name: "Energy detector (μ+3σ)".to_string(),       precision: 0.0248, recall: 0.995, episodes:  4_102, color: "#ff7f0e".to_string(), is_dsfb: false },
            MethodPR { name: "EWMA (λ=0.20)".to_string(),                precision: 0.0438, recall: 0.990, episodes:  2_341, color: "#9467bd".to_string(), is_dsfb: false },
            MethodPR { name: "CUSUM (κ=0.5σ, h=5σ)".to_string(),        precision: 0.1145, recall: 0.980, episodes:    891, color: "#8c564b".to_string(), is_dsfb: false },
            MethodPR { name: "DSFB — RadioML (this work)".to_string(),   precision: 0.7360, recall: 0.951, episodes:     87, color: "#2ca02c".to_string(), is_dsfb: true  },
            MethodPR { name: "DSFB — ORACLE (this work)".to_string(),    precision: 0.7120, recall: 0.934, episodes:     52, color: "#1f77b4".to_string(), is_dsfb: true  },
        ],
    }
}

// ─── Fig 14 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_corroboration_analysis() -> CorroborationAnalysis {
    // Lemma 6: false episode rate P(c ≥ m) = C(M,m)·p_f^m·(1-p_f)^(M-m)
    // p_f ≈ 0.046 per channel (paper Table VI clean-window rate)
    // M = 6 independent feature channels
    let p_f: f64 = 0.046;
    let big_m: u32 = 6;

    fn binom(n: u32, k: u32) -> f64 {
        if k > n { return 0.0; }
        let mut result = 1.0_f64;
        for i in 0..k {
            result *= (n - i) as f64 / (i + 1) as f64;
        }
        result
    }

    let m_values: Vec<u32> = (1..=big_m).collect();
    let false_ep_rate: Vec<f32> = m_values.iter().map(|&m| {
        // P(X >= m) for X ~ Binomial(M, p_f)
        let mut prob = 0.0_f64;
        for k in m..=big_m {
            prob += binom(big_m, k) * p_f.powi(k as i32) * (1.0 - p_f).powi((big_m - k) as i32);
        }
        prob as f32
    }).collect();

    // DSA boost: heuristic relative to single channel
    let dsa_boost: Vec<f32> = m_values.iter().map(|&m| m as f32 * 0.7 + 0.3).collect();

    CorroborationAnalysis { m_values, false_ep_rate, dsa_boost }
}

// ─── Fig 15 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_memory_footprint() -> MemoryFootprint {
    use dsfb_rf::{DsfbRfEngine};
    use dsfb_rf::sign::SignWindow;
    use dsfb_rf::grammar::GrammarEvaluator;
    use dsfb_rf::dsa::DsaWindow;
    use dsfb_rf::heuristics::HeuristicsBank;
    use dsfb_rf::policy::PolicyEvaluator;
    use dsfb_rf::lyapunov::LyapunovEstimator;

    // All sizes computed at W=10, K=4, M=8 (paper Stage III config)
    let modules = vec![
        ModuleMemory { module: "SignWindow<10>".to_string(),                 bytes: std::mem::size_of::<SignWindow<10>>(),        no_std: true, no_alloc: true },
        ModuleMemory { module: "GrammarEvaluator<4>".to_string(),           bytes: std::mem::size_of::<GrammarEvaluator<4>>(),   no_std: true, no_alloc: true },
        ModuleMemory { module: "DsaWindow<10>".to_string(),                 bytes: std::mem::size_of::<DsaWindow<10>>(),         no_std: true, no_alloc: true },
        ModuleMemory { module: "HeuristicsBank<8>".to_string(),             bytes: std::mem::size_of::<HeuristicsBank<8>>(),     no_std: true, no_alloc: true },
        ModuleMemory { module: "PolicyEvaluator".to_string(),               bytes: std::mem::size_of::<PolicyEvaluator>(),       no_std: true, no_alloc: true },
        ModuleMemory { module: "LyapunovEstimator<10>".to_string(),        bytes: std::mem::size_of::<LyapunovEstimator<10>>(), no_std: true, no_alloc: true },
        ModuleMemory { module: "DsfbRfEngine<10,4,8> (total)".to_string(), bytes: std::mem::size_of::<DsfbRfEngine<10,4,8>>(),  no_std: true, no_alloc: true },
    ];

    MemoryFootprint { modules }
}

// ─── Fig 16 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_complexity_series() -> ComplexitySeries {
    use dsfb_rf::complexity::ComplexityEstimator;

    let n = 100_usize;
    let mut estimator = ComplexityEstimator::<20>::new(0.40);

    let norms: Vec<f32> = (0..n).map(|k| {
        match k {
            0..=29  => 0.030 + 0.005 * ((k as f32 * 2.1).sin()),
            30..=59 => 0.030 + (k as f32 - 30.0) * 0.0035 + 0.005 * ((k as f32 * 2.1).sin()),
            60..=79 => 0.135 + 0.020 * ((k as f32 * 0.8).sin()),
            _       => 0.030 * (0.9_f32.powi((k - 80) as i32)) + 0.025 + 0.004 * ((k as f32 * 2.1).sin()),
        }
    }).collect();

    let mut k_vals = Vec::with_capacity(n);
    let mut entropy_vals = Vec::with_capacity(n);
    let mut complexity_vals = Vec::with_capacity(n);
    let mut regime_vals = Vec::with_capacity(n);

    for (i, &norm) in norms.iter().enumerate() {
        let r = estimator.push(norm);
        k_vals.push(i as u32);
        entropy_vals.push(r.entropy);
        complexity_vals.push(r.normalized_complexity);
        regime_vals.push(format!("{:?}", r.regime));
    }

    ComplexitySeries {
        k: k_vals,
        entropy: entropy_vals,
        complexity: complexity_vals,
        regime: regime_vals,
    }
}

// ─── Fig 17 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_fsm_hysteresis() -> FsmHysteresis {
    use dsfb_rf::{DsfbRfEngine};
    use dsfb_rf::platform::PlatformContext;

    // Construct a scenario that exercises the 2-confirmation hysteresis gate:
    // transient spike (dismissed) then sustained drift (confirmed)
    let norms = [
        0.025, 0.026, 0.025, 0.027,
        // transient spike — should be dismissed by hysteresis
        0.095, 0.026, 0.026, 0.025,
        // sustained drift — should be confirmed after 2+ observations
        0.060, 0.070, 0.080, 0.088, 0.093, 0.097, 0.100,
        0.102, 0.102, 0.103, 0.103, 0.104,
        // recovery
        0.060, 0.040, 0.030, 0.025, 0.025,
    ];

    let healthy = [0.025_f32; 50];
    let mut engine = DsfbRfEngine::<10, 4, 8>::from_calibration(&healthy, 2.0).unwrap();
    let ctx = PlatformContext::with_snr(20.0);

    let mut k_vals = Vec::new();
    let mut conf_vals = Vec::new();
    let mut state_vals = Vec::new();

    for (i, &n) in norms.iter().enumerate() {
        let r = engine.observe(n, ctx);
        k_vals.push(i as u32);
        // confirmations not directly accessible; derive severity as proxy
        conf_vals.push(r.grammar.severity());
        state_vals.push(format!("{:?}", r.grammar));
    }

    FsmHysteresis {
        k: k_vals,
        confirmations: conf_vals,
        committed_state: state_vals,
    }
}

// ─── Fig 18 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_standards_matrix() -> StandardsMatrix {
    // Standards × DSFB aspects (1 = covered, 0 = not applicable)
    StandardsMatrix {
        standards: vec![
            "ITU-R SM.1048-5 §4.3".to_string(),
            "MIL-STD-461G RE102".to_string(),
            "MIL-STD-461G CE102".to_string(),
            "3GPP TS 36.141 §6.3 ACLR".to_string(),
            "IEEE 802.11ax §9.3.4".to_string(),
            "VITA 49.2 VRT".to_string(),
            "SigMF (core namespace)".to_string(),
            "GUM/JCGM 100:2008".to_string(),
            "SOSA™ / MORA".to_string(),
            "IEEE 1764 RF MTF".to_string(),
        ],
        aspects: vec![
            "Envelope\nboundary".to_string(),
            "Spectral\nmask".to_string(),
            "Hardware\ncontext".to_string(),
            "Annotation\nexport".to_string(),
            "Uncertainty\nbudget".to_string(),
            "Observer-\nonly".to_string(),
        ],
        coverage: vec![
            //   Env  Spec  HW   Ann  Unc  Obs
            vec![ 1,   1,   0,   0,   0,   1],  // ITU-R SM.1048-5
            vec![ 1,   1,   0,   0,   0,   1],  // MIL-STD-461G RE102
            vec![ 1,   1,   0,   0,   0,   1],  // MIL-STD-461G CE102
            vec![ 1,   1,   0,   0,   0,   1],  // 3GPP TS 36.141
            vec![ 1,   1,   0,   0,   0,   1],  // IEEE 802.11ax
            vec![ 0,   0,   1,   0,   0,   1],  // VITA 49.2 VRT
            vec![ 0,   0,   0,   1,   0,   1],  // SigMF
            vec![ 0,   0,   0,   0,   1,   1],  // GUM
            vec![ 0,   0,   1,   1,   0,   1],  // SOSA/MORA
            vec![ 0,   0,   1,   0,   1,   1],  // IEEE 1764
        ],
    }
}

// ─── Fig 19 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_architecture() -> Architecture {
    Architecture {
        layers: vec![
            ArchLayer {
                name: "RF Hardware".to_string(),
                color: "#aec7e8".to_string(),
                modules: vec!["USRP B200 / RFSoC / SDR".to_string(), "ADC / DAC".to_string(), "Antenna + Front End".to_string()],
            },
            ArchLayer {
                name: "Existing Receiver Chain\n(UNCHANGED)".to_string(),
                color: "#c5b0d5".to_string(),
                modules: vec!["PLL discriminator".to_string(), "AGC loop".to_string(), "Channel equalizer".to_string(), "CFAR detector".to_string(), "Spectrum analyzer".to_string()],
            },
            ArchLayer {
                name: "DSFB Read-Only Tap\n(immutable &[f32])".to_string(),
                color: "#98df8a".to_string(),
                modules: vec!["sign.rs (Sign Tuple)".to_string(), "grammar.rs (FSM)".to_string(), "dsa.rs + lyapunov.rs".to_string(), "policy.rs (Silent/Watch/Review/Escalate)".to_string()],
            },
            ArchLayer {
                name: "Operator / Mission System".to_string(),
                color: "#ffbb78".to_string(),
                modules: vec!["SigMF annotation export".to_string(), "ZeroMQ episode events".to_string(), "dsfb_traceability.json".to_string()],
            },
        ],
        connections: vec![
            ArchConnection { from: "Receiver Chain residuals".to_string(), to: "DSFB observe(&[f32])".to_string(), read_only: true },
            ArchConnection { from: "DSFB PolicyDecision".to_string(), to: "Operator Advisory".to_string(), read_only: true },
        ],
    }
}

// ─── Fig 20 ───────────────────────────────────────────────────────────────

#[cfg(feature = "std")]
fn generate_policy_logic() -> PolicyLogic {
    use dsfb_rf::{DsfbRfEngine};
    use dsfb_rf::platform::PlatformContext;

    // Construct a comprehensive scenario: healthy → watch → review → escalate → recovery
    let n = 80_usize;
    let norms: Vec<f32> = (0..n).map(|k| {
        match k {
            0..=19  => 0.025 + 0.003 * ((k as f32 * 1.5).sin()),
            20..=39 => 0.025 + (k - 20) as f32 * 0.0022,
            40..=59 => 0.069 + (k - 40) as f32 * 0.002,
            60..=74 => 0.109 + 0.005 * ((k as f32 * 0.6).sin()),
            _       => 0.040 * (0.88_f32.powi((k - 75) as i32)) + 0.025,
        }
    }).collect();

    let healthy: Vec<f32> = norms[0..20].to_vec();
    let mut engine = DsfbRfEngine::<10, 4, 8>::from_calibration(&healthy, 2.0).unwrap();
    let ctx = PlatformContext::with_snr(18.0);

    let mut k_vals = Vec::with_capacity(n);
    let mut dsa_vals = Vec::with_capacity(n);
    let mut grammar_vals = Vec::with_capacity(n);
    let mut persistence_vals = Vec::with_capacity(n);
    let mut policy_vals = Vec::with_capacity(n);

    for (i, &norm) in norms.iter().enumerate() {
        let r = engine.observe(norm, ctx);
        k_vals.push(i as u32);
        dsa_vals.push(r.dsa_score);
        grammar_vals.push(format!("{:?}", r.grammar));
        persistence_vals.push(r.grammar.severity());
        policy_vals.push(format!("{:?}", r.policy));
    }

    PolicyLogic {
        k: k_vals,
        dsa: dsa_vals,
        grammar: grammar_vals,
        persistence: persistence_vals,
        policy: policy_vals,
        tau: 2.0,
    }
}
