///|
/// The relaxation rows a cut can be rounded from.
///
/// A cut is rounded from one row of an optimal tableau, and that row is a combination
/// of the model's rows: `Σᵢ yᵢ·(row i)` where `y` is a row of the basis inverse. What
/// this module hands out is exactly that combination, in the one form a checker can
/// re-derive it in — **one multiplier per model row**, with the kernel's own
/// normalizations undone, so that
///
/// ```text
/// rⱼ = Σᵢ yᵢ·aᵢⱼ β = Σᵢ yᵢ·bᵢ
/// ```
///
/// recomputed against the model the caller handed in *is* the tableau row. Nothing else
/// travels: not the kernel's row order, not its slack columns, not its sign flips. The
/// checker in `verify` recomputes `r` and `β` from these numbers and the model, which is
/// the only way a cut can be checked by something that has never seen a simplex.
///
/// Two mapping details are worth stating because they are where a sign goes wrong:
///
/// - the kernel normalizes a row with a negative right hand side by negating it, so a
/// row multiplier of the kernel is the model row's multiplier only after that
/// negation is undone (`row_sign`);
/// - a row the kernel invented — the row form of a free variable's upper bound, or of
/// bounds that contradict each other — has no model row to be a multiplier *of*, and a
/// row whose multipliers touch one is not handed out at all. Nothing is lost: such a
/// row is not a statement about the model that `verify` could check, and a cut whose
/// witness cannot be checked is not a cut this project is willing to use.
///|
/// One tableau row, as the combination of model rows that produced it.
pub(all) struct TableauRow {
/// The model variable the row is basic in: the variable the row solves for. Its
/// coefficient in the combination comes out as one.
basic : Int32
/// Its value in the basis these rows were computed from. Reported for the caller's
/// benefit — the rounded row's own right hand side is recomputed from the model, not
/// read off this number, which carries the degeneracy perturbation the run used.
value : Double
/// One entry per model row: the coefficient of that row in the combination.
multipliers : Array[Double]
} derive(Debug)
///|
/// The tableau rows of a solved problem, one per basic model variable.
///
/// The basis has to be one this problem's kernel version produced (`SimplexResult::basis`
/// is the intended source): the rows are read through the basis inverse, and a basis of
/// the wrong shape is refused rather than factored.
pub fn tableau_rows(
model : @model.Model,
options : SimplexOptions,
basis : SimplexBasis,
) -> Result[Array[TableauRow], String] {
match from_model_with(model, true) {
Err(reason) => Err(reason)
Ok(problem) => {
if basis.nrows != problem.nrows || basis.ncols != problem.ncols {
return Err(
"the basis is " +
basis.nrows.to_string() +
" x " +
basis.ncols.to_string() +
" but this model's kernel problem is " +
problem.nrows.to_string() +
" x " +
problem.ncols.to_string(),
)
}
let state = State::new_with_basis(problem, options, basis)
if !state.factorized {
return Err("the basis could not be factorized")
}
// Which model variable each structural column belongs to. A variable with no
// lower bound is two columns (`x = p − n`) and both of them belong to it; the
// combination's coefficients on the two are opposite, so it is still one
// coefficient on one model variable, which is what the checker recomputes.
let variable_of_column : Array[Int32] = Array::make(problem.ncols, -1)
for j = 0; j < problem.var_columns.length(); j = j + 1 {
for part in problem.var_columns[j] {
let (column, _) = part
variable_of_column[column] = j
}
}
let rows : Array[TableauRow] = []
for i = 0; i < state.m; i = i + 1 {
let column = state.basis[i]
if column >= problem.structural {
continue
}
let basic = variable_of_column[column]
if basic < 0 {
continue
}
let weights = state.inverse_row(i)
let mut largest = 0.0
for k = 0; k < state.m; k = k + 1 {
if @core.fabs(weights[k]) > largest {
largest = @core.fabs(weights[k])
}
}
// Weights below the round-off of the vector itself are not part of the row;
// keeping them would put the arithmetic of a zero into the witness.
let floor = largest * 1.0e-12
let multipliers : Array[Double] = Array::make(
model.num_constraints(),
0.0,
)
let mut usable = true
for k = 0; k < state.m; k = k + 1 {
let weight = weights[k]
if @core.fabs(weight) <= floor {
continue
}
let source = problem.row_source[k]
if source < 0 {
usable = false
break
}
multipliers[source] = multipliers[source] +
problem.row_sign[k] * weight
}
if !usable {
continue
}
rows.push({ basic, value: state.xb[i], multipliers, })
}
Ok(rows)
}
}
}