///|
/// Standard-form construction.
///
/// The kernel only understands `min cᵀx, A x = b, l ≤ x ≤ u`. Everything else the
/// model layer allows is mapped onto that here, and each transformation is
/// written down because two of them move the objective constant:
///
/// | model level construct | kernel level transformation |
/// | --- | --- |
/// | `x ≥ lb` with finite `lb` | substitute `x = lb + y` (`y ≥ 0`): the column's coefficients times `lb` are subtracted from the row right hand sides, and `c·lb` is added to the objective constant |
/// | `x ≤ ub` with finite `ub` | **column upper bound** `y ≤ ub − lb` — the kernel's ratio test handles both bounds, so a bound does not cost a row |
/// | `ub < lb` (contradictory bounds) | the old explicit row `Σ sign·y ≤ ub − lb`, whose negative right hand side makes the run report `Infeasible` instead of handing the kernel a negative range |
/// | unbounded below | split `x = p − n` into two non-negative columns with opposite coefficients and costs; if `ub` is finite as well, the pair gets an explicit row `p − n ≤ ub`, because a difference of two columns has no per-column bound |
/// | `a·x ≤ b` | slack column `+1`, non-negative |
/// | `a·x ≥ b` | surplus column `−1` plus an artificial column `+1` |
/// | `a·x = b` | artificial column `+1` |
/// | row with negative right hand side | the whole row is negated and `≤` / `≥` swapped, so every right hand side is non-negative and a slack or artificial basis always exists |
/// | `maximize` | costs and the objective constant are negated; the sign back to the model's sense is kept in `model_sign` |
///
/// Upper bounds used to be rows too, and on `fast0507` that turned 489 model rows
/// into 63 490 kernel rows. The pivot count follows the kernel's row count, not the
/// column count (measured: a covering LP of 20 / 40 / 120 / 240 rows takes
/// 57 / 137 / 632 / 2 220 pivots at any column count), so those 63 009 extra rows
/// were the whole cost of the instance.
///|
/// The standard-form problem handed to the kernel.
pub struct KernelProblem {
nrows : Int32
ncols : Int32
/// Number of leading columns that correspond to model variables; the rest are
/// slack, surplus and artificial columns.
structural : Int32
matrix : @core.SparseMatrix
rhs : Array[Double]
/// Upper bound of every kernel column, in kernel column order. A column's lower
/// bound is zero for all of them (the model's lower bounds were absorbed into
/// `var_offset`), so one array is enough. `@model.infinite_bound` means the column
/// has no upper bound.
///
/// Bounds live here rather than in extra rows: the ratio test reads them both
/// ways, and a nonbasic variable may sit at either end of its range.
upper : Array[Double]
cost : Array[Double]
is_artificial : Array[Bool]
/// Reconstruction of a model variable from kernel columns:
/// `x_j = var_offset[j] + Σ coefficient · y_column`.
var_columns : Array[Array[(Int32, Double)]]
var_offset : Array[Double]
/// `min` sense constant added to the kernel objective.
objective_constant : Double
/// Multiplier back to the model's sense: `model objective = model_sign · kernel objective`.
model_sign : Double
/// Column that starts basic in each row: the slack of a `≤` row, the
/// artificial column of a `≥` or `=` row. Every right hand side is
/// non-negative by construction, so this basis is feasible for Phase I.
initial_basis : Array[Int32]
/// For each kernel row, the model constraint it came from, or `-1` when this
/// package invented the row (the row form of a free variable's upper bound, or of
/// bounds that contradict each other).
///
/// The kernel's duals live on its own rows, so a certificate stated against the
/// model needs this map: a row that is not a model row has no dual to report.
row_source : Array[Int32]
/// Multiplier that turns the kernel's dual for that row into a multiplier of the
/// model row's oriented form, which is the convention certificates use. Zero on an
/// invented row.
///
/// The kernel works on normalized rows (a `≤` row is a slack, a `≥` row a surplus,
/// a negative right hand side negates the row), and each of those choices moves a
/// sign: `y_model = ± y_kernel`. Deriving it here, next to the normalization that
/// caused it, is the only place it can be got right; a reader of a certificate
/// should not have to redo it.
row_dual_factor : Array[Double]
/// `+1` when the kernel row is the model row, `-1` when it is the model row negated
/// because its right hand side was negative.
///
/// Not the same number as `row_dual_factor`, which also folds in the relation's own
/// sign (a slack column moves a dual one way, a surplus the other). That factor is
/// the right one for the *dual* of a row, because a dual is stated against the row's
/// oriented form. A row multiplier used as a coefficient *of the model row* — which
/// is what a cut's witness is, `Σ yᵢ·(row i) = row` — needs this one instead, and
/// reading the factor above for it would put a sign error into every cut taken from a
/// `≤` row.
row_sign : Array[Double]
/// The **model** relation of each kernel row, before the sign normalisation above.
///
/// A certificate's row multipliers carry a sign condition on a `≤` or `≥` row and none at
/// all on an equality, so a producer that measures its own multipliers against that
/// convention has to know which model rows are equalities. `row_dual_factor` cannot
/// answer it — its magnitude is one on every relation — and recovering the relation from
/// the slack column the assembly happened to add would be reading one field for another,
/// which is how a cut witness came to carry a sign error on every `≤` row.
row_relation : Array[@model.Rel]
/// The model's own objective coefficients, in the minimisation sense `verify` reads them
/// in (`sense_sign · c`, the same number `verify`'s `dense_cost` accumulates).
///
/// The kernel's cost vector is not this one: it is stated on kernel columns, carries the
/// negation of a maximisation and the sign of a free variable's split. A certificate is
/// judged against the *model's* numbers, so a self-check that stands in for the checker has
/// to measure them — `simplex/certificate.mbt::duality_gap_violation` does, and this is
/// where it gets them. Carrying them is the point: re-deriving a model coefficient from the
/// normalized image means composing sign flips that the model layer already made once, and
/// a producer that measures a *reconstruction* of the checker's input is measuring a
/// different thing, which is a mistake this project has made three times.
model_cost : Array[Double]
/// The model's variable lower bounds, `lⱼ` — not the kernel's, which are all zero because
/// the model's were absorbed into `var_offset`.
///
/// The duality gap's box minimum is taken over each variable's *own* range, so recovering
/// `lⱼ` as `var_offset[j]` is right for an ordinary variable and wrong for a variable the
/// kernel split in two (where the offset is zero and the bound is a row).
model_lower : Array[Double]
/// The model's variable upper bounds, `uⱼ`, with `@model.infinite_bound` where there is
/// none. `upper` is the kernel's column bound, which is `uⱼ − lⱼ` for an ordinary variable
/// and absent for a split one; this is the number the checker multiplies a reduced cost by.
model_upper : Array[Double]
/// The model's own right hand sides, one per model row, in model row order.
///
/// `rhs` is the normalized kernel right hand side, which has the row's sign and every
/// variable offset folded into it, so the model's `bᵢ` cannot be read back out of it without
/// re-summing `Σⱼ aᵢⱼ·lⱼ` in an order the model layer did not use.
model_rhs : Array[Double]
} derive(Debug)
///|
/// Convenience view of the kernel solution for the first `structural` columns.
pub fn KernelProblem::solution(
self : KernelProblem,
kernel_values : Array[Double],
) -> Array[Double] {
let out : Array[Double] = []
for j = 0; j < self.var_columns.length(); j = j + 1 {
let mut value = self.var_offset[j]
let parts = self.var_columns[j]
for k = 0; k < parts.length(); k = k + 1 {
let (column, coefficient) = parts[k]
value = value + coefficient * kernel_values[column]
}
out.push(value)
}
out
}
///|
/// `true` when the value is the negative infinity sentinel of the model layer.
fn is_unbounded_below(value : Double) -> Bool {
value <= -1.0e29
}
///|
/// `true` when the value is the positive infinity sentinel of the model layer.
fn is_unbounded_above(value : Double) -> Bool {
value >= 1.0e29
}
///|
/// `true` when a kernel column has an upper bound this kernel has to respect.
/// Shared with the iteration loop, which asks the same question of every column it
/// prices and of every basic column it limits.
fn kernel_has_upper(value : Double) -> Bool {
!is_unbounded_above(value)
}
///|
/// The multiplier that turns the kernel's dual for a row into a multiplier of the
/// model row's oriented form (the convention `verify` documents), or zero when the
/// row is not a model row.
///
/// `sign` is `-1` when the kernel row is the negation of the model row. Starting from
/// the identity that makes both sides describe the same reduced costs,
/// `cⱼ − y_kernel·(sign·aᵢⱼ) = cⱼ + y_model·aᵢⱼ` for a `≤` model row and
/// `cⱼ − y_model·aᵢⱼ` for a `≥` or `=` model row, gives the table below. The kernel's
/// convention for its own rows (a slack forces `y ≤ 0` on a `≤` row, a surplus
/// `y ≥ 0` on a `≥` one) is what makes the two agree rather than cancel.
fn dual_factor(source : Int32, rel : @model.Rel, sign : Double) -> Double {
if source < 0 {
0.0
} else {
match rel {
@model.Rel::LessEqual => -sign
@model.Rel::GreaterEqual => sign
@model.Rel::Equal => sign
}
}
}
///|
/// Builds the kernel problem for `min cᵀx` subject to `rows` and `x ≥ 0`.
///
/// This is the shape the reference solver in `oracle` accepts as well, which is
/// what makes differential testing between the two possible.
pub fn KernelProblem::from_standard(
num_vars : Int32,
cost : Array[Double],
rows : Array[(Array[Double], @model.Rel, Double)],
) -> KernelProblem {
// Model variables are the first `num_vars` columns, one column each.
let var_columns : Array[Array[(Int32, Double)]] = []
let var_offset : Array[Double] = []
let structural_upper : Array[Double] = []
// The standard form has no model layer of its own: its variables are non-negative with no
// upper bound, so the model `verify` is handed for a result of this entry point is exactly
// that. (`oracle/dense_tableau.mbt` solves the same shape, which is what makes the two
// comparable.)
let model_lower : Array[Double] = []
let model_upper : Array[Double] = []
for j = 0; j < num_vars; j = j + 1 {
var_columns.push([(j, 1.0)])
var_offset.push(0.0)
structural_upper.push(@model.infinite_bound)
model_lower.push(0.0)
model_upper.push(@model.infinite_bound)
}
let row_entries : Array[Array[(Int32, Double)]] = []
let row_rels : Array[@model.Rel] = []
let row_rhs : Array[Double] = []
// The standard-form entry point has no model rows: its rows *are* the caller's.
let row_sources : Array[Int32] = []
for i = 0; i < rows.length(); i = i + 1 {
let (coefficients, rel, rhs) = rows[i]
let entries : Array[(Int32, Double)] = []
for j = 0; j < num_vars && j < coefficients.length(); j = j + 1 {
if coefficients[j] != 0.0 {
entries.push((j, coefficients[j]))
}
}
row_entries.push(entries)
row_rels.push(rel)
row_rhs.push(rhs)
row_sources.push(i)
}
assemble(
num_vars, cost, 0.0, 1.0, var_columns, var_offset, structural_upper, row_entries,
row_rels, row_rhs, row_sources, cost, model_lower, model_upper, row_rhs,
)
}
///|
/// Builds the kernel problem for a model, applying the transformations listed in
/// the package comment. Returns the reason when the model uses something the
/// kernel deliberately does not accept (integer variables until the branch and
/// bound stage).
pub fn KernelProblem::from_model(
model : @model.Model,
) -> Result[KernelProblem, String] {
from_model_with(model, false)
}
///|
/// Same as `from_model`, with the option of treating integer variables as
/// continuous: the LP relaxation of a mixed integer model.
pub fn from_model_with(
model : @model.Model,
relax_integrality : Bool,
) -> Result[KernelProblem, String] {
let nvars = model.num_vars()
// The multiplier that turns the model's sense into the minimisation the kernel solves, and
// the same number `verify`'s `sense_sign` returns: a certificate is stated in the model's
// sense, so both halves of it need this one.
let model_sign = if model.sense() == @model.Sense::Maximize {
-1.0
} else {
1.0
}
// Pass 1: decide a kernel representation for every model variable.
let var_columns : Array[Array[(Int32, Double)]] = []
let var_offset : Array[Double] = []
// Upper bound of each structural column, in column order. Only the explicit row
// case (below) leaves it at infinity.
let structural_upper : Array[Double] = []
// The model's own bounds, kept next to the kernel's, because the duality gap is stated
// over the model's box: a reduced cost points at `lⱼ` or at `uⱼ`, and for a split
// variable neither is a kernel column bound or an offset.
let model_lower : Array[Double] = []
let model_upper : Array[Double] = []
let mut next_column = 0
// Row form of an upper bound, used only where a column bound cannot express it:
// a split pair `p − n ≤ ub`, or bounds that contradict each other.
let bound_rows : Array[(Array[(Int32, Double)], Double)] = []
for j = 0; j < nvars; j = j + 1 {
let variable = model.get_var(j)
model_lower.push(variable.lb)
model_upper.push(variable.ub)
if variable.is_int && !relax_integrality {
return Err(
"integer variables are not supported by the linear kernel (planned for the branch and bound stage); use SimplexOptions::relaxed() for the LP relaxation",
)
}
if is_unbounded_below(variable.lb) {
// Split into a positive and a negative part: x = p - n, p, n >= 0.
let positive = next_column
let negative = next_column + 1
next_column = next_column + 2
var_columns.push([(positive, 1.0), (negative, -1.0)])
var_offset.push(0.0)
structural_upper.push(@model.infinite_bound)
structural_upper.push(@model.infinite_bound)
if !is_unbounded_above(variable.ub) {
// A difference of two columns has no per-column bound, so this one stays a
// row. Splits are the rare case; bounds are the common one.
bound_rows.push(([(positive, 1.0), (negative, -1.0)], variable.ub))
}
} else {
let column = next_column
next_column = next_column + 1
var_columns.push([(column, 1.0)])
var_offset.push(variable.lb)
if is_unbounded_above(variable.ub) {
structural_upper.push(@model.infinite_bound)
} else if variable.ub >= variable.lb {
// The ordinary case: `x ≤ ub` becomes the column bound `y ≤ ub − lb`.
structural_upper.push(variable.ub - variable.lb)
} else {
// Contradictory bounds are not a negative range to hand the kernel — they
// are an infeasibility to prove. Keeping the row form means the run still
// reports it, with the same right hand side it always did.
structural_upper.push(@model.infinite_bound)
bound_rows.push(([(column, 1.0)], variable.ub - variable.lb))
}
}
}
let structural = next_column
// Pass 2: rows in kernel-column space. A model row `Σ aⱼ xⱼ rel b` becomes
// `Σ aⱼ (offsetⱼ + Σ sign·y) rel b`, so the offsets move to the right hand
// side and each variable contributes one entry per kernel column.
let row_entries : Array[Array[(Int32, Double)]] = []
let row_rels : Array[@model.Rel] = []
let row_rhs : Array[Double] = []
// Which model row each kernel row came from; the rows this package adds below are
// not model rows and get -1.
let row_sources : Array[Int32] = []
// The model's own right hand sides, the numbers a certificate's multipliers are paired
// with. `row_rhs` below is the kernel's: the same row with every variable offset already
// moved to the other side, which is not a number the checker ever sees.
let model_rhs : Array[Double] = []
let mut objective_constant = 0.0
// The model's objective in the minimisation sense `verify` reads it in. Accumulated in the
// model's own term order, which is the order `verify`'s `dense_cost` accumulates in, so the
// two agree digit for digit rather than to within a re-summation.
let model_cost : Array[Double] = Array::make(nvars, 0.0)
let model_objective = model.objective()
for i = 0; i < model.num_constraints(); i = i + 1 {
let constraint = model.constraint(i)
let entries : Array[(Int32, Double)] = []
let mut adjusted_rhs = constraint.rhs
for k = 0; k < constraint.terms.length(); k = k + 1 {
let (variable_index, coefficient) = constraint.terms[k]
if variable_index < 0 || variable_index >= nvars {
return Err(
"constraint " +
constraint.name +
" refers to a variable that does not exist",
)
}
adjusted_rhs = adjusted_rhs - coefficient * var_offset[variable_index]
let parts = var_columns[variable_index]
for p = 0; p < parts.length(); p = p + 1 {
let (column, sign) = parts[p]
entries.push((column, coefficient * sign))
}
}
row_entries.push(entries)
row_rels.push(constraint.rel)
row_sources.push(i)
model_rhs.push(constraint.rhs)
row_rhs.push(adjusted_rhs)
}
for k = 0; k < model_objective.length(); k = k + 1 {
let (variable_index, coefficient) = model_objective[k]
objective_constant = objective_constant +
coefficient * var_offset[variable_index]
model_cost[variable_index] = model_cost[variable_index] +
model_sign * coefficient
}
for b = 0; b < bound_rows.length(); b = b + 1 {
let (entries, ub) = bound_rows[b]
row_entries.push(entries)
row_rels.push(@model.Rel::LessEqual)
row_rhs.push(ub)
row_sources.push(-1)
}
// Pass 3: objective in kernel columns, negated for a maximisation.
let kernel_cost : Array[Double] = Array::make(structural, 0.0)
for k = 0; k < model_objective.length(); k = k + 1 {
let (variable_index, coefficient) = model_objective[k]
let parts = var_columns[variable_index]
for p = 0; p < parts.length(); p = p + 1 {
let (column, sign) = parts[p]
kernel_cost[column] = kernel_cost[column] +
model_sign * coefficient * sign
}
}
Ok(
assemble(
structural,
kernel_cost,
model_sign * objective_constant,
model_sign,
var_columns,
var_offset,
structural_upper,
row_entries,
row_rels,
row_rhs,
row_sources,
model_cost,
model_lower,
model_upper,
model_rhs,
),
)
}
///|
/// Shared assembly: normalises row signs, adds slack / surplus / artificial
/// columns, and packs the matrix in CSC form.
///
/// `structural_upper` gives the upper bound of each structural column; the columns
/// this function adds (slacks, surpluses, artificials) are all unbounded above,
/// which is what a slack variable is.
///
/// The four `model_*` parameters are the model layer's own numbers, carried through
/// unchanged because a certificate is stated against the model and never against this
/// normal form. They are documented field by field on `KernelProblem`.
fn assemble(
structural : Int32,
structural_cost : Array[Double],
objective_constant : Double,
model_sign : Double,
var_columns : Array[Array[(Int32, Double)]],
var_offset : Array[Double],
structural_upper : Array[Double],
row_entries : Array[Array[(Int32, Double)]],
row_rels : Array[@model.Rel],
row_rhs : Array[Double],
row_sources : Array[Int32],
model_cost : Array[Double],
model_lower : Array[Double],
model_upper : Array[Double],
model_rhs : Array[Double],
) -> KernelProblem {
let m = row_entries.length()
// Row sign normalisation: every right hand side becomes non-negative, which is
// what guarantees a starting basis of slacks and artificials. The sign is kept so a
// dual solution can be mapped back: a negated row's multiplier is the negation of
// the model row's.
let rels : Array[@model.Rel] = []
let rhs : Array[Double] = []
let rows : Array[Array[(Int32, Double)]] = []
let dual_factors : Array[Double] = []
let signs : Array[Double] = []
for i = 0; i < m; i = i + 1 {
let rel = row_rels[i]
let value = row_rhs[i]
if value < 0.0 {
let flipped = row_entries[i]
let negated : Array[(Int32, Double)] = []
for k = 0; k < flipped.length(); k = k + 1 {
let (column, coefficient) = flipped[k]
negated.push((column, -coefficient))
}
rows.push(negated)
rhs.push(-value)
dual_factors.push(dual_factor(row_sources[i], rel, -1.0))
signs.push(-1.0)
rels.push(
match rel {
@model.Rel::LessEqual => @model.Rel::GreaterEqual
@model.Rel::GreaterEqual => @model.Rel::LessEqual
@model.Rel::Equal => @model.Rel::Equal
},
)
} else {
rows.push(row_entries[i])
rhs.push(value)
dual_factors.push(dual_factor(row_sources[i], rel, 1.0))
signs.push(1.0)
rels.push(rel)
}
}
// Extra columns: one per row (slack) plus surplus and artificial where needed.
let extra_columns : Array[Array[(Int32, Double)]] = []
let extra_cost : Array[Double] = []
let extra_artificial : Array[Bool] = []
// For each row: the column that starts basic (slack, or artificial when the
// row is an equality or a `≥` row) and the surplus column where one exists.
let initial_basis_column : Array[Int32] = []
for i = 0; i < m; i = i + 1 {
let rel = rels[i]
if rel == @model.Rel::LessEqual {
initial_basis_column.push(structural + extra_columns.length())
extra_columns.push([(i, 1.0)])
extra_cost.push(0.0)
extra_artificial.push(false)
} else if rel == @model.Rel::GreaterEqual {
extra_columns.push([(i, -1.0)])
extra_cost.push(0.0)
extra_artificial.push(false)
initial_basis_column.push(structural + extra_columns.length())
extra_columns.push([(i, 1.0)])
extra_cost.push(0.0)
extra_artificial.push(true)
} else {
initial_basis_column.push(structural + extra_columns.length())
extra_columns.push([(i, 1.0)])
extra_cost.push(0.0)
extra_artificial.push(true)
}
}
// Column major packing: structural columns come from the row lists.
let ncols = structural + extra_columns.length()
let columns : Array[Array[(Int32, Double)]] = []
for index = 0; index < ncols; index = index + 1 {
columns.push([])
}
for i = 0; i < m; i = i + 1 {
let entries = rows[i]
for k = 0; k < entries.length(); k = k + 1 {
let (column, coefficient) = entries[k]
columns[column].push((i, coefficient))
}
}
for k = 0; k < extra_columns.length(); k = k + 1 {
let entries = extra_columns[k]
for p = 0; p < entries.length(); p = p + 1 {
let (row, coefficient) = entries[p]
columns[structural + k].push((row, coefficient))
}
}
let cost : Array[Double] = []
let is_artificial : Array[Bool] = []
for j = 0; j < structural; j = j + 1 {
cost.push(structural_cost[j])
is_artificial.push(false)
}
for k = 0; k < extra_columns.length(); k = k + 1 {
cost.push(extra_cost[k])
is_artificial.push(extra_artificial[k])
}
// Bounds in kernel column order: the structural ones the caller supplied, then
// one unbounded entry per column added above.
let upper : Array[Double] = []
for j = 0; j < structural; j = j + 1 {
upper.push(structural_upper[j])
}
for k = 0; k < extra_columns.length(); k = k + 1 {
upper.push(@model.infinite_bound)
}
{
nrows: m,
ncols,
structural,
matrix: @core.SparseMatrix::from_columns(m, columns),
rhs,
upper,
cost,
is_artificial,
var_columns,
var_offset,
objective_constant,
model_sign,
initial_basis: initial_basis_column,
row_source: row_sources,
row_dual_factor: dual_factors,
row_sign: signs,
row_relation: row_rels,
model_cost,
model_lower,
model_upper,
model_rhs,
}
}
///|
/// Column index of the slack (or surplus, for `≥` rows) of row `i`. Used by the
/// kernel to pick a starting basis.
pub fn KernelProblem::slack_row_map(self : KernelProblem) -> Array[Int32] {
let m = self.nrows
let map : Array[Int32] = Array::make(m, -1)
for j = self.structural; j < self.ncols; j = j + 1 {
let entries = self.matrix.column(j)
if entries.length() == 1 && !self.is_artificial[j] {
map[entries[0].0] = j
}
}
map
}
///|
/// Identifies the kernel layout: everything about a problem that decides which column
/// an index names — the starting column of each row (which pins where the extra columns
/// fell), each variable's column split, the rows' relations and their normalisation
/// signs. Two problems built from models that differ only in bounds can differ here
/// (see `SimplexBasis::layout` for the measurement), and a basis may only be loaded
/// into the problem whose fingerprint it carries.
fn KernelProblem::layout_fingerprint(self : KernelProblem) -> String {
let mut text = ""
text = text +
self.nrows.to_string() +
";" +
self.ncols.to_string() +
";" +
self.structural.to_string() +
";"
for i = 0; i < self.nrows; i = i + 1 {
text = text + self.initial_basis[i].to_string() + ","
}
text = text + ";"
for j = 0; j < self.var_columns.length(); j = j + 1 {
text = text + self.var_columns[j].length().to_string() + ","
}
text = text + ";"
for i = 0; i < self.nrows; i = i + 1 {
let rel = if self.row_relation[i] == @model.Rel::LessEqual {
"L"
} else if self.row_relation[i] == @model.Rel::GreaterEqual {
"G"
} else {
"E"
}
text = text + rel + (if self.row_sign[i] < 0.0 { "-" } else { "+" }) + ","
}
text
}