use cobre_solver::SolverStatistics;
#[derive(Debug, Clone, Default)]
pub struct SolverStatsDelta {
pub lp_solves: u64,
pub lp_successes: u64,
pub first_try_successes: u64,
pub lp_failures: u64,
pub retry_attempts: u64,
pub basis_offered: u64,
pub basis_consistency_failures: u64,
pub simplex_iterations: u64,
pub solve_time_ms: f64,
pub load_model_count: u64,
pub load_model_time_ms: f64,
pub set_bounds_time_ms: f64,
pub basis_set_time_ms: f64,
pub retry_level_histogram: Vec<u64>,
}
fn ensure_histogram_capacity(result: &mut Vec<u64>, source: &[u64]) {
if result.is_empty() && !source.is_empty() {
result.resize(source.len(), 0);
}
}
impl SolverStatsDelta {
#[must_use]
pub fn from_snapshots(before: &SolverStatistics, after: &SolverStatistics) -> Self {
Self {
lp_solves: after.solve_count - before.solve_count,
lp_successes: after.success_count - before.success_count,
first_try_successes: after.first_try_successes - before.first_try_successes,
lp_failures: after.failure_count - before.failure_count,
retry_attempts: after.retry_count - before.retry_count,
basis_offered: after.basis_offered - before.basis_offered,
basis_consistency_failures: after.basis_consistency_failures
- before.basis_consistency_failures,
simplex_iterations: after.total_iterations - before.total_iterations,
solve_time_ms: (after.total_solve_time_seconds - before.total_solve_time_seconds)
* 1000.0,
load_model_count: after.load_model_count - before.load_model_count,
load_model_time_ms: (after.total_load_model_time_seconds
- before.total_load_model_time_seconds)
* 1000.0,
set_bounds_time_ms: (after.total_set_bounds_time_seconds
- before.total_set_bounds_time_seconds)
* 1000.0,
basis_set_time_ms: (after.total_basis_set_time_seconds
- before.total_basis_set_time_seconds)
* 1000.0,
retry_level_histogram: after
.retry_level_histogram
.iter()
.zip(&before.retry_level_histogram)
.map(|(a, b)| a - b)
.collect(),
}
}
pub fn accumulate_into(dst: &mut Self, rhs: &Self) {
dst.lp_solves += rhs.lp_solves;
dst.lp_successes += rhs.lp_successes;
dst.first_try_successes += rhs.first_try_successes;
dst.lp_failures += rhs.lp_failures;
dst.retry_attempts += rhs.retry_attempts;
dst.basis_offered += rhs.basis_offered;
dst.basis_consistency_failures += rhs.basis_consistency_failures;
dst.simplex_iterations += rhs.simplex_iterations;
dst.solve_time_ms += rhs.solve_time_ms;
dst.load_model_count += rhs.load_model_count;
dst.load_model_time_ms += rhs.load_model_time_ms;
dst.set_bounds_time_ms += rhs.set_bounds_time_ms;
dst.basis_set_time_ms += rhs.basis_set_time_ms;
ensure_histogram_capacity(&mut dst.retry_level_histogram, &rhs.retry_level_histogram);
for (d, s) in dst
.retry_level_histogram
.iter_mut()
.zip(&rhs.retry_level_histogram)
{
*d += s;
}
}
pub fn clone_into_reuse(&self, dst: &mut Self) {
dst.lp_solves = self.lp_solves;
dst.lp_successes = self.lp_successes;
dst.first_try_successes = self.first_try_successes;
dst.lp_failures = self.lp_failures;
dst.retry_attempts = self.retry_attempts;
dst.basis_offered = self.basis_offered;
dst.basis_consistency_failures = self.basis_consistency_failures;
dst.simplex_iterations = self.simplex_iterations;
dst.solve_time_ms = self.solve_time_ms;
dst.load_model_count = self.load_model_count;
dst.load_model_time_ms = self.load_model_time_ms;
dst.set_bounds_time_ms = self.set_bounds_time_ms;
dst.basis_set_time_ms = self.basis_set_time_ms;
let n = self.retry_level_histogram.len();
dst.retry_level_histogram.resize(n, 0);
dst.retry_level_histogram
.copy_from_slice(&self.retry_level_histogram);
}
pub fn reset_in_place(&mut self) {
self.lp_solves = 0;
self.lp_successes = 0;
self.first_try_successes = 0;
self.lp_failures = 0;
self.retry_attempts = 0;
self.basis_offered = 0;
self.basis_consistency_failures = 0;
self.simplex_iterations = 0;
self.solve_time_ms = 0.0;
self.load_model_count = 0;
self.load_model_time_ms = 0.0;
self.set_bounds_time_ms = 0.0;
self.basis_set_time_ms = 0.0;
self.retry_level_histogram.clear();
}
#[must_use]
pub fn aggregate<'a>(deltas: impl Iterator<Item = &'a Self>) -> Self {
let mut result = Self::default();
for d in deltas {
result.lp_solves += d.lp_solves;
result.lp_successes += d.lp_successes;
result.first_try_successes += d.first_try_successes;
result.lp_failures += d.lp_failures;
result.retry_attempts += d.retry_attempts;
result.basis_offered += d.basis_offered;
result.basis_consistency_failures += d.basis_consistency_failures;
result.simplex_iterations += d.simplex_iterations;
result.solve_time_ms += d.solve_time_ms;
result.load_model_count += d.load_model_count;
result.load_model_time_ms += d.load_model_time_ms;
result.set_bounds_time_ms += d.set_bounds_time_ms;
result.basis_set_time_ms += d.basis_set_time_ms;
ensure_histogram_capacity(&mut result.retry_level_histogram, &d.retry_level_histogram);
for (dst, src) in result
.retry_level_histogram
.iter_mut()
.zip(&d.retry_level_histogram)
{
*dst += src;
}
}
result
}
}
#[must_use]
pub fn aggregate_solver_statistics(
stats: impl Iterator<Item = SolverStatistics>,
) -> SolverStatistics {
let mut result = SolverStatistics::default();
for s in stats {
result.solve_count += s.solve_count;
result.success_count += s.success_count;
result.failure_count += s.failure_count;
result.total_iterations += s.total_iterations;
result.retry_count += s.retry_count;
result.total_solve_time_seconds += s.total_solve_time_seconds;
result.basis_consistency_failures += s.basis_consistency_failures;
result.first_try_successes += s.first_try_successes;
result.basis_offered += s.basis_offered;
result.load_model_count += s.load_model_count;
result.total_load_model_time_seconds += s.total_load_model_time_seconds;
result.total_set_bounds_time_seconds += s.total_set_bounds_time_seconds;
result.total_basis_set_time_seconds += s.total_basis_set_time_seconds;
result.basis_reconstructions += s.basis_reconstructions;
ensure_histogram_capacity(&mut result.retry_level_histogram, &s.retry_level_histogram);
for (dst, src) in result
.retry_level_histogram
.iter_mut()
.zip(&s.retry_level_histogram)
{
*dst += src;
}
}
result
}
#[derive(Debug, Clone)]
pub struct SolverStatsLogEntry {
pub iteration: u64,
pub phase: &'static str,
pub stage: i32,
pub opening: Option<i32>,
pub rank: i32,
pub worker_id: Option<i32>,
pub delta: SolverStatsDelta,
}
impl SolverStatsLogEntry {
#[must_use]
pub fn from_raw(
iteration: u64,
phase: &'static str,
stage: i32,
opening: i32,
rank: i32,
worker_id: i32,
delta: SolverStatsDelta,
) -> Self {
Self {
iteration,
phase,
stage,
opening: (opening != -1).then_some(opening),
rank,
worker_id: (worker_id != -1).then_some(worker_id),
delta,
}
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn delta_to_stats_row(
id: u32,
phase: &str,
stage: i32,
opening: Option<i32>,
rank: Option<i32>,
worker_id: Option<i32>,
delta: &SolverStatsDelta,
) -> cobre_io::SolverStatsRow {
cobre_io::SolverStatsRow {
iteration: id,
phase: phase.to_string(),
stage,
opening,
rank,
worker_id,
lp_solves: delta.lp_solves as u32,
lp_successes: delta.lp_successes as u32,
lp_retries: delta.lp_successes.saturating_sub(delta.first_try_successes) as u32,
lp_failures: delta.lp_failures as u32,
retry_attempts: delta.retry_attempts as u32,
basis_offered: delta.basis_offered as u32,
basis_consistency_failures: delta.basis_consistency_failures as u32,
simplex_iterations: delta.simplex_iterations,
solve_time_ms: delta.solve_time_ms,
load_model_time_ms: delta.load_model_time_ms,
set_bounds_time_ms: delta.set_bounds_time_ms,
basis_set_time_ms: delta.basis_set_time_ms,
retry_level_histogram: delta.retry_level_histogram.clone(),
}
}
#[must_use]
pub fn solver_stats_log_to_rows(log: &[SolverStatsLogEntry]) -> Vec<cobre_io::SolverStatsRow> {
log.iter()
.map(|entry| {
#[allow(clippy::cast_possible_truncation)] let id = entry.iteration as u32;
delta_to_stats_row(
id,
entry.phase,
entry.stage,
entry.opening,
Some(entry.rank),
entry.worker_id,
&entry.delta,
)
})
.collect()
}
pub const SOLVER_STATS_DELTA_SCALAR_FIELDS: usize = 13;
pub const SCENARIO_STATS_STRIDE: usize = 1 + SOLVER_STATS_DELTA_SCALAR_FIELDS;
pub const WORKER_STATS_ENTRY_STRIDE: usize = 2 + SOLVER_STATS_DELTA_SCALAR_FIELDS;
#[must_use]
#[inline]
pub fn worker_opening_stats_buffer_size(n_workers: usize, n_slots: usize) -> usize {
n_workers * n_slots * WORKER_STATS_ENTRY_STRIDE
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn pack_delta_scalars(delta: &SolverStatsDelta) -> [f64; SOLVER_STATS_DELTA_SCALAR_FIELDS] {
[
delta.lp_solves as f64, delta.lp_successes as f64, delta.first_try_successes as f64, delta.lp_failures as f64, delta.retry_attempts as f64, delta.basis_offered as f64, delta.basis_consistency_failures as f64, delta.simplex_iterations as f64, delta.load_model_count as f64, delta.solve_time_ms, delta.load_model_time_ms, delta.set_bounds_time_ms, delta.basis_set_time_ms, ]
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn unpack_delta_scalars(buf: &[f64; SOLVER_STATS_DELTA_SCALAR_FIELDS]) -> SolverStatsDelta {
SolverStatsDelta {
lp_solves: buf[0] as u64,
lp_successes: buf[1] as u64,
first_try_successes: buf[2] as u64,
lp_failures: buf[3] as u64,
retry_attempts: buf[4] as u64,
basis_offered: buf[5] as u64,
basis_consistency_failures: buf[6] as u64,
simplex_iterations: buf[7] as u64,
load_model_count: buf[8] as u64,
solve_time_ms: buf[9],
load_model_time_ms: buf[10],
set_bounds_time_ms: buf[11],
basis_set_time_ms: buf[12],
retry_level_histogram: Vec::new(),
}
}
#[must_use]
pub fn pack_scenario_stats(stats: &[(u32, SolverStatsDelta)]) -> Vec<f64> {
let mut buf = Vec::with_capacity(stats.len() * SCENARIO_STATS_STRIDE);
for (scenario_id, delta) in stats {
buf.push(f64::from(*scenario_id));
buf.extend_from_slice(&pack_delta_scalars(delta));
}
buf
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn unpack_scenario_stats(buf: &[f64]) -> Vec<(u32, SolverStatsDelta)> {
debug_assert_eq!(
buf.len() % SCENARIO_STATS_STRIDE,
0,
"buffer length must be a multiple of SCENARIO_STATS_STRIDE"
);
buf.chunks_exact(SCENARIO_STATS_STRIDE)
.map(|chunk| {
let scenario_id = chunk[0] as u32;
let arr = [
chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7], chunk[8],
chunk[9], chunk[10], chunk[11], chunk[12], chunk[13],
];
(scenario_id, unpack_delta_scalars(&arr))
})
.collect()
}
#[allow(clippy::cast_precision_loss)]
pub fn pack_worker_opening_stats(
out: &mut [f64],
stats: &[SolverStatsDelta],
n_workers: usize,
n_slots: usize,
) {
debug_assert_eq!(stats.len(), n_workers * n_slots);
debug_assert_eq!(out.len(), n_workers * n_slots * WORKER_STATS_ENTRY_STRIDE);
for w in 0..n_workers {
for k in 0..n_slots {
let entry_base = (w * n_slots + k) * WORKER_STATS_ENTRY_STRIDE;
out[entry_base] = w as f64;
out[entry_base + 1] = k as f64;
let scalars = pack_delta_scalars(&stats[w * n_slots + k]);
out[entry_base + 2..entry_base + WORKER_STATS_ENTRY_STRIDE].copy_from_slice(&scalars);
}
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::expect_used
)]
pub fn unpack_worker_opening_stats(
buf: &[f64],
out: &mut [SolverStatsDelta],
n_workers: usize,
n_slots: usize,
) {
debug_assert_eq!(buf.len(), n_workers * n_slots * WORKER_STATS_ENTRY_STRIDE);
debug_assert_eq!(out.len(), n_workers * n_slots);
for w in 0..n_workers {
for k in 0..n_slots {
let entry_base = (w * n_slots + k) * WORKER_STATS_ENTRY_STRIDE;
let scalars: [f64; SOLVER_STATS_DELTA_SCALAR_FIELDS] = buf
[entry_base + 2..entry_base + WORKER_STATS_ENTRY_STRIDE]
.try_into()
.expect("slice length equals SOLVER_STATS_DELTA_SCALAR_FIELDS");
out[w * n_slots + k] = unpack_delta_scalars(&scalars);
}
}
}
#[derive(Debug)]
pub struct StageWorkerStatsBuffer {
data: Vec<SolverStatsDelta>,
n_workers: usize,
n_slots: usize,
}
impl StageWorkerStatsBuffer {
#[must_use]
pub fn new(n_workers: usize, n_slots: usize) -> Self {
Self {
data: vec![SolverStatsDelta::default(); n_workers * n_slots],
n_workers,
n_slots,
}
}
#[inline]
#[must_use]
pub fn index(&self, worker_id: usize, slot: usize) -> usize {
debug_assert!(
worker_id < self.n_workers,
"worker_id {worker_id} >= n_workers {}",
self.n_workers
);
debug_assert!(
slot < self.n_slots,
"slot {slot} >= n_slots {}",
self.n_slots
);
worker_id * self.n_slots + slot
}
#[must_use]
pub fn get(&self, worker_id: usize, slot: usize) -> &SolverStatsDelta {
let idx = self.index(worker_id, slot);
&self.data[idx]
}
pub fn set(&mut self, worker_id: usize, slot: usize, delta: SolverStatsDelta) {
let idx = self.index(worker_id, slot);
self.data[idx] = delta;
}
pub fn reset(&mut self) {
for slot in &mut self.data {
slot.reset_in_place();
}
}
#[must_use]
pub fn as_slice(&self) -> &[SolverStatsDelta] {
&self.data
}
#[must_use]
pub fn n_workers(&self) -> usize {
self.n_workers
}
#[must_use]
pub fn n_slots(&self) -> usize {
self.n_slots
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::float_cmp,
clippy::cast_precision_loss
)]
mod tests {
use super::*;
#[test]
fn test_from_snapshots_all_deltas() {
let before = SolverStatistics {
solve_count: 10,
success_count: 9,
failure_count: 1,
total_iterations: 500,
retry_count: 3,
total_solve_time_seconds: 2.0,
basis_consistency_failures: 1,
first_try_successes: 7,
basis_offered: 8,
load_model_count: 5,
total_load_model_time_seconds: 1.0,
total_set_bounds_time_seconds: 0.25,
total_basis_set_time_seconds: 0.1,
basis_reconstructions: 0,
retry_level_histogram: vec![0; 12],
};
let after = SolverStatistics {
solve_count: 20,
success_count: 18,
failure_count: 2,
total_iterations: 1100,
retry_count: 5,
total_solve_time_seconds: 4.5,
basis_consistency_failures: 3,
first_try_successes: 15,
basis_offered: 17,
load_model_count: 12,
total_load_model_time_seconds: 3.0,
total_set_bounds_time_seconds: 0.75,
total_basis_set_time_seconds: 0.3,
basis_reconstructions: 0,
retry_level_histogram: vec![0; 12],
};
let delta = SolverStatsDelta::from_snapshots(&before, &after);
assert_eq!(delta.lp_solves, 10);
assert_eq!(delta.lp_successes, 9);
assert_eq!(delta.first_try_successes, 8);
assert_eq!(delta.lp_failures, 1);
assert_eq!(delta.retry_attempts, 2);
assert_eq!(delta.basis_offered, 9);
assert_eq!(delta.basis_consistency_failures, 2);
assert_eq!(delta.simplex_iterations, 600);
assert!((delta.solve_time_ms - 2500.0).abs() < 1e-6);
assert_eq!(delta.load_model_count, 7);
assert!((delta.load_model_time_ms - 2000.0).abs() < 1e-6);
assert!((delta.set_bounds_time_ms - 500.0).abs() < 1e-6);
assert!((delta.basis_set_time_ms - 200.0).abs() < 1e-6);
}
#[test]
fn test_from_snapshots_zero_delta() {
let snap = SolverStatistics {
solve_count: 5,
success_count: 5,
failure_count: 0,
total_iterations: 200,
retry_count: 0,
total_solve_time_seconds: 1.0,
basis_consistency_failures: 0,
first_try_successes: 5,
basis_offered: 3,
load_model_count: 3,
total_load_model_time_seconds: 0.1,
total_set_bounds_time_seconds: 0.02,
total_basis_set_time_seconds: 0.01,
basis_reconstructions: 0,
retry_level_histogram: vec![0; 12],
};
let delta = SolverStatsDelta::from_snapshots(&snap, &snap);
assert_eq!(delta.lp_solves, 0);
assert_eq!(delta.lp_successes, 0);
assert_eq!(delta.first_try_successes, 0);
assert_eq!(delta.lp_failures, 0);
assert_eq!(delta.retry_attempts, 0);
assert_eq!(delta.basis_offered, 0);
assert_eq!(delta.basis_consistency_failures, 0);
assert_eq!(delta.simplex_iterations, 0);
assert!((delta.solve_time_ms).abs() < 1e-10);
assert!((delta.load_model_time_ms).abs() < 1e-10);
assert!((delta.set_bounds_time_ms).abs() < 1e-10);
assert!((delta.basis_set_time_ms).abs() < 1e-10);
}
#[test]
fn test_aggregate_empty_returns_default() {
let agg = SolverStatsDelta::aggregate(std::iter::empty());
assert_eq!(agg.lp_solves, 0);
assert_eq!(agg.solve_time_ms, 0.0);
}
#[test]
fn test_aggregate_sums_all_fields() {
let d1 = SolverStatsDelta {
lp_solves: 10,
lp_successes: 9,
first_try_successes: 8,
lp_failures: 1,
retry_attempts: 2,
basis_offered: 7,
basis_consistency_failures: 1,
simplex_iterations: 500,
solve_time_ms: 100.0,
load_model_count: 5,
load_model_time_ms: 10.0,
set_bounds_time_ms: 2.0,
basis_set_time_ms: 1.0,
retry_level_histogram: vec![0; 12],
};
let d2 = SolverStatsDelta {
lp_solves: 20,
lp_successes: 19,
first_try_successes: 17,
lp_failures: 1,
retry_attempts: 3,
basis_offered: 15,
basis_consistency_failures: 2,
simplex_iterations: 800,
solve_time_ms: 200.0,
load_model_count: 10,
load_model_time_ms: 20.0,
set_bounds_time_ms: 4.0,
basis_set_time_ms: 2.0,
retry_level_histogram: vec![0; 12],
};
let agg = SolverStatsDelta::aggregate([d1, d2].iter());
assert_eq!(agg.lp_solves, 30);
assert_eq!(agg.lp_successes, 28);
assert_eq!(agg.first_try_successes, 25);
assert_eq!(agg.lp_failures, 2);
assert_eq!(agg.retry_attempts, 5);
assert_eq!(agg.basis_offered, 22);
assert_eq!(agg.basis_consistency_failures, 3);
assert_eq!(agg.simplex_iterations, 1300);
assert!((agg.solve_time_ms - 300.0).abs() < 1e-6);
assert_eq!(agg.load_model_count, 15);
assert!((agg.load_model_time_ms - 30.0).abs() < 1e-6);
assert!((agg.set_bounds_time_ms - 6.0).abs() < 1e-6);
assert!((agg.basis_set_time_ms - 3.0).abs() < 1e-6);
}
#[test]
fn test_aggregate_solver_statistics_sums_all_fields() {
let s1 = SolverStatistics {
solve_count: 10,
success_count: 9,
failure_count: 1,
total_iterations: 500,
retry_count: 3,
total_solve_time_seconds: 2.0,
basis_consistency_failures: 1,
first_try_successes: 7,
basis_offered: 8,
load_model_count: 5,
total_load_model_time_seconds: 1.0,
total_set_bounds_time_seconds: 0.25,
total_basis_set_time_seconds: 0.05,
basis_reconstructions: 4,
retry_level_histogram: vec![1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
};
let s2 = SolverStatistics {
solve_count: 20,
success_count: 18,
failure_count: 2,
total_iterations: 1100,
retry_count: 5,
total_solve_time_seconds: 4.5,
basis_consistency_failures: 3,
first_try_successes: 15,
basis_offered: 17,
load_model_count: 12,
total_load_model_time_seconds: 3.0,
total_set_bounds_time_seconds: 0.75,
total_basis_set_time_seconds: 0.15,
basis_reconstructions: 10,
retry_level_histogram: vec![0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
};
let agg = aggregate_solver_statistics([s1, s2].into_iter());
assert_eq!(agg.solve_count, 30);
assert_eq!(agg.success_count, 27);
assert_eq!(agg.failure_count, 3);
assert_eq!(agg.total_iterations, 1600);
assert_eq!(agg.retry_count, 8);
assert!((agg.total_solve_time_seconds - 6.5).abs() < 1e-10);
assert_eq!(agg.basis_consistency_failures, 4);
assert_eq!(agg.first_try_successes, 22);
assert_eq!(agg.basis_offered, 25);
assert_eq!(agg.load_model_count, 17);
assert!((agg.total_load_model_time_seconds - 4.0).abs() < 1e-10);
assert!((agg.total_set_bounds_time_seconds - 1.0).abs() < 1e-10);
assert!((agg.total_basis_set_time_seconds - 0.2).abs() < 1e-10);
assert_eq!(agg.retry_level_histogram[0], 1);
assert_eq!(agg.retry_level_histogram[1], 2);
assert_eq!(agg.retry_level_histogram[2], 0);
assert_eq!(agg.basis_reconstructions, 14);
}
fn make_delta(lp_solves: u64) -> SolverStatsDelta {
SolverStatsDelta {
lp_solves,
lp_successes: lp_solves,
first_try_successes: lp_solves / 2,
lp_failures: 0,
retry_attempts: 1,
basis_offered: lp_solves,
basis_consistency_failures: 2,
simplex_iterations: lp_solves * 10,
solve_time_ms: lp_solves as f64 * 0.5,
load_model_count: 3,
load_model_time_ms: 1.5,
set_bounds_time_ms: 0.25,
basis_set_time_ms: 0.125,
retry_level_histogram: vec![0; 12],
}
}
#[test]
fn test_pack_unpack_delta_scalars_round_trip() {
let delta = make_delta(600);
let packed = pack_delta_scalars(&delta);
assert_eq!(packed.len(), SOLVER_STATS_DELTA_SCALAR_FIELDS);
let unpacked = unpack_delta_scalars(&packed);
assert_eq!(unpacked.lp_solves, delta.lp_solves);
assert_eq!(unpacked.lp_successes, delta.lp_successes);
assert_eq!(unpacked.first_try_successes, delta.first_try_successes);
assert_eq!(unpacked.lp_failures, delta.lp_failures);
assert_eq!(unpacked.retry_attempts, delta.retry_attempts);
assert_eq!(unpacked.basis_offered, delta.basis_offered);
assert_eq!(
unpacked.basis_consistency_failures,
delta.basis_consistency_failures
);
assert_eq!(unpacked.simplex_iterations, delta.simplex_iterations);
assert_eq!(unpacked.load_model_count, delta.load_model_count);
assert!((unpacked.solve_time_ms - delta.solve_time_ms).abs() < 1e-10);
assert!((unpacked.load_model_time_ms - delta.load_model_time_ms).abs() < 1e-10);
assert!((unpacked.set_bounds_time_ms - delta.set_bounds_time_ms).abs() < 1e-10);
assert!((unpacked.basis_set_time_ms - delta.basis_set_time_ms).abs() < 1e-10);
assert!(unpacked.retry_level_histogram.is_empty());
}
#[test]
fn test_pack_unpack_delta_scalars_identity_for_lp_solves_600() {
let delta = make_delta(600);
let packed = pack_delta_scalars(&delta);
let unpacked = unpack_delta_scalars(&packed);
assert_eq!(unpacked.lp_solves, 600);
}
#[test]
fn test_pack_unpack_scenario_stats_round_trip_three_entries() {
let stats = vec![
(7u32, make_delta(100)),
(12u32, make_delta(200)),
(25u32, make_delta(300)),
];
let buf = pack_scenario_stats(&stats);
assert_eq!(buf.len(), 3 * SCENARIO_STATS_STRIDE);
let unpacked = unpack_scenario_stats(&buf);
assert_eq!(unpacked.len(), 3);
assert_eq!(unpacked[0].0, 7);
assert_eq!(unpacked[1].0, 12);
assert_eq!(unpacked[2].0, 25);
assert_eq!(unpacked[0].1.lp_solves, 100);
assert_eq!(unpacked[1].1.lp_solves, 200);
assert_eq!(unpacked[2].1.lp_solves, 300);
assert!((unpacked[0].1.solve_time_ms - 50.0).abs() < 1e-10);
assert!((unpacked[1].1.solve_time_ms - 100.0).abs() < 1e-10);
assert!((unpacked[2].1.solve_time_ms - 150.0).abs() < 1e-10);
}
#[test]
fn test_pack_scenario_stats_empty_round_trip() {
let buf = pack_scenario_stats(&[]);
assert!(buf.is_empty());
let unpacked = unpack_scenario_stats(&buf);
assert!(unpacked.is_empty());
}
#[test]
fn test_pack_delta_scalars_field_count() {
let delta = SolverStatsDelta::default();
let packed = pack_delta_scalars(&delta);
assert_eq!(
packed.len(),
13,
"pack_delta_scalars must return 13 elements"
);
assert_eq!(packed.len(), SOLVER_STATS_DELTA_SCALAR_FIELDS);
assert_eq!(SOLVER_STATS_DELTA_SCALAR_FIELDS, 13);
}
#[test]
fn test_accumulate_into_all_fields() {
let mut dst = make_delta(10);
dst.retry_level_histogram = vec![1, 0, 2, 0];
let rhs = make_delta(5);
let mut rhs_full = rhs.clone();
rhs_full.retry_level_histogram = vec![0, 3, 0, 1];
SolverStatsDelta::accumulate_into(&mut dst, &rhs_full);
assert_eq!(dst.lp_solves, 15);
assert_eq!(dst.lp_successes, 15);
assert_eq!(dst.first_try_successes, 7); assert_eq!(dst.lp_failures, 0);
assert_eq!(dst.retry_attempts, 2); assert_eq!(dst.basis_offered, 15);
assert_eq!(dst.basis_consistency_failures, 4); assert_eq!(dst.simplex_iterations, 150); assert!((dst.solve_time_ms - 7.5).abs() < 1e-10); assert_eq!(dst.load_model_count, 6); assert!((dst.load_model_time_ms - 3.0).abs() < 1e-10);
assert!((dst.set_bounds_time_ms - 0.5).abs() < 1e-10);
assert!((dst.basis_set_time_ms - 0.25).abs() < 1e-10);
assert_eq!(dst.retry_level_histogram, vec![1, 3, 2, 1]);
}
#[test]
fn test_solver_stats_log_per_opening_shape() {
let fwd_entry = SolverStatsLogEntry::from_raw(1, "forward", 0, -1, 0, -1, make_delta(4));
let bwd_entry_0 = SolverStatsLogEntry::from_raw(1, "backward", 2, 0, 0, 0, make_delta(2));
let bwd_entry_1 = SolverStatsLogEntry::from_raw(1, "backward", 2, 1, 0, 1, make_delta(3));
let lb_entry =
SolverStatsLogEntry::from_raw(1, "lower_bound", -1, -1, 0, -1, make_delta(1));
let log: Vec<SolverStatsLogEntry> = vec![fwd_entry, bwd_entry_0, bwd_entry_1, lb_entry];
assert_eq!(log[0].phase, "forward");
assert!(
log[0].stage >= 0,
"forward stage must be a real stage index, got {}",
log[0].stage
);
assert_eq!(log[0].opening, None);
assert_eq!(
log[0].worker_id, None,
"forward rows have no per-worker dimension"
);
assert_eq!(log[1].stage, 2);
assert_eq!(log[1].opening, Some(0));
assert_eq!(log[1].rank, 0);
assert_eq!(log[1].worker_id, Some(0));
assert_eq!(log[1].delta.lp_solves, 2);
assert_eq!(log[2].stage, 2);
assert_eq!(log[2].opening, Some(1));
assert_eq!(log[2].rank, 0);
assert_eq!(log[2].worker_id, Some(1));
assert_eq!(log[2].delta.lp_solves, 3);
let backward_entries: Vec<&SolverStatsDelta> = log
.iter()
.filter(|e| e.phase == "backward")
.map(|e| &e.delta)
.collect();
let collapsed = SolverStatsDelta::aggregate(backward_entries.into_iter());
assert_eq!(collapsed.lp_solves, 5);
assert_eq!(log[3].opening, None);
assert_eq!(log[3].worker_id, None);
}
#[test]
fn solver_stats_log_entry_from_raw_decodes_minus_one_to_none() {
let forward = SolverStatsLogEntry::from_raw(3, "forward", 1, -1, 0, -1, make_delta(5));
assert_eq!(forward.opening, None);
assert_eq!(forward.worker_id, None);
let backward = SolverStatsLogEntry::from_raw(3, "backward", 2, 0, 1, 4, make_delta(2));
assert_eq!(backward.opening, Some(0));
assert_eq!(backward.worker_id, Some(4));
assert_eq!(backward.rank, 1);
assert_eq!(backward.stage, 2);
}
#[test]
fn test_solver_stats_log_to_rows_decodes_minus_one_to_none() {
let backward = SolverStatsLogEntry::from_raw(3, "backward", 2, 0, 1, 4, make_delta(2));
let forward = SolverStatsLogEntry::from_raw(3, "forward", 1, -1, 1, -1, make_delta(5));
let log: Vec<SolverStatsLogEntry> = vec![backward, forward];
let rows = solver_stats_log_to_rows(&log);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].opening, Some(0));
assert_eq!(rows[0].worker_id, Some(4));
assert_eq!(rows[0].rank, Some(1));
assert_eq!(rows[0].iteration, 3);
assert_eq!(rows[0].stage, 2);
assert_eq!(rows[0].phase, "backward");
assert_eq!(rows[0].lp_solves, 2);
assert_eq!(rows[1].opening, None);
assert_eq!(rows[1].worker_id, None);
assert_eq!(rows[1].rank, Some(1));
assert_eq!(rows[1].phase, "forward");
assert_eq!(rows[1].lp_solves, 5);
}
#[test]
fn test_forward_stage_stats_summed_across_workers() {
let worker0: Vec<SolverStatsDelta> = vec![
make_delta(10), make_delta(20), make_delta(30), ];
let worker1: Vec<SolverStatsDelta> = vec![
make_delta(5), make_delta(15), make_delta(25), ];
let n_stages = 3;
let mut stage_stats: Vec<SolverStatsDelta> =
(0..n_stages).map(|_| SolverStatsDelta::default()).collect();
for worker_stage_stats in [&worker0, &worker1] {
for (dst, src) in stage_stats.iter_mut().zip(worker_stage_stats) {
SolverStatsDelta::accumulate_into(dst, src);
}
}
assert_eq!(
stage_stats[0].lp_solves, 15,
"stage 0: 10 + 5 = 15 lp_solves"
);
assert_eq!(
stage_stats[1].lp_solves, 35,
"stage 1: 20 + 15 = 35 lp_solves"
);
assert_eq!(
stage_stats[2].lp_solves, 55,
"stage 2: 30 + 25 = 55 lp_solves"
);
assert_eq!(stage_stats[0].simplex_iterations, 150); assert_eq!(stage_stats[1].simplex_iterations, 350); assert_eq!(stage_stats[2].simplex_iterations, 550);
let log: Vec<SolverStatsLogEntry> = stage_stats
.iter()
.enumerate()
.map(|(t, delta)| {
SolverStatsLogEntry::from_raw(
1,
"forward",
i32::try_from(t).expect("stage fits i32"),
-1,
0, -1, delta.clone(),
)
})
.collect();
assert_eq!(log.len(), 3, "one entry per stage");
for (t, entry) in log.iter().enumerate() {
assert_eq!(entry.phase, "forward");
assert_eq!(
entry.stage,
i32::try_from(t).expect("stage fits i32"),
"stage index must match loop variable"
);
assert_eq!(
entry.opening, None,
"forward rows have no opening dimension"
);
assert_eq!(
entry.worker_id, None,
"forward rows have no per-worker dimension"
);
}
}
#[test]
fn test_stage_worker_stats_buffer_index_layout() {
let buf = StageWorkerStatsBuffer::new(3, 4);
assert_eq!(buf.index(0, 0), 0);
assert_eq!(buf.index(0, 3), 3);
assert_eq!(buf.index(1, 0), 4);
assert_eq!(buf.index(2, 3), 11);
assert_eq!(buf.as_slice().len(), 12);
assert_eq!(buf.n_workers(), 3);
assert_eq!(buf.n_slots(), 4);
}
#[test]
fn test_stage_worker_stats_buffer_reset_zeroes_all_slots() {
let mut buf = StageWorkerStatsBuffer::new(2, 3);
for w in 0..2 {
for k in 0..3 {
let d = SolverStatsDelta {
lp_solves: 7,
..SolverStatsDelta::default()
};
buf.set(w, k, d);
}
}
for slot in buf.as_slice() {
assert_eq!(slot.lp_solves, 7);
}
buf.reset();
for slot in buf.as_slice() {
assert_eq!(slot.lp_solves, 0);
}
}
#[test]
fn test_pack_worker_opening_stats_roundtrip() {
let n_workers = 3;
let n_slots = 4;
let mut input: Vec<SolverStatsDelta> = Vec::with_capacity(n_workers * n_slots);
for w in 0..n_workers {
for k in 0..n_slots {
input.push(SolverStatsDelta {
lp_solves: (w * 10 + k) as u64,
..SolverStatsDelta::default()
});
}
}
let mut buf = vec![0.0_f64; worker_opening_stats_buffer_size(n_workers, n_slots)];
assert_eq!(buf.len(), n_workers * n_slots * WORKER_STATS_ENTRY_STRIDE);
pack_worker_opening_stats(&mut buf, &input, n_workers, n_slots);
let mut recovered = vec![SolverStatsDelta::default(); n_workers * n_slots];
unpack_worker_opening_stats(&buf, &mut recovered, n_workers, n_slots);
for w in 0..n_workers {
for k in 0..n_slots {
assert_eq!(
recovered[w * n_slots + k].lp_solves,
(w * 10 + k) as u64,
"lp_solves mismatch at (w={w}, k={k})"
);
}
}
}
#[test]
fn test_pack_worker_opening_stats_buffer_size() {
assert_eq!(worker_opening_stats_buffer_size(10, 20), 10 * 20 * 15);
assert_eq!(worker_opening_stats_buffer_size(10, 20), 3000);
}
#[test]
fn test_pack_worker_opening_stats_layout_invariant() {
let n_workers = 2;
let n_slots = 4;
let input = vec![SolverStatsDelta::default(); n_workers * n_slots];
let mut buf = vec![0.0_f64; worker_opening_stats_buffer_size(n_workers, n_slots)];
pack_worker_opening_stats(&mut buf, &input, n_workers, n_slots);
assert_eq!(buf[0], 0.0);
assert_eq!(buf[1], 0.0);
assert_eq!(buf[WORKER_STATS_ENTRY_STRIDE], 0.0);
assert_eq!(buf[WORKER_STATS_ENTRY_STRIDE + 1], 1.0);
let w1_k0 = WORKER_STATS_ENTRY_STRIDE * n_slots;
assert_eq!(buf[w1_k0], 1.0);
assert_eq!(buf[w1_k0 + 1], 0.0);
}
#[test]
fn test_solver_stats_delta_mpi_wire_format_13_fields() {
assert_eq!(SOLVER_STATS_DELTA_SCALAR_FIELDS, 13);
assert_eq!(SCENARIO_STATS_STRIDE, 14);
assert_eq!(WORKER_STATS_ENTRY_STRIDE, 15);
let delta = SolverStatsDelta {
lp_solves: 1,
lp_successes: 2,
first_try_successes: 3,
lp_failures: 4,
retry_attempts: 5,
basis_offered: 6,
basis_consistency_failures: 7,
simplex_iterations: 8,
load_model_count: 9,
solve_time_ms: 10.5,
load_model_time_ms: 11.25,
set_bounds_time_ms: 12.125,
basis_set_time_ms: 13.0625,
retry_level_histogram: vec![1, 2, 3], };
let packed = pack_delta_scalars(&delta);
assert_eq!(packed.len(), 13);
assert_eq!(packed[0], 1.0); assert_eq!(packed[8], 9.0); assert!((packed[9] - 10.5).abs() < 1e-10); assert!((packed[12] - 13.0625).abs() < 1e-10);
let unpacked = unpack_delta_scalars(&packed);
assert_eq!(unpacked.lp_solves, 1);
assert_eq!(unpacked.lp_successes, 2);
assert_eq!(unpacked.first_try_successes, 3);
assert_eq!(unpacked.lp_failures, 4);
assert_eq!(unpacked.retry_attempts, 5);
assert_eq!(unpacked.basis_offered, 6);
assert_eq!(unpacked.basis_consistency_failures, 7);
assert_eq!(unpacked.simplex_iterations, 8);
assert_eq!(unpacked.load_model_count, 9);
assert!((unpacked.solve_time_ms - 10.5).abs() < 1e-10);
assert!((unpacked.load_model_time_ms - 11.25).abs() < 1e-10);
assert!((unpacked.set_bounds_time_ms - 12.125).abs() < 1e-10);
assert!((unpacked.basis_set_time_ms - 13.0625).abs() < 1e-10);
assert!(unpacked.retry_level_histogram.is_empty());
}
#[test]
fn test_unpack_delta_scalars_array_length_is_compile_time() {
assert_eq!(
std::mem::size_of::<[f64; SOLVER_STATS_DELTA_SCALAR_FIELDS]>(),
13 * std::mem::size_of::<f64>()
);
let buf: [f64; 13] = [0.0; 13];
let _ = unpack_delta_scalars(&buf);
}
#[test]
fn solver_stats_delta_clone_into_reuse_preserves_values() {
let src = SolverStatsDelta {
lp_solves: 42,
lp_successes: 40,
first_try_successes: 38,
lp_failures: 2,
retry_attempts: 5,
basis_offered: 35,
basis_consistency_failures: 3,
simplex_iterations: 1200,
solve_time_ms: 99.5,
load_model_count: 10,
load_model_time_ms: 7.25,
set_bounds_time_ms: 1.5,
basis_set_time_ms: 0.75,
retry_level_histogram: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
};
let mut dst = SolverStatsDelta {
lp_solves: 0,
retry_level_histogram: vec![0; 5], ..SolverStatsDelta::default()
};
src.clone_into_reuse(&mut dst);
assert_eq!(dst.lp_solves, 42);
assert_eq!(dst.lp_successes, 40);
assert_eq!(dst.first_try_successes, 38);
assert_eq!(dst.lp_failures, 2);
assert_eq!(dst.retry_attempts, 5);
assert_eq!(dst.basis_offered, 35);
assert_eq!(dst.basis_consistency_failures, 3);
assert_eq!(dst.simplex_iterations, 1200);
assert!((dst.solve_time_ms - 99.5).abs() < 1e-10);
assert_eq!(dst.load_model_count, 10);
assert!((dst.load_model_time_ms - 7.25).abs() < 1e-10);
assert!((dst.set_bounds_time_ms - 1.5).abs() < 1e-10);
assert!((dst.basis_set_time_ms - 0.75).abs() < 1e-10);
assert_eq!(
dst.retry_level_histogram,
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
);
let src2 = SolverStatsDelta {
lp_solves: 7,
retry_level_histogram: vec![10; 12],
..SolverStatsDelta::default()
};
src2.clone_into_reuse(&mut dst);
assert_eq!(dst.lp_solves, 7);
assert_eq!(dst.retry_level_histogram, vec![10; 12]);
}
#[test]
fn solver_stats_delta_reset_in_place_zeroes_all_fields() {
let mut d = SolverStatsDelta {
lp_solves: 99,
lp_successes: 88,
first_try_successes: 77,
lp_failures: 11,
retry_attempts: 6,
basis_offered: 50,
basis_consistency_failures: 4,
simplex_iterations: 500,
solve_time_ms: 42.0,
load_model_count: 8,
load_model_time_ms: 3.0,
set_bounds_time_ms: 1.0,
basis_set_time_ms: 0.5,
retry_level_histogram: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
};
d.reset_in_place();
assert_eq!(d.lp_solves, 0);
assert_eq!(d.lp_successes, 0);
assert_eq!(d.first_try_successes, 0);
assert_eq!(d.lp_failures, 0);
assert_eq!(d.retry_attempts, 0);
assert_eq!(d.basis_offered, 0);
assert_eq!(d.basis_consistency_failures, 0);
assert_eq!(d.simplex_iterations, 0);
assert_eq!(d.solve_time_ms, 0.0);
assert_eq!(d.load_model_count, 0);
assert_eq!(d.load_model_time_ms, 0.0);
assert_eq!(d.set_bounds_time_ms, 0.0);
assert_eq!(d.basis_set_time_ms, 0.0);
assert!(d.retry_level_histogram.is_empty());
}
}