//! Bucket routing: Controller A (warmup-completion detector) + Phase 1→2
//! transition orchestration (dispatches the 5 device kernels in
//! `bucket_transition_kernels.cu` in order).
//!
//! Per spec §3.1 and §2.3. All-on-device per
//! `feedback_no_htod_htoh_only_mapped_pinned`: no bulk DtoH, only single-scalar
//! mapped-pinned reads for the host-side trigger decision. Static-constant
//! uploads at transition time are off the hot path (one-time-init pattern,
//! allowed per the same feedback).
//!
//! Memory-rule anchors:
//!   * `pearl_first_observation_bootstrap` — sentinel = 0, first observation
//!     replaces directly; no waiting period.
//!   * `pearl_wiener_alpha_floor_for_nonstationary` — α floor = 0.4 in
//!     co-adapting closed loops where the target drifts. Controller A's
//!     signal (`CfC.tau`) co-adapts with the trunk during Phase 1, so the
//!     floor applies. Full Wiener-α (`α = diff_var/(diff_var+sample_var+ε)`)
//!     becomes available once we have multi-step diff_var/sample_var
//!     statistics; for the warmup-window scaffolding the constant 0.4 floor
//!     is the correct lower bound.
//!   * `pearl_blend_formulas_must_have_permanent_floor` — trigger threshold
//!     uses `max(noise_floor/4, noise_floor/16)`, not a blend.
//!   * `pearl_controller_anchors_isv_driven` + `feedback_isv_for_adaptive_bounds`
//!     — hard cap derives from observed dispersion (MAD/median over the first
//!     100 steps), not a tuned constant.
//!   * `pearl_canary_input_freshness_launch_order` — `execute_transition`
//!     dispatches the 5 kernels in the fixed order
//!     `tau_sort → bucket_assign → bucket_iqr → channels_in_bucket → heads_compact`;
//!     each kernel reads outputs of its predecessor on the same stream.
//!   * `pearl_scoped_init_seed_for_reproducibility` — any caller-side
//!     reallocation of `tau_all_d` or `heads_w_skip_compact_d` must wrap the
//!     realloc in `scoped_init_seed(cfg.seed)`; this module operates on
//!     pre-allocated device buffers handed in by the trainer.

use anyhow::{Context, Result};
use cudarc::driver::{CudaSlice, CudaStream};
use std::sync::Arc;
use crate::trainer::raw_launch::{RawArgs, raw_launch};

use crate::heads::N_HORIZONS;

/// CfC hidden dimension. Spec §2.2: matches `crates/ml-alpha/src/heads.rs:20`.
pub const HIDDEN_DIM: usize = 128;

/// Maximum bucket dim — used for the uniform per-branch CUDA block shape.
/// Post-2026-05-22 horizon-rebase to N_HORIZONS=3: with 128 hidden channels
/// distributed across 3 buckets, the initial uniform allocation is
/// `[43, 43, 42]`; the runtime ISV-driven τ-assignment may concentrate up to
/// ~75% of channels in one bucket under extreme τ distributions, so
/// MAX_BUCKET_DIM is sized for that upper bound.
pub const MAX_BUCKET_DIM: usize = 96;

/// Sentinel value written into unused slots of `channels_in_bucket` (slots
/// beyond `bucket_dim_k[k]` for buckets shorter than `MAX_BUCKET_DIM`).
/// Per-branch kernels treat values ≥ `HIDDEN_DIM` as "no work" and skip;
/// the sentinel is only ever read if the kernel ignores `bucket_dim_k`,
/// so it's belt-and-suspenders against future indexing bugs.
pub const CHANNELS_IN_BUCKET_SENTINEL: u32 = u32::MAX;

/// Initial per-bucket channel counts. Sum = HIDDEN_DIM = 128. Used as the
/// uniform-allocation hint before Phase 1→2 transition; the actual runtime
/// `bucket_dim_k_d` may differ once the τ-assignment kernel writes ISV-driven
/// counts at transition (each channel assigned to nearest-τ bucket). Last
/// bucket absorbs the HIDDEN_DIM-not-divisible-by-3 remainder.
pub const BUCKET_DIM_K: [u32; N_HORIZONS] = [43, 43, 42];

/// Cumulative-sum offsets for the initial `BUCKET_DIM_K`. Last entry =
/// HIDDEN_DIM for end-cap. Recomputed at runtime to match the post-transition
/// `bucket_dim_k_d` values.
pub const BUCKET_CHANNEL_OFFSET: [u32; N_HORIZONS + 1] = [0, 43, 86, 128];

const BUCKET_TRANSITION_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/bucket_transition_kernels.cubin"));

/// Controller A state — warmup-completion detector. Per spec §3.1, tracks:
///   * first-observation `noise_floor` (host-local f32, set at t=1 from the
///     mapped-pinned scalar read on step 1);
///   * Wiener-α EMA of per-step `‖τ_t − τ_{t-1}‖_F²`;
///   * `step_count` for safety-cap arming;
///   * `hard_cap_steps` derived from observed dispersion in the first 100
///     steps (`half_life × (1 + MAD/median)`);
///   * `transitioned` latches once Phase 2 has been triggered.
///
/// The host's role is read-only per `feedback_cpu_is_read_only`: signals are
/// computed on the device by `tau_change_frobenius_kernel` (added by Task 9);
/// the host reads a single mapped-pinned scalar per step and calls
/// [`ControllerA::update`].
pub struct ControllerA {
    /// First-observation noise floor (set at t=1 directly per
    /// `pearl_first_observation_bootstrap`).
    pub noise_floor: f32,
    /// Wiener-α EMA of `tau_change` with non-stationary floor (α = 0.4 per
    /// `pearl_wiener_alpha_floor_for_nonstationary`).
    pub tau_change_ema: f32,
    /// Step counter since Phase 1 start (1-indexed after the first
    /// [`update`] call).
    pub step_count: u64,
    /// ISV-derived hard cap on Phase 1 length. `None` until computed at the
    /// end of step 100; after that, set to
    /// `half_life × (1 + MAD/median)` per the derivation in spec §3.1.
    pub hard_cap_steps: Option<u64>,
    /// Whether Phase 2 has already been triggered. Latches at first trigger
    /// to make [`update`] idempotent across follow-on calls.
    pub transitioned: bool,
    /// Per-step `tau_change` observations for the first 100 steps. Used at
    /// step 100 to derive `hard_cap_steps`; not retained beyond that for
    /// memory hygiene.
    pub first100_tau_changes: Vec<f32>,
}

impl Default for ControllerA {
    fn default() -> Self {
        Self::new()
    }
}

impl ControllerA {
    pub fn new() -> Self {
        Self {
            noise_floor: 0.0,
            tau_change_ema: 0.0,
            step_count: 0,
            hard_cap_steps: None,
            transitioned: false,
            first100_tau_changes: Vec::with_capacity(100),
        }
    }

    /// Update per-step. Returns `true` iff the Phase 1→2 transition should
    /// trigger on THIS call (latching is owned by [`Self::transitioned`], so
    /// subsequent calls return `false` even though the flag stays set).
    ///
    /// Trigger condition (spec §3.1):
    ///   * `tau_change_ema < max(noise_floor/4, noise_floor/16)` — the
    ///     standard SNR-margin pattern; the 4× factor matches the SP16
    ///     controller idiom for "signal stabilized" and the 4² safety floor
    ///     prevents a degenerate near-zero `noise_floor` from collapsing the
    ///     threshold to 0.
    ///   * OR the ISV-derived hard cap fires (overridable for diagnostics
    ///     via `bucket_warmup_cap_override`).
    pub fn update(&mut self, tau_change: f32, bucket_warmup_cap_override: Option<u64>) -> bool {
        self.step_count += 1;

        // First-observation bootstrap — sentinel = 0, replace directly per
        // `pearl_first_observation_bootstrap`. No waiting period; the very
        // first `tau_change` IS the noise floor.
        if self.step_count == 1 {
            // Clamp at 1e-12 so subsequent threshold division never blows up
            // on a literal-zero first observation (degenerate but possible
            // if the model is loaded from a frozen-tau checkpoint).
            self.noise_floor = tau_change.max(1e-12);
            self.tau_change_ema = tau_change;
            self.first100_tau_changes.push(tau_change);
            return false;
        }

        // Wiener-α EMA. Full formula α = diff_var/(diff_var+sample_var+ε)
        // requires multi-step variance accumulators; for the Phase-1
        // warmup-detection scaffolding the constant floor 0.4 IS the lower
        // bound that `pearl_wiener_alpha_floor_for_nonstationary` mandates
        // for co-adapting closed loops, so it's the correct value here.
        // When Task 9 adds the full variance tracking, this becomes
        // `max(diff_var/(diff_var+sample_var+ε), 0.4)`.
        let alpha = 0.4f32;
        self.tau_change_ema = (1.0 - alpha) * self.tau_change_ema + alpha * tau_change;

        if self.step_count <= 100 {
            self.first100_tau_changes.push(tau_change);
        }

        // At end of step 100, compute the ISV-derived hard cap from observed
        // dispersion. half_life = first step at which `tau_change` crossed
        // below noise_floor/2; if no crossing within 100 steps, default to
        // 100 (matches the "first-obs bootstrap if no crossing" guidance in
        // spec §3.1).
        if self.step_count == 100 && self.hard_cap_steps.is_none() {
            let mut sorted = self.first100_tau_changes.clone();
            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
            let median = sorted[sorted.len() / 2];
            let mad: f32 = sorted
                .iter()
                .map(|x| (x - median).abs())
                .sum::<f32>()
                / sorted.len() as f32;
            let half_life = self
                .first100_tau_changes
                .iter()
                .position(|&x| x < self.noise_floor / 2.0)
                .map(|p| p as u64)
                .unwrap_or(100);
            // dispersion_factor = 1 + MAD/median replaces a hardcoded 1.5×
            // safety multiplier with a signal-derived factor per
            // `feedback_isv_for_adaptive_bounds`. median.max(1e-12) keeps the
            // division safe under a degenerate all-zero observation window.
            let dispersion_factor = 1.0 + mad / median.max(1e-12);
            self.hard_cap_steps = Some((half_life as f32 * dispersion_factor).ceil() as u64);
        }

        // CLI override takes precedence over the ISV-derived cap (diagnostic
        // path; production should leave this as `None`).
        let effective_cap = bucket_warmup_cap_override.or(self.hard_cap_steps);

        // max-with-floor per `pearl_blend_formulas_must_have_permanent_floor`:
        // never blend, always max. The /16 floor is 4² below `noise_floor`
        // and prevents a collapse-to-zero trigger condition if noise_floor
        // itself starts at the 1e-12 clamp.
        let threshold = (self.noise_floor / 4.0).max(self.noise_floor / 16.0);
        let signal_triggered = self.tau_change_ema < threshold;
        let cap_triggered = effective_cap.is_some_and(|c| self.step_count >= c);

        if (signal_triggered || cap_triggered) && !self.transitioned {
            self.transitioned = true;
            true
        } else {
            false
        }
    }
}

/// Bucket-routing metadata produced by the Phase 1→2 transition.
///
/// All fields are device-resident; the host never reads them in bulk per
/// `feedback_cpu_is_read_only`. The trainer holds a single instance of this
/// struct from `execute_transition` onward and passes the slices into the
/// Phase 2 forward/backward kernels.
///
/// `bucket_tau_iqr_lo_d` and `bucket_tau_iqr_hi_d` carry the RAW per-bucket
/// Q1/Q3 from `bucket_iqr_kernel`. The slack-factor widening
/// `slack_factor_k = sqrt(Q3_k / Q1_k)` (spec §3.2) is applied by the trainer
/// in Task 9 via a separate kernel; this module returns the unmodified
/// quartiles so the widening kernel can be wired in atomically with its
/// Controller B consumer.
///
/// ALPHA fix (2026-05-21): adds `channels_in_bucket_d`, the bucket →
/// original-channel lookup table. Per-branch kernels now read this table
/// instead of indexing into a reordered τ layout — see
/// `cfc_step_per_branch.cu` and `h_mag_per_bucket_kernel`.
pub struct BucketRoutingMetadata {
    /// Inverse map: channel index → bucket id. `[HIDDEN_DIM]`.
    pub bucket_id_per_channel_d: CudaSlice<u8>,
    /// Bucket starting offsets in compact ragged storage. `[N_HORIZONS + 1]`,
    /// last entry = HIDDEN_DIM for end-cap. Retained for downstream
    /// kernels (e.g. `heads_compact_kernel`) that still consume compact
    /// ragged layout; per-branch CfC kernels now use `channels_in_bucket_d`.
    pub bucket_channel_offset_d: CudaSlice<u32>,
    /// Per-bucket channel count. `[N_HORIZONS]`, sum = HIDDEN_DIM.
    pub bucket_dim_k_d: CudaSlice<u32>,
    /// Bucket → original-channel lookup. `[N_HORIZONS × MAX_BUCKET_DIM]`,
    /// row-major. `channels_in_bucket[k][i]` = original channel index of
    /// the i-th channel assigned to bucket k. Unused slots beyond
    /// `bucket_dim_k[k]` carry `CHANNELS_IN_BUCKET_SENTINEL`
    /// (`u32::MAX`). Populated by `channels_in_bucket_kernel`.
    pub channels_in_bucket_d: CudaSlice<u32>,
    /// Heads compact-ragged offsets. Same shape and values as
    /// `bucket_channel_offset_d` (both index into compact storage); held as
    /// a separate slice so the heads kernel can read it independently.
    pub heads_w_skip_offset_d: CudaSlice<u32>,
    /// Per-bucket τ Q1 (raw, not yet widened by slack_factor). `[N_HORIZONS]`.
    pub bucket_tau_iqr_lo_d: CudaSlice<f32>,
    /// Per-bucket τ Q3 (raw, not yet widened by slack_factor). `[N_HORIZONS]`.
    pub bucket_tau_iqr_hi_d: CudaSlice<f32>,
}

/// Dispatch the 5 transition kernels in order on the given stream.
///
/// Per spec §2.3 and `pearl_canary_input_freshness_launch_order`:
///   1. `tau_sort_kernel` writes `sorted_indices_d`
///   2. `bucket_assign_kernel` reads `sorted_indices_d`, writes
///      `bucket_id_per_channel_d`
///   3. `bucket_iqr_kernel` reads `sorted_indices_d` + `cfc_tau_d`, writes
///      raw Q1/Q3 per bucket
///   4. `channels_in_bucket_kernel` reads `bucket_id_per_channel_d` +
///      `bucket_dim_k_d`, writes `channels_in_bucket_d` (ALPHA fix
///      2026-05-21: replaces `tau_reorder_kernel` — τ stays in original
///      channel layout, the lookup table maps bucket → original channel)
///   5. `heads_compact_kernel` reads `bucket_id_per_channel_d` +
///      `bucket_channel_offset_d` + `heads_w_skip_orig_d`, writes
///      `heads_w_skip_compact_d`
///
/// All launches are on the SAME stream, so their producer/consumer ordering
/// is enforced by the stream's implicit serialization — no explicit
/// synchronization needed between launches.
///
/// `heads_w_skip_compact_d` is pre-allocated by the caller (the trainer)
/// per `pearl_scoped_init_seed_for_reproducibility`; this function only
/// writes into it. The `cfc_tau_d` parameter is passed for the Q1/Q3
/// IQR computation. The caller's `tau_all_d` (CfC τ) is NOT modified —
/// the ALPHA fix keeps τ in original channel layout; the previous
/// reorder is replaced by the `channels_in_bucket_d` lookup table.
/// `BUCKET_CHANNEL_OFFSET` and `BUCKET_DIM_K` are uploaded ONCE here from
/// host-side const arrays — this is a one-time-init pattern (called once
/// per training run between epochs, not on the hot path), permitted under
/// `feedback_no_htod_htoh_only_mapped_pinned`.
pub fn execute_transition(
    stream: &Arc<CudaStream>,
    cfc_tau_d: &CudaSlice<f32>,
    heads_w_skip_orig_d: &CudaSlice<f32>,
    heads_w_skip_compact_d: &mut CudaSlice<f32>,
) -> Result<BucketRoutingMetadata> {
    let ctx = stream.context();
    let module = ctx
        .load_cubin(BUCKET_TRANSITION_CUBIN.to_vec())
        .context("load bucket_transition cubin")?;

    let tau_sort_fn = module
        .load_function("tau_sort_kernel")
        .context("load tau_sort_kernel")?;
    let bucket_assign_fn = module
        .load_function("bucket_assign_kernel")
        .context("load bucket_assign_kernel")?;
    let bucket_iqr_fn = module
        .load_function("bucket_iqr_kernel")
        .context("load bucket_iqr_kernel")?;
    let channels_in_bucket_fn = module
        .load_function("channels_in_bucket_kernel")
        .context("load channels_in_bucket_kernel")?;
    let heads_compact_fn = module
        .load_function("heads_compact_kernel")
        .context("load heads_compact_kernel")?;

    // Allocate metadata + scratch buffers. `sorted_indices_d` is scratch — it
    // feeds bucket_assign and bucket_iqr but is not part of the returned
    // metadata.
    let bucket_id_per_channel_d = stream
        .alloc_zeros::<u8>(HIDDEN_DIM)
        .context("alloc bucket_id_per_channel_d")?;
    let sorted_indices_d = stream
        .alloc_zeros::<u32>(HIDDEN_DIM)
        .context("alloc sorted_indices_d (scratch)")?;
    let mut bucket_channel_offset_d = stream
        .alloc_zeros::<u32>(N_HORIZONS + 1)
        .context("alloc bucket_channel_offset_d")?;
    let mut bucket_dim_k_d = stream
        .alloc_zeros::<u32>(N_HORIZONS)
        .context("alloc bucket_dim_k_d")?;
    let channels_in_bucket_d = stream
        .alloc_zeros::<u32>(N_HORIZONS * MAX_BUCKET_DIM)
        .context("alloc channels_in_bucket_d")?;
    let mut heads_w_skip_offset_d = stream
        .alloc_zeros::<u32>(N_HORIZONS + 1)
        .context("alloc heads_w_skip_offset_d")?;
    let bucket_tau_iqr_lo_d = stream
        .alloc_zeros::<f32>(N_HORIZONS)
        .context("alloc bucket_tau_iqr_lo_d")?;
    let bucket_tau_iqr_hi_d = stream
        .alloc_zeros::<f32>(N_HORIZONS)
        .context("alloc bucket_tau_iqr_hi_d")?;

    // One-time static-constant upload. Spec §2.3 step 6 lists this as an
    // accepted implementation (the alternative would be __constant__ memory
    // baked into the cubin). It is NOT on the hot path: `execute_transition`
    // runs once per training run, between epochs, outside any captured CUDA
    // graph; therefore this HtoD is permitted under
    // `feedback_no_htod_htoh_only_mapped_pinned` (which forbids HtoD only
    // on the hot path / inside captured graphs).
    stream
        .memcpy_htod(&BUCKET_CHANNEL_OFFSET, &mut bucket_channel_offset_d)
        .context("htod BUCKET_CHANNEL_OFFSET")?;
    stream
        .memcpy_htod(&BUCKET_DIM_K, &mut bucket_dim_k_d)
        .context("htod BUCKET_DIM_K")?;
    // heads_w_skip_offset_d shares the same compact-position indexing as
    // bucket_channel_offset_d; uploaded separately so the heads forward
    // kernel can read it without aliasing.
    stream
        .memcpy_htod(&BUCKET_CHANNEL_OFFSET, &mut heads_w_skip_offset_d)
        .context("htod heads_w_skip_offset (=BUCKET_CHANNEL_OFFSET)")?;

    let rs = stream.cu_stream();

    // Kernel 1: tau_sort_kernel — bitonic-merge sort of τ ascending.
    // Single block × 32 threads, 4 passes for HIDDEN_DIM=128. Shared mem:
    // HIDDEN_DIM × 8 bytes (4 for key, 4 for value).
    {
        let mut args = RawArgs::new();
        args.push_ptr(cfc_tau_d.raw_ptr());
        args.push_ptr(sorted_indices_d.raw_ptr());
        let mut ptrs = args.build_arg_ptrs();
        unsafe {
            raw_launch(
                tau_sort_fn.cu_function(), (1,1,1), (32,1,1),
                HIDDEN_DIM as u32 * 8, rs, &mut ptrs[..args.len()],
            ).map_err(|e| anyhow::anyhow!("tau_sort_kernel: {:?}", e))?;
        }
    }

    // Kernel 2: bucket_assign_kernel — write bucket id per channel via the
    // static quintile boundaries `[0, 25, 50, 75, 100, 128]`.
    {
        let mut args = RawArgs::new();
        args.push_ptr(sorted_indices_d.raw_ptr());
        args.push_ptr(bucket_id_per_channel_d.raw_ptr());
        let mut ptrs = args.build_arg_ptrs();
        unsafe {
            raw_launch(
                bucket_assign_fn.cu_function(), (1,1,1), (HIDDEN_DIM as u32,1,1),
                0, rs, &mut ptrs[..args.len()],
            ).map_err(|e| anyhow::anyhow!("bucket_assign_kernel: {:?}", e))?;
        }
    }

    // Kernel 3: bucket_iqr_kernel — per-bucket Q1/Q3. N_HORIZONS blocks × 32
    // threads (one warp per bucket).
    {
        let mut args = RawArgs::new();
        args.push_ptr(cfc_tau_d.raw_ptr());
        args.push_ptr(sorted_indices_d.raw_ptr());
        args.push_ptr(bucket_tau_iqr_lo_d.raw_ptr());
        args.push_ptr(bucket_tau_iqr_hi_d.raw_ptr());
        let mut ptrs = args.build_arg_ptrs();
        unsafe {
            raw_launch(
                bucket_iqr_fn.cu_function(), (N_HORIZONS as u32,1,1), (32,1,1),
                32 * 4, rs, &mut ptrs[..args.len()],
            ).map_err(|e| anyhow::anyhow!("bucket_iqr_kernel: {:?}", e))?;
        }
    }

    // Kernel 4: channels_in_bucket_kernel — build the [N_HORIZONS ×
    // MAX_BUCKET_DIM] lookup table from `bucket_id_per_channel`. ALPHA
    // fix (2026-05-21): replaces the old `tau_reorder_kernel`; τ stays
    // in original channel layout; downstream per-branch kernels read
    // this lookup to find the original channel of each (bucket, tid).
    // Single block × HIDDEN_DIM threads; the sentinel-fill loop covers
    // 140 slots (N_HORIZONS × MAX_BUCKET_DIM), and the cursor lives in
    // shared memory across the per-channel sequential write loop.
    {
        let mut args = RawArgs::new();
        args.push_ptr(bucket_id_per_channel_d.raw_ptr());
        args.push_ptr(bucket_dim_k_d.raw_ptr());
        args.push_ptr(channels_in_bucket_d.raw_ptr());
        let mut ptrs = args.build_arg_ptrs();
        unsafe {
            raw_launch(
                channels_in_bucket_fn.cu_function(), (1,1,1), (HIDDEN_DIM as u32,1,1),
                0, rs, &mut ptrs[..args.len()],
            ).map_err(|e| anyhow::anyhow!("channels_in_bucket_kernel: {:?}", e))?;
        }
    }

    // Kernel 5: heads_compact_kernel — reorder heads_w_skip into compact
    // ragged layout. Single block × HIDDEN_DIM threads; shared mem holds
    // per-bucket cursors (`N_HORIZONS + 1` u32 = 24 bytes).
    {
        let mut args = RawArgs::new();
        args.push_ptr(heads_w_skip_orig_d.raw_ptr());
        args.push_ptr(bucket_id_per_channel_d.raw_ptr());
        args.push_ptr(bucket_channel_offset_d.raw_ptr());
        args.push_ptr(heads_w_skip_compact_d.raw_ptr());
        let mut ptrs = args.build_arg_ptrs();
        unsafe {
            raw_launch(
                heads_compact_fn.cu_function(), (1,1,1), (HIDDEN_DIM as u32,1,1),
                ((N_HORIZONS + 1) * 4) as u32, rs, &mut ptrs[..args.len()],
            ).map_err(|e| anyhow::anyhow!("heads_compact_kernel: {:?}", e))?;
        }
    }

    // Task 9 applies the slack_factor = sqrt(Q3/Q1) widening (spec §3.2) via
    // a separate kernel that mutates `bucket_tau_iqr_lo_d`/`bucket_tau_iqr_hi_d`
    // in place. Returning the raw Q1/Q3 here keeps the widening atomic with
    // Controller B's post-Adam-projection consumer.

    Ok(BucketRoutingMetadata {
        bucket_id_per_channel_d,
        bucket_channel_offset_d,
        bucket_dim_k_d,
        channels_in_bucket_d,
        heads_w_skip_offset_d,
        bucket_tau_iqr_lo_d,
        bucket_tau_iqr_hi_d,
    })
}
