pub use cobre_solver::ffi::{HIGHS_BASIS_STATUS_BASIC, HIGHS_BASIS_STATUS_LOWER};
use cobre_solver::Basis;
use crate::workspace::CapturedBasis;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ReconstructionTarget {
pub base_row_count: usize,
pub num_cols: usize,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ReconstructionStats {
pub preserved: u32,
pub new_tight: u32,
pub new_slack: u32,
}
pub fn reconstruct_basis<'a, I>(
stored: &CapturedBasis,
target: ReconstructionTarget,
current_cut_rows: I,
out: &mut Basis,
slot_lookup: &mut Vec<Option<u32>>,
) -> ReconstructionStats
where
I: Iterator<Item = (usize, f64, &'a [f64])>,
{
debug_assert!(
stored.basis.row_status.len() == stored.base_row_count + stored.cut_row_slots.len(),
"CapturedBasis invariant violated: row_status.len() {} != base_row_count {} + \
cut_row_slots.len() {}",
stored.basis.row_status.len(),
stored.base_row_count,
stored.cut_row_slots.len(),
);
reconstruct_col_statuses(stored, target, out);
reconstruct_template_row_statuses(stored, target, out);
build_slot_lookup(stored.cut_row_slots.as_slice(), slot_lookup);
let mut stats = ReconstructionStats::default();
for (target_slot, _intercept, _coefficients) in current_cut_rows {
let row_status_byte = if let Some(pos) = slot_lookup.get(target_slot).and_then(|o| *o) {
let stored_row_idx = stored.base_row_count + pos as usize;
stats.preserved += 1;
stored.basis.row_status[stored_row_idx]
} else {
stats.new_slack += 1;
HIGHS_BASIS_STATUS_BASIC
};
out.row_status.push(row_status_byte);
}
stats
}
pub fn reconstruct_basis_uniform_basic(
stored: &CapturedBasis,
target: ReconstructionTarget,
cut_row_count: usize,
out: &mut Basis,
) {
reconstruct_col_statuses(stored, target, out);
reconstruct_template_row_statuses(stored, target, out);
debug_assert_eq!(
out.row_status.len(),
target.base_row_count,
"reconstruct_template_row_statuses must leave exactly base_row_count entries before \
cut-row seeding"
);
out.row_status.resize(
target.base_row_count + cut_row_count,
HIGHS_BASIS_STATUS_BASIC,
);
}
fn reconstruct_col_statuses(stored: &CapturedBasis, target: ReconstructionTarget, out: &mut Basis) {
out.col_status.clear();
out.col_status.extend_from_slice(&stored.basis.col_status);
if out.col_status.len() != target.num_cols {
out.col_status
.resize(target.num_cols, HIGHS_BASIS_STATUS_BASIC);
}
}
fn reconstruct_template_row_statuses(
stored: &CapturedBasis,
target: ReconstructionTarget,
out: &mut Basis,
) {
out.row_status.clear();
if stored.basis.row_status.len() >= target.base_row_count {
out.row_status
.extend_from_slice(&stored.basis.row_status[..target.base_row_count]);
} else {
out.row_status.extend_from_slice(&stored.basis.row_status);
out.row_status
.resize(target.base_row_count, HIGHS_BASIS_STATUS_BASIC);
}
}
fn build_slot_lookup(reconcilable_slots: &[u32], slot_lookup: &mut Vec<Option<u32>>) {
if let Some(max_slot_val) = reconcilable_slots.iter().copied().max() {
let max_slot = max_slot_val as usize;
if slot_lookup.len() <= max_slot {
debug_assert!(
false,
"slot_lookup undersized ({} <= max_slot {}); caller must pre-size to \
initial_pool_capacity",
slot_lookup.len(),
max_slot,
);
slot_lookup.resize(max_slot + 1, None);
}
}
slot_lookup.fill(None);
#[allow(clippy::cast_possible_truncation)]
for (position, &slot) in reconcilable_slots.iter().enumerate() {
slot_lookup[slot as usize] = Some(position as u32);
}
}
pub fn enforce_basic_count_invariant(
out: &mut Basis,
num_row: usize,
base_row_count: usize,
) -> u32 {
debug_assert_eq!(
num_row,
out.row_status.len(),
"enforce_basic_count_invariant: num_row ({num_row}) != out.row_status.len() ({})",
out.row_status.len(),
);
debug_assert!(
base_row_count <= num_row,
"enforce_basic_count_invariant: base_row_count ({base_row_count}) > num_row ({num_row})",
);
let col_basic = out
.col_status
.iter()
.filter(|&&s| s == HIGHS_BASIS_STATUS_BASIC)
.count();
let row_basic = out
.row_status
.iter()
.filter(|&&s| s == HIGHS_BASIS_STATUS_BASIC)
.count();
let total_basic = col_basic + row_basic;
if total_basic <= num_row {
return 0;
}
let mut excess = total_basic - num_row;
let mut demotions: u32 = 0;
for idx in (base_row_count..out.row_status.len()).rev() {
if excess == 0 {
break;
}
if out.row_status[idx] == HIGHS_BASIS_STATUS_BASIC {
out.row_status[idx] = HIGHS_BASIS_STATUS_LOWER;
excess -= 1;
demotions += 1;
}
}
demotions
}
#[cfg(test)]
#[allow(clippy::doc_markdown)]
mod tests {
use cobre_solver::Basis;
use super::{
HIGHS_BASIS_STATUS_BASIC as B, HIGHS_BASIS_STATUS_LOWER as L, ReconstructionStats,
ReconstructionTarget, enforce_basic_count_invariant, reconstruct_basis,
reconstruct_basis_uniform_basic,
};
use crate::workspace::CapturedBasis;
fn make_stored_basis(
base_rows: usize,
num_cols: usize,
slots: &[u32],
cut_statuses: &[i32],
state_at_capture: &[f64],
) -> CapturedBasis {
assert_eq!(slots.len(), cut_statuses.len());
let total_rows = base_rows + cut_statuses.len();
let mut cb = CapturedBasis::new(
num_cols,
total_rows,
base_rows,
slots.len(),
state_at_capture.len(),
);
cb.basis.row_status.clear();
cb.basis.row_status.resize(base_rows, B);
cb.basis.row_status.extend_from_slice(cut_statuses);
cb.basis.col_status.clear();
cb.basis.col_status.resize(num_cols, B);
cb.cut_row_slots.extend_from_slice(slots);
cb.state_at_capture.extend_from_slice(state_at_capture);
cb
}
#[test]
fn returns_basic_for_all_new_cuts() {
let stored = make_stored_basis(1, 2, &[], &[], &[1.0]);
let cuts: Vec<(usize, f64, Vec<f64>)> = vec![
(5, 0.0, vec![0.0, 0.0]),
(6, 0.0, vec![0.0, 0.0]),
(7, 0.0, vec![0.0, 0.0]),
];
let target = ReconstructionTarget {
base_row_count: 1,
num_cols: 2,
};
let mut out = Basis::new(0, 0);
let mut lookup: Vec<Option<u32>> = vec![None; 16];
let stats = reconstruct_basis(
&stored,
target,
cuts.iter().map(|(s, i, c)| (*s, *i, c.as_slice())),
&mut out,
&mut lookup,
);
assert_eq!(
stats,
ReconstructionStats {
preserved: 0,
new_tight: 0,
new_slack: 3,
},
);
assert_eq!(out.row_status.len(), 4);
assert_eq!(&out.row_status[1..], &[B, B, B]);
}
#[test]
fn copies_stored_status_for_preserved_slots() {
let stored = make_stored_basis(1, 2, &[10, 20], &[B, L], &[1.0]);
let cuts: Vec<(usize, f64, Vec<f64>)> =
vec![(10, 0.0, vec![0.0, 0.0]), (20, 0.0, vec![0.0, 0.0])];
let target = ReconstructionTarget {
base_row_count: 1,
num_cols: 2,
};
let mut out = Basis::new(0, 0);
let mut lookup: Vec<Option<u32>> = vec![None; 32];
let stats = reconstruct_basis(
&stored,
target,
cuts.iter().map(|(s, i, c)| (*s, *i, c.as_slice())),
&mut out,
&mut lookup,
);
assert_eq!(
stats,
ReconstructionStats {
preserved: 2,
new_tight: 0,
new_slack: 0,
},
);
assert_eq!(out.row_status.len(), 3);
assert_eq!(out.row_status[1], B, "slot 10 → stored BASIC");
assert_eq!(out.row_status[2], L, "slot 20 → stored LOWER");
}
#[test]
fn mixed_case_preserved_and_new() {
let stored = make_stored_basis(2, 3, &[10, 20, 30, 40], &[L, B, L, B], &[1.0, 2.0]);
let cuts: Vec<(usize, f64, Vec<f64>)> = vec![
(10, 0.0, vec![0.0, 0.0]),
(25, 0.0, vec![0.0, 0.0]),
(30, 0.0, vec![0.0, 0.0]),
(45, 0.0, vec![0.0, 0.0]),
(50, 0.0, vec![0.0, 0.0]),
];
let target = ReconstructionTarget {
base_row_count: 2,
num_cols: 3,
};
let mut out = Basis::new(0, 0);
let mut lookup: Vec<Option<u32>> = vec![None; 64];
let stats = reconstruct_basis(
&stored,
target,
cuts.iter().map(|(s, i, c)| (*s, *i, c.as_slice())),
&mut out,
&mut lookup,
);
assert_eq!(
stats,
ReconstructionStats {
preserved: 2,
new_tight: 0,
new_slack: 3,
},
"preserved={{10, 30}}, new_slack={{25, 45, 50}}",
);
assert_eq!(out.row_status.len(), 7);
assert_eq!(out.row_status[2], L, "slot 10 → stored LOWER");
assert_eq!(out.row_status[3], B, "slot 25 → new → BASIC");
assert_eq!(out.row_status[4], L, "slot 30 → stored LOWER");
assert_eq!(out.row_status[5], B, "slot 45 → new → BASIC");
assert_eq!(out.row_status[6], B, "slot 50 → new → BASIC");
}
#[test]
fn empty_iterator_preserves_template_rows() {
let stored = make_stored_basis(3, 2, &[10, 20], &[B, L], &[1.0]);
let target = ReconstructionTarget {
base_row_count: 3,
num_cols: 2,
};
let mut out = Basis::new(0, 0);
let mut lookup: Vec<Option<u32>> = vec![None; 32];
let cuts: Vec<(usize, f64, Vec<f64>)> = vec![];
let stats = reconstruct_basis(
&stored,
target,
cuts.iter().map(|(s, i, c)| (*s, *i, c.as_slice())),
&mut out,
&mut lookup,
);
assert_eq!(stats, ReconstructionStats::default());
assert_eq!(out.row_status.len(), 3);
assert!(
out.row_status.iter().all(|&s| s == B),
"template rows must be copied verbatim (all BASIC in this fixture)",
);
}
#[test]
fn uniform_basic_appends_all_basic_cut_rows() {
let mut stored = CapturedBasis::new(3, 2, 2, 0, 0);
stored.basis.col_status.clear();
stored.basis.col_status.extend_from_slice(&[B, B, L]);
stored.basis.row_status.clear();
stored.basis.row_status.extend_from_slice(&[L, L]);
let target = ReconstructionTarget {
base_row_count: 2,
num_cols: 3,
};
let mut out = Basis::new(0, 0);
reconstruct_basis_uniform_basic(&stored, target, 4, &mut out);
assert_eq!(out.col_status, vec![B, B, L]);
assert_eq!(out.row_status.len(), 6);
assert_eq!(&out.row_status[0..2], &[L, L]);
assert_eq!(&out.row_status[2..6], &[B, B, B, B]);
}
#[test]
fn uniform_basic_then_invariant_repair_balances() {
let mut stored = CapturedBasis::new(3, 2, 2, 0, 0);
stored.basis.col_status.clear();
stored.basis.col_status.extend_from_slice(&[B, B, B]); stored.basis.row_status.clear();
stored.basis.row_status.extend_from_slice(&[L, L]);
let target = ReconstructionTarget {
base_row_count: 2,
num_cols: 3,
};
let mut out = Basis::new(0, 0);
reconstruct_basis_uniform_basic(&stored, target, 4, &mut out);
assert_eq!(out.col_status, vec![B, B, B]);
assert_eq!(out.row_status, vec![L, L, B, B, B, B]);
let num_row = target.base_row_count + 4; let demotions = enforce_basic_count_invariant(&mut out, num_row, target.base_row_count);
assert_eq!(demotions, 1, "exactly one excess BASIC cut row demoted");
let col_basic = out.col_status.iter().filter(|&&s| s == B).count();
let row_basic = out.row_status.iter().filter(|&&s| s == B).count();
assert_eq!(
col_basic + row_basic,
num_row,
"col_basic + row_basic must equal num_row after repair"
);
}
#[test]
fn uniform_basic_ignores_cut_row_slots() {
let target = ReconstructionTarget {
base_row_count: 1,
num_cols: 2,
};
let with_slots = make_stored_basis(1, 2, &[10, 20, 30], &[B, L, B], &[1.0]);
assert!(
!with_slots.cut_row_slots.is_empty(),
"fixture must have non-empty cut_row_slots to make the test meaningful"
);
let mut out_with = Basis::new(0, 0);
reconstruct_basis_uniform_basic(&with_slots, target, 3, &mut out_with);
let mut without_slots = make_stored_basis(1, 2, &[10, 20, 30], &[B, L, B], &[1.0]);
without_slots.cut_row_slots.clear();
let mut out_without = Basis::new(0, 0);
reconstruct_basis_uniform_basic(&without_slots, target, 3, &mut out_without);
assert_eq!(out_with.col_status, out_without.col_status);
assert_eq!(out_with.row_status, out_without.row_status);
}
#[test]
fn uniform_basic_zero_cut_rows() {
let stored = make_stored_basis(3, 2, &[10, 20], &[B, L], &[1.0]);
let target = ReconstructionTarget {
base_row_count: 3,
num_cols: 2,
};
let mut out = Basis::new(0, 0);
reconstruct_basis_uniform_basic(&stored, target, 0, &mut out);
assert_eq!(out.row_status.len(), 3);
assert_eq!(out.col_status.len(), 2);
}
}