pub struct RankAdaptiveController {
pub tolerance: f64,
pub rank_budget: usize,
pub min_tolerance: f64,
pub max_tolerance: f64,
pub conservation_tight: f64,
pub conservation_loose: f64,
pub tighten_factor: f64,
pub loosen_factor: f64,
pub current_max_rank: usize,
pub budget_saturated: bool,
pub truncation_errors: Vec<f64>,
pub conservation_errors: Vec<(f64, f64, f64)>, }
impl RankAdaptiveController {
pub fn new(rank_budget: usize) -> Self {
Self {
tolerance: 1e-6,
rank_budget,
min_tolerance: 1e-12,
max_tolerance: 1e-2,
conservation_tight: 1e-6,
conservation_loose: 1e-8,
tighten_factor: 0.5,
loosen_factor: 2.0,
current_max_rank: 1,
budget_saturated: false,
truncation_errors: Vec::new(),
conservation_errors: Vec::new(),
}
}
pub fn with_thresholds(
rank_budget: usize,
initial_tolerance: f64,
conservation_tight: f64,
conservation_loose: f64,
) -> Self {
let mut ctrl = Self::new(rank_budget);
ctrl.tolerance = initial_tolerance;
ctrl.conservation_tight = conservation_tight;
ctrl.conservation_loose = conservation_loose;
ctrl
}
pub fn update(
&mut self,
mass_error: f64,
momentum_error: f64,
energy_error: f64,
max_rank: usize,
truncation_error: f64,
) -> f64 {
self.current_max_rank = max_rank;
self.truncation_errors.push(truncation_error);
self.conservation_errors
.push((mass_error, momentum_error, energy_error));
let worst_conservation = mass_error.max(momentum_error).max(energy_error);
self.budget_saturated = max_rank >= self.rank_budget;
if self.budget_saturated {
return self.tolerance;
}
if worst_conservation > self.conservation_tight {
self.tolerance = (self.tolerance * self.tighten_factor).max(self.min_tolerance);
} else if worst_conservation < self.conservation_loose {
self.tolerance = (self.tolerance * self.loosen_factor).min(self.max_tolerance);
}
self.tolerance
}
pub fn should_increase_budget(&self) -> bool {
if !self.budget_saturated {
return false;
}
let n = self.conservation_errors.len();
if n < 3 {
return false;
}
self.conservation_errors[n - 3..]
.iter()
.all(|(m, p, e)| m.max(*p).max(*e) > self.conservation_tight)
}
pub fn summary(&self) -> RankAdaptiveSummary {
let n = self.conservation_errors.len();
let (last_mass, last_mom, last_energy) = if n > 0 {
self.conservation_errors[n - 1]
} else {
(0.0, 0.0, 0.0)
};
RankAdaptiveSummary {
tolerance: self.tolerance,
max_rank: self.current_max_rank,
rank_budget: self.rank_budget,
budget_saturated: self.budget_saturated,
last_mass_error: last_mass,
last_momentum_error: last_mom,
last_energy_error: last_energy,
total_steps: n,
}
}
}
#[derive(Clone, Debug)]
pub struct RankAdaptiveSummary {
pub tolerance: f64,
pub max_rank: usize,
pub rank_budget: usize,
pub budget_saturated: bool,
pub last_mass_error: f64,
pub last_momentum_error: f64,
pub last_energy_error: f64,
pub total_steps: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rank_adaptive_tightens_on_conservation_error() {
let mut ctrl = RankAdaptiveController::new(100);
ctrl.tolerance = 1e-6;
ctrl.conservation_tight = 1e-6;
let new_tol = ctrl.update(1e-4, 1e-5, 1e-5, 10, 1e-6);
assert!(
new_tol < 1e-6,
"Tolerance should decrease on conservation error: {new_tol}"
);
}
#[test]
fn rank_adaptive_loosens_when_well_conserved() {
let mut ctrl = RankAdaptiveController::new(100);
ctrl.tolerance = 1e-8;
ctrl.conservation_loose = 1e-8;
let new_tol = ctrl.update(1e-12, 1e-12, 1e-12, 10, 1e-8);
assert!(
new_tol > 1e-8,
"Tolerance should increase when over-resolving: {new_tol}"
);
}
#[test]
fn rank_adaptive_respects_budget() {
let mut ctrl = RankAdaptiveController::new(20);
ctrl.tolerance = 1e-6;
let tol_before = ctrl.tolerance;
let new_tol = ctrl.update(1e-4, 1e-4, 1e-4, 20, 1e-6);
assert_eq!(
new_tol, tol_before,
"Should not tighten when budget-saturated"
);
assert!(ctrl.budget_saturated);
}
#[test]
fn rank_adaptive_budget_increase_recommendation() {
let mut ctrl = RankAdaptiveController::new(20);
ctrl.conservation_tight = 1e-6;
ctrl.update(1e-4, 1e-4, 1e-4, 20, 1e-6);
ctrl.update(1e-4, 1e-4, 1e-4, 20, 1e-6);
ctrl.update(1e-4, 1e-4, 1e-4, 20, 1e-6);
assert!(
ctrl.should_increase_budget(),
"Should recommend budget increase after sustained saturation"
);
}
#[test]
fn rank_adaptive_tolerance_bounds() {
let mut ctrl = RankAdaptiveController::new(100);
ctrl.min_tolerance = 1e-10;
ctrl.max_tolerance = 1e-3;
ctrl.tolerance = 1e-10;
let new_tol = ctrl.update(1.0, 1.0, 1.0, 5, 1e-10);
assert!(
new_tol >= 1e-10,
"Tolerance should not go below minimum: {new_tol}"
);
ctrl.tolerance = 1e-3;
let new_tol = ctrl.update(1e-15, 1e-15, 1e-15, 5, 1e-3);
assert!(
new_tol <= 1e-3,
"Tolerance should not go above maximum: {new_tol}"
);
}
}