use crate::balancing::{self, BalancingCombine};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sample {
pub force_residual: f64,
pub xi_residual: f64,
}
impl Sample {
#[inline]
#[must_use]
pub fn residual_norm(&self, strategy: BalancingCombine) -> Option<f64> {
balancing::combine_channels(self.force_residual, self.xi_residual, strategy)
}
}
pub const DEFAULT_COMBINE: BalancingCombine = BalancingCombine::SumOfSquares;
pub fn residual_stream(samples: &[Sample], out: &mut [f64]) -> usize {
debug_assert!(samples.len() <= 1_000_000, "sample slice unreasonably large");
debug_assert!(matches!(DEFAULT_COMBINE, BalancingCombine::SumOfSquares), "default combine pinned by paper §5");
residual_stream_with(samples, out, DEFAULT_COMBINE)
}
pub fn residual_stream_with(
samples: &[Sample],
out: &mut [f64],
strategy: BalancingCombine,
) -> usize {
debug_assert!(samples.len() <= 1_000_000, "sample slice unreasonably large");
let n = samples.len().min(out.len());
debug_assert!(n <= out.len() && n <= samples.len(), "n bounded by both buffers");
let mut i = 0_usize;
while i < n {
out[i] = samples[i].residual_norm(strategy).unwrap_or(0.0);
i += 1;
}
debug_assert_eq!(i, n, "loop must run exactly n iterations");
n
}
pub fn fixture_residuals(out: &mut [f64]) -> usize {
debug_assert!(!out.is_empty(), "fixture buffer must be non-empty");
residual_stream(&FIXTURE, out)
}
pub const FIXTURE: [Sample; 6] = [
Sample { force_residual: 0.5, xi_residual: 0.01 },
Sample { force_residual: 0.8, xi_residual: 0.02 },
Sample { force_residual: 0.1, xi_residual: 0.05 },
Sample { force_residual: 0.1, xi_residual: 0.08 },
Sample { force_residual: 3.0, xi_residual: 0.10 },
Sample { force_residual: 0.6, xi_residual: 0.02 },
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_sample_is_zero_residual() {
let s = Sample { force_residual: 0.0, xi_residual: 0.0 };
assert!(s.residual_norm(DEFAULT_COMBINE).expect("finite").abs() < 1e-12);
}
#[test]
fn euclidean_combination_is_norm_of_both() {
let s = Sample { force_residual: 3.0, xi_residual: 4.0 };
let r = s.residual_norm(BalancingCombine::SumOfSquares).expect("finite");
assert!((r - 5.0).abs() < 1e-12);
}
#[test]
fn weighted_combine_suppresses_channel_on_request() {
let s = Sample { force_residual: 2.0, xi_residual: 100.0 };
let r = s
.residual_norm(BalancingCombine::WeightedSum { w_force: 1.0, w_xi: 0.0 })
.expect("finite");
assert!((r - 2.0).abs() < 1e-12);
}
#[test]
fn fixture_touchdown_has_highest_residual() {
let mut out = [0.0_f64; 6];
let n = residual_stream(&FIXTURE, &mut out);
assert_eq!(n, 6);
let touchdown = out[4];
for (i, r) in out[..n].iter().enumerate() {
if i != 4 {
assert!(touchdown > *r, "touchdown (idx=4) must be the largest residual, got out={out:?}");
}
}
}
}