Skip to main content

simplex_trace/
solver.rs

1use num_traits::{One, Signed, Zero};
2
3use crate::error::Error;
4use crate::num::{Coef, Rational};
5use crate::problem::{Problem, Relation, Sense};
6use crate::sensitivity::{self, Cells, Sensitivity, StandardForm};
7use crate::trace::{
8    DualRatioTest, NamedValue, Phase, RatioTest, Snapshot, Step, Termination, Trace,
9};
10
11/// Which variant of the simplex method to run.
12///
13/// More methods (such as the dual simplex) may be added in minor releases.
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum Method {
19    /// Primal simplex. Only for problems whose constraints are all `<=` with a non-negative
20    /// right-hand side after normalization (the slacks give the starting basis).
21    Simplex,
22    /// Big-M method, with `M` kept symbolic (no numeric value is ever chosen).
23    BigM,
24    /// Two-phase method. Falls back to a single phase when no artificial variable is needed.
25    TwoPhase,
26    /// Dual simplex. Every constraint is written as `<=` (a `>=` is multiplied by `-1`), so
27    /// the slacks give a starting basis that may be infeasible (negative right-hand sides)
28    /// but must be dual feasible: non-negative costs when minimizing, non-positive when
29    /// maximizing, and no equality constraints. Otherwise
30    /// [`Error::NotDualFeasible`](crate::Error::NotDualFeasible) is returned.
31    DualSimplex,
32}
33
34/// Rule for choosing the entering column.
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum PivotRule {
40    /// The textbook rule: the most negative `z_j - c_j` when maximizing, the most positive
41    /// when minimizing. Ties go to the lowest column index. In the dual simplex, the row with
42    /// the most negative right-hand side leaves.
43    Dantzig,
44    /// The lowest-index column that improves the objective. Slower, but never cycles. In the
45    /// dual simplex, the negative row whose basic variable has the lowest index leaves.
46    Bland,
47}
48
49/// Solver settings.
50///
51/// ```
52/// use simplex_trace::{Options, PivotRule};
53///
54/// let options = Options::default().pivot_rule(PivotRule::Bland).max_iterations(500);
55/// assert_eq!(options.pivot_rule, PivotRule::Bland);
56/// ```
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58#[cfg_attr(feature = "serde", serde(default))]
59#[derive(Clone, Debug, PartialEq, Eq)]
60#[non_exhaustive]
61pub struct Options {
62    pub pivot_rule: PivotRule,
63    /// Maximum number of pivots per phase before giving up with [`Error::IterationLimit`].
64    pub max_iterations: usize,
65}
66
67impl Default for Options {
68    fn default() -> Self {
69        Options {
70            pivot_rule: PivotRule::Dantzig,
71            max_iterations: 10_000,
72        }
73    }
74}
75
76impl Options {
77    pub fn pivot_rule(mut self, rule: PivotRule) -> Self {
78        self.pivot_rule = rule;
79        self
80    }
81
82    pub fn max_iterations(mut self, n: usize) -> Self {
83        self.max_iterations = n;
84        self
85    }
86}
87
88/// Final result of a solve.
89///
90/// With the `serde` feature: `{"status": "optimal", "objective": "17/5", "values": ["2/5", "9/5"]}`,
91/// `{"status": "infeasible"}` or `{"status": "unbounded"}`.
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "snake_case"))]
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub enum Status {
96    /// `values` holds one entry per original variable; `objective` is in the problem's own sense.
97    Optimal {
98        #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
99        objective: Rational,
100        #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::vec"))]
101        values: Vec<Rational>,
102    },
103    Infeasible,
104    Unbounded,
105}
106
107/// The status, the problem that was solved, the sensitivity analysis (when optimal) and the
108/// full step-by-step trace.
109///
110/// With the `serde` feature the status fields sit at the top level:
111/// `{"status": "optimal", "objective": "17/5", "values": [...], "problem": {...},
112/// "sensitivity": {...}, "trace": {...}}`.
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114#[derive(Clone, Debug, PartialEq, Eq)]
115#[non_exhaustive]
116pub struct Solution {
117    #[cfg_attr(feature = "serde", serde(flatten))]
118    pub status: Status,
119    /// The problem this solution answers, so that [`reoptimize`] can extend it.
120    pub problem: Problem,
121    /// Shadow prices, reduced costs and ranges, computed for every optimal solution.
122    ///
123    /// `None` exactly when the status is not optimal.
124    #[cfg_attr(
125        feature = "serde",
126        serde(default, skip_serializing_if = "Option::is_none")
127    )]
128    pub sensitivity: Option<Sensitivity>,
129    pub trace: Trace,
130}
131
132/// Solves `problem` with the given method, recording every step.
133///
134/// ```
135/// use simplex_trace::{solve, Method, Options, Problem, Relation, Status, ratio};
136///
137/// let problem = Problem::maximize([3, 5])
138///     .subject_to([1, 0], Relation::Le, 4)
139///     .subject_to([0, 2], Relation::Le, 12)
140///     .subject_to([3, 2], Relation::Le, 18);
141///
142/// let solution = solve(&problem, Method::Simplex, &Options::default()).unwrap();
143/// assert_eq!(
144///     solution.status,
145///     Status::Optimal { objective: ratio(36, 1), values: vec![ratio(2, 1), ratio(6, 1)] }
146/// );
147/// println!("{}", solution.trace);
148/// ```
149pub fn solve(problem: &Problem, method: Method, options: &Options) -> Result<Solution, Error> {
150    problem.validate()?;
151    let mut trace = Trace::new(problem.sense);
152    let sense = problem.sense;
153    let costs = &problem.objective;
154
155    if method == Method::DualSimplex {
156        check_dual_feasible(problem)?;
157        let mut t = Tableau::dual_form(problem);
158        t.set_objective(sense, "z", costs, &Coef::zero());
159        let end = t.optimize_dual(options, &mut trace)?;
160        return Ok(finish(&t, problem, end, trace));
161    }
162
163    let mut t = Tableau::standard_form(problem);
164    let end = match method {
165        Method::Simplex if t.has_artificials() => return Err(Error::ArtificialVariablesRequired),
166        Method::Simplex => t.single_phase(sense, costs, Coef::zero(), options, &mut trace)?,
167        Method::TwoPhase if !t.has_artificials() => {
168            t.single_phase(sense, costs, Coef::zero(), options, &mut trace)?
169        }
170        Method::TwoPhase => t.two_phase(sense, costs, options, &mut trace)?,
171        Method::BigM => {
172            // The penalty always works against the objective: -M when maximizing, +M when minimizing.
173            let penalty = match sense {
174                Sense::Maximize => Coef::big_m(-Rational::one()),
175                Sense::Minimize => Coef::big_m(Rational::one()),
176            };
177            t.single_phase(sense, costs, penalty, options, &mut trace)?
178        }
179        Method::DualSimplex => unreachable!("handled above"),
180    };
181    Ok(finish(&t, problem, end, trace))
182}
183
184/// Re-optimizes a solved problem after appending constraints to it, with the dual simplex.
185///
186/// `problem` must be `previous.problem` plus zero or more `<=` / `>=` constraints at the end,
187/// and `previous` must be optimal. The trace starts from the previous final tableau (without
188/// artificial columns) with the new rows added, each expressed in the current basis with its
189/// own slack basic ([`Step::ConstraintAdded`]), and continues with dual simplex pivots until
190/// the tableau is feasible again or a row proves the extended problem infeasible.
191///
192/// ```
193/// use simplex_trace::{reoptimize, solve, Method, Options, Problem, Relation, Status, ratio};
194///
195/// // TOYCO (Taha): max 3x1 + 2x2 + 5x3, optimal at z = 1350.
196/// let toyco = Problem::maximize([3, 2, 5])
197///     .subject_to([1, 2, 1], Relation::Le, 430)
198///     .subject_to([3, 0, 2], Relation::Le, 460)
199///     .subject_to([1, 4, 0], Relation::Le, 420);
200/// let first = solve(&toyco, Method::Simplex, &Options::default()).unwrap();
201///
202/// // A new constraint the current optimum violates: one dual pivot fixes it.
203/// let extended = toyco.subject_to([3, 3, 1], Relation::Le, 500);
204/// let second = reoptimize(&first, &extended, &Options::default()).unwrap();
205/// assert_eq!(
206///     second.status,
207///     Status::Optimal { objective: ratio(1330, 1), values: vec![ratio(0, 1), ratio(90, 1), ratio(230, 1)] }
208/// );
209/// assert_eq!(second.trace.pivot_count(), 1);
210/// ```
211pub fn reoptimize(
212    previous: &Solution,
213    problem: &Problem,
214    options: &Options,
215) -> Result<Solution, Error> {
216    problem.validate()?;
217    let old = &previous.problem;
218    let (Status::Optimal { .. }, Some(sensitivity)) = (&previous.status, &previous.sensitivity)
219    else {
220        return Err(Error::NotOptimal);
221    };
222    let m = old.constraints.len();
223    let same_prefix = problem.sense == old.sense
224        && problem.objective == old.objective
225        && problem.var_names == old.var_names
226        && problem.constraints.len() >= m
227        && problem.constraints[..m] == old.constraints[..];
228    if !same_prefix {
229        return Err(Error::InvalidProblem(
230            "the extended problem must be the previous problem with constraints appended".into(),
231        ));
232    }
233    for (i, c) in problem.constraints.iter().enumerate().skip(m) {
234        if c.relation == Relation::Eq {
235            return Err(Error::NotDualFeasible(format!(
236                "constraint {} is an equality; added constraints must be <= or >=",
237                i + 1
238            )));
239        }
240    }
241
242    let mut trace = Trace::new(problem.sense);
243    let mut t = Tableau::from_basis(old, sensitivity)?;
244    t.append_constraints(problem, m, &mut trace);
245    let end = t.optimize_dual(options, &mut trace)?;
246    Ok(finish(&t, problem, end, trace))
247}
248
249/// The dual simplex needs the slack basis to be dual feasible: the starting objective row is
250/// `-c_j`, which has the optimal sign exactly when every cost has the right sign.
251fn check_dual_feasible(problem: &Problem) -> Result<(), Error> {
252    for (i, c) in problem.constraints.iter().enumerate() {
253        if c.relation == Relation::Eq {
254            return Err(Error::NotDualFeasible(format!(
255                "constraint {} is an equality, which has no slack",
256                i + 1
257            )));
258        }
259    }
260    let (wrong, needs) = match problem.sense {
261        Sense::Maximize => (Signed::is_positive as fn(&Rational) -> bool, "non-positive"),
262        Sense::Minimize => (Signed::is_negative as fn(&Rational) -> bool, "non-negative"),
263    };
264    if let Some((j, c)) = problem.objective.iter().enumerate().find(|(_, c)| wrong(c)) {
265        return Err(Error::NotDualFeasible(format!(
266            "{} has cost {c}, but the costs must be {needs} when {}",
267            problem.var_name(j),
268            match problem.sense {
269                Sense::Maximize => "maximizing",
270                Sense::Minimize => "minimizing",
271            }
272        )));
273    }
274    Ok(())
275}
276
277/// Builds the solution from the final tableau: status, sensitivity (if optimal) and the
278/// closing step of the trace.
279fn finish(t: &Tableau, problem: &Problem, end: End, mut trace: Trace) -> Solution {
280    let sensitivity = match end {
281        End::Optimal => t.sensitivity(problem),
282        _ => None,
283    };
284    let (status, termination) = match end {
285        End::Optimal => {
286            let status = Status::Optimal {
287                objective: t.z_rhs.c.clone(),
288                values: t.values(problem.num_vars()),
289            };
290            (status, Termination::Optimal)
291        }
292        End::Infeasible => (Status::Infeasible, Termination::Infeasible),
293        End::Unbounded => (Status::Unbounded, Termination::Unbounded),
294    };
295    trace.push(Step::Finished { termination });
296    Solution {
297        status,
298        problem: problem.clone(),
299        sensitivity,
300        trace,
301    }
302}
303
304enum End {
305    Optimal,
306    Infeasible,
307    Unbounded,
308}
309
310#[derive(Clone)]
311struct Tableau {
312    /// Sense of the objective currently in the `z` row (phase 1 is always `Minimize`).
313    sense: Sense,
314    objective_label: &'static str,
315    names: Vec<String>,
316    artificial: Vec<bool>,
317    /// For each artificial column, the constraint it was added to.
318    artificial_owner: Vec<Option<usize>>,
319    /// Redundant equalities whose row was removed (by constraint index).
320    removed: Vec<usize>,
321    rows: Vec<Vec<Rational>>,
322    rhs: Vec<Rational>,
323    basis: Vec<usize>,
324    z: Vec<Coef>,
325    z_rhs: Coef,
326}
327
328impl Tableau {
329    /// Normalizes every row to a non-negative right-hand side and adds one slack (`<=`),
330    /// surplus (`>=`) and/or artificial (`>=`, `=`) column per constraint.
331    /// Column order: original variables, slack/surplus, artificial.
332    fn standard_form(problem: &Problem) -> Self {
333        let n = problem.num_vars();
334        let normalized: Vec<(Vec<Rational>, Relation, Rational)> = problem
335            .constraints
336            .iter()
337            .map(|c| {
338                if c.rhs.is_negative() {
339                    let flipped = match c.relation {
340                        Relation::Le => Relation::Ge,
341                        Relation::Ge => Relation::Le,
342                        Relation::Eq => Relation::Eq,
343                    };
344                    (
345                        c.coeffs.iter().map(|v| -v.clone()).collect(),
346                        flipped,
347                        -c.rhs.clone(),
348                    )
349                } else {
350                    (c.coeffs.clone(), c.relation, c.rhs.clone())
351                }
352            })
353            .collect();
354
355        let mut names: Vec<String> = (0..n).map(|j| problem.var_name(j)).collect();
356        let mut slack_col = vec![None; normalized.len()];
357        for (i, (_, relation, _)) in normalized.iter().enumerate() {
358            if *relation != Relation::Eq {
359                slack_col[i] = Some(names.len());
360                names.push(format!("s{}", i + 1));
361            }
362        }
363        let first_artificial = names.len();
364        let mut art_col = vec![None; normalized.len()];
365        let mut artificial_owner = vec![None; first_artificial];
366        for (i, (_, relation, _)) in normalized.iter().enumerate() {
367            if *relation != Relation::Le {
368                art_col[i] = Some(names.len());
369                artificial_owner.push(Some(i));
370                names.push(format!("a{}", i + 1));
371            }
372        }
373
374        let width = names.len();
375        let mut rows = Vec::with_capacity(normalized.len());
376        let mut rhs = Vec::with_capacity(normalized.len());
377        let mut basis = Vec::with_capacity(normalized.len());
378        for (i, (coeffs, relation, b)) in normalized.into_iter().enumerate() {
379            let mut row = vec![Rational::zero(); width];
380            row[..n].clone_from_slice(&coeffs);
381            if let Some(s) = slack_col[i] {
382                row[s] = if relation == Relation::Le {
383                    Rational::one()
384                } else {
385                    -Rational::one()
386                };
387            }
388            if let Some(a) = art_col[i] {
389                row[a] = Rational::one();
390            }
391            basis.push(
392                art_col[i]
393                    .or(slack_col[i])
394                    .expect("every row has a basic column"),
395            );
396            rows.push(row);
397            rhs.push(b);
398        }
399
400        Tableau {
401            sense: problem.sense,
402            objective_label: "z",
403            artificial: (0..width).map(|j| j >= first_artificial).collect(),
404            artificial_owner,
405            removed: Vec::new(),
406            names,
407            rows,
408            rhs,
409            basis,
410            z: vec![Coef::zero(); width],
411            z_rhs: Coef::zero(),
412        }
413    }
414
415    /// The dual simplex start: every constraint written as `<=` (a `>=` is multiplied by `-1`,
416    /// whatever the sign of its right-hand side) with a slack, so the slacks form the basis.
417    /// Right-hand sides may be negative. Equalities must have been rejected before.
418    fn dual_form(problem: &Problem) -> Self {
419        let n = problem.num_vars();
420        let m = problem.constraints.len();
421        let width = n + m;
422        let mut names: Vec<String> = (0..n).map(|j| problem.var_name(j)).collect();
423        names.extend((0..m).map(|i| format!("s{}", i + 1)));
424
425        let mut rows = Vec::with_capacity(m);
426        let mut rhs = Vec::with_capacity(m);
427        for (i, c) in problem.constraints.iter().enumerate() {
428            let mut row = vec![Rational::zero(); width];
429            let (coeffs, b) = match c.relation {
430                Relation::Le => (c.coeffs.clone(), c.rhs.clone()),
431                Relation::Ge => (
432                    c.coeffs.iter().map(|v| -v.clone()).collect(),
433                    -c.rhs.clone(),
434                ),
435                Relation::Eq => unreachable!("rejected by check_dual_feasible"),
436            };
437            row[..n].clone_from_slice(&coeffs);
438            row[n + i] = Rational::one();
439            rows.push(row);
440            rhs.push(b);
441        }
442
443        Tableau {
444            sense: problem.sense,
445            objective_label: "z",
446            artificial: vec![false; width],
447            artificial_owner: vec![None; width],
448            removed: Vec::new(),
449            names,
450            rows,
451            rhs,
452            basis: (n..width).collect(),
453            z: vec![Coef::zero(); width],
454            z_rhs: Coef::zero(),
455        }
456    }
457
458    /// Rebuilds the final tableau of an optimal solution of `problem` from its basis: the
459    /// real columns of the standard form (artificial columns dropped), without the rows of
460    /// redundant equalities, pivoted to `sensitivity.basic_variables` in that row order.
461    fn from_basis(problem: &Problem, sensitivity: &Sensitivity) -> Result<Self, Error> {
462        let mismatch =
463            || Error::InvalidProblem("the previous solution does not belong to its problem".into());
464        let mut t = Tableau::standard_form(problem);
465        let real = t
466            .artificial
467            .iter()
468            .position(|&a| a)
469            .unwrap_or(t.names.len());
470        t.names.truncate(real);
471        t.artificial.truncate(real);
472        t.artificial_owner.truncate(real);
473        for row in t.rows.iter_mut() {
474            row.truncate(real);
475        }
476        t.z = vec![Coef::zero(); real];
477
478        if sensitivity.constraints.len() != t.rows.len() {
479            return Err(mismatch());
480        }
481        t.removed = (0..t.rows.len())
482            .filter(|&i| sensitivity.constraints[i].redundant)
483            .collect();
484        for &i in t.removed.iter().rev() {
485            t.rows.remove(i);
486            t.rhs.remove(i);
487        }
488
489        let basis: Vec<usize> = sensitivity
490            .basic_variables
491            .iter()
492            .map(|name| t.names.iter().position(|n| n == name).ok_or_else(mismatch))
493            .collect::<Result<_, _>>()?;
494        if basis.len() != t.rows.len() {
495            return Err(mismatch());
496        }
497        // Gauss-Jordan: make each basic column a unit vector, one unassigned row at a time
498        // (the basis matrix is nonsingular, so a usable row always exists), then put the
499        // rows in basis order so the tableau reads like the one in the trace.
500        let mut assigned = vec![false; t.rows.len()];
501        let mut row_of = Vec::with_capacity(basis.len());
502        for &j in &basis {
503            let i = (0..t.rows.len())
504                .find(|&i| !assigned[i] && !t.rows[i][j].is_zero())
505                .ok_or_else(mismatch)?;
506            t.pivot(i, j);
507            assigned[i] = true;
508            row_of.push(i);
509        }
510        t.rows = row_of.iter().map(|&i| t.rows[i].clone()).collect();
511        t.rhs = row_of.iter().map(|&i| t.rhs[i].clone()).collect();
512        t.basis = basis;
513        t.set_objective(problem.sense, "z", &problem.objective, &Coef::zero());
514        Ok(t)
515    }
516
517    /// Appends constraints `from..` of `problem` as new rows, each written as `<=` with a new
518    /// slack that is basic, and priced out against the current basis so the tableau stays
519    /// consistent. A negative right-hand side means the current solution violates it.
520    fn append_constraints(&mut self, problem: &Problem, from: usize, trace: &mut Trace) {
521        let n = problem.num_vars();
522        for (i, c) in problem.constraints.iter().enumerate().skip(from) {
523            let (coeffs, mut b): (Vec<Rational>, Rational) = match c.relation {
524                Relation::Le => (c.coeffs.clone(), c.rhs.clone()),
525                Relation::Ge => (
526                    c.coeffs.iter().map(|v| -v.clone()).collect(),
527                    -c.rhs.clone(),
528                ),
529                Relation::Eq => unreachable!("rejected by reoptimize"),
530            };
531            self.names.push(format!("s{}", i + 1));
532            self.artificial.push(false);
533            self.artificial_owner.push(None);
534            self.z.push(Coef::zero());
535            for row in self.rows.iter_mut() {
536                row.push(Rational::zero());
537            }
538            let width = self.names.len();
539            let mut row = vec![Rational::zero(); width];
540            row[..n].clone_from_slice(&coeffs);
541            row[width - 1] = Rational::one();
542            for (r, &j) in self.basis.iter().enumerate() {
543                let factor = row[j].clone();
544                if factor.is_zero() {
545                    continue;
546                }
547                for (v, p) in row.iter_mut().zip(&self.rows[r]) {
548                    *v -= &factor * p;
549                }
550                b -= &factor * &self.rhs[r];
551            }
552            let satisfied = !b.is_negative();
553            self.rows.push(row);
554            self.rhs.push(b);
555            self.basis.push(width - 1);
556            trace.push(Step::ConstraintAdded {
557                constraint: i,
558                row: self.rows.len() - 1,
559                satisfied,
560            });
561        }
562    }
563
564    fn has_artificials(&self) -> bool {
565        self.artificial.iter().any(|&a| a)
566    }
567
568    fn snapshot(&self) -> Snapshot {
569        Snapshot {
570            sense: self.sense,
571            objective_label: self.objective_label.to_string(),
572            columns: self.names.clone(),
573            basis: self.basis.clone(),
574            rows: self.rows.clone(),
575            rhs: self.rhs.clone(),
576            z: self.z.clone(),
577            z_rhs: self.z_rhs.clone(),
578        }
579    }
580
581    /// Sets the objective row to `z_j - c_j` for the objective `sense` with costs `costs`
582    /// (original variables; slack/surplus cost 0, artificials cost `artificial_cost`), then
583    /// prices out the basic columns so the row is consistent with the current basis.
584    fn set_objective(
585        &mut self,
586        sense: Sense,
587        label: &'static str,
588        costs: &[Rational],
589        artificial_cost: &Coef,
590    ) {
591        self.sense = sense;
592        self.objective_label = label;
593        self.z = (0..self.names.len())
594            .map(|j| {
595                if j < costs.len() {
596                    -Coef::constant(costs[j].clone())
597                } else if self.artificial[j] {
598                    -artificial_cost.clone()
599                } else {
600                    Coef::zero()
601                }
602            })
603            .collect();
604        self.z_rhs = Coef::zero();
605        for i in 0..self.rows.len() {
606            let factor = self.z[self.basis[i]].clone();
607            if factor.is_zero() {
608                continue;
609            }
610            for (zj, aij) in self.z.iter_mut().zip(&self.rows[i]) {
611                zj.sub_mul(&factor, aij);
612            }
613            self.z_rhs.sub_mul(&factor, &self.rhs[i]);
614        }
615    }
616
617    fn pivot(&mut self, row: usize, col: usize) {
618        let pivot = self.rows[row][col].clone();
619        for v in self.rows[row].iter_mut() {
620            *v /= &pivot;
621        }
622        self.rhs[row] /= &pivot;
623
624        let pivot_row = self.rows[row].clone();
625        let pivot_rhs = self.rhs[row].clone();
626        for i in 0..self.rows.len() {
627            if i == row || self.rows[i][col].is_zero() {
628                continue;
629            }
630            let factor = self.rows[i][col].clone();
631            for (v, p) in self.rows[i].iter_mut().zip(&pivot_row) {
632                *v -= &factor * p;
633            }
634            self.rhs[i] -= &factor * &pivot_rhs;
635        }
636
637        let factor = self.z[col].clone();
638        if !factor.is_zero() {
639            for (zj, p) in self.z.iter_mut().zip(&pivot_row) {
640                zj.sub_mul(&factor, p);
641            }
642            self.z_rhs.sub_mul(&factor, &pivot_rhs);
643        }
644        self.basis[row] = col;
645    }
646
647    /// Maximizing: a column improves when `z_j - c_j < 0`, Dantzig picks the most negative.
648    /// Minimizing: when `z_j - c_j > 0`, Dantzig picks the most positive.
649    ///
650    /// With Big-M the comparison is lexicographic: the `M` part (the penalty on artificials)
651    /// is improved first, the real objective only once the penalty cannot improve. Dantzig
652    /// gets this from the ordering of [`Coef`]; Bland picks the lowest-index column that
653    /// improves the `M` part, and only if none does, the lowest-index column that improves
654    /// the constant part. That is Bland applied to two problems in sequence (the `M` row does
655    /// not change while columns with no `M` part enter), so it still cannot cycle.
656    fn entering(&self, rule: PivotRule) -> Option<usize> {
657        let sense = self.sense;
658        let improves = |j: &usize| match sense {
659            Sense::Maximize => self.z[*j].is_negative(),
660            Sense::Minimize => self.z[*j].is_positive(),
661        };
662        let improves_penalty = |j: &usize| match sense {
663            Sense::Maximize => self.z[*j].m.is_negative(),
664            Sense::Minimize => self.z[*j].m.is_positive(),
665        };
666        let mut candidates = (0..self.z.len()).filter(improves);
667        match rule {
668            PivotRule::Bland => (0..self.z.len())
669                .find(improves_penalty)
670                .or_else(|| candidates.next()),
671            // `min_by` keeps the first of equal elements, so ties go to the lowest index.
672            PivotRule::Dantzig => candidates.min_by(|&a, &b| match sense {
673                Sense::Maximize => self.z[a].cmp(&self.z[b]),
674                Sense::Minimize => self.z[b].cmp(&self.z[a]),
675            }),
676        }
677    }
678
679    /// Minimum-ratio test. Ties go to the row whose basic variable has the lowest index.
680    fn ratio_test(&self, col: usize) -> (Vec<RatioTest>, Option<usize>) {
681        let ratios: Vec<RatioTest> = (0..self.rows.len())
682            .map(|i| {
683                let a = &self.rows[i][col];
684                let ratio = if a.is_positive() {
685                    Some(&self.rhs[i] / a)
686                } else {
687                    None
688                };
689                RatioTest { row: i, ratio }
690            })
691            .collect();
692        let leaving = ratios
693            .iter()
694            .filter_map(|r| r.ratio.as_ref().map(|v| (v, self.basis[r.row], r.row)))
695            .min()
696            .map(|(_, _, row)| row);
697        (ratios, leaving)
698    }
699
700    /// Runs simplex iterations from the current (feasible) basis until optimal or unbounded.
701    fn optimize(
702        &mut self,
703        phase: Phase,
704        options: &Options,
705        trace: &mut Trace,
706    ) -> Result<End, Error> {
707        let mut iteration = 0;
708        loop {
709            trace.push(Step::Tableau {
710                phase,
711                iteration,
712                snapshot: self.snapshot(),
713            });
714
715            let Some(entering) = self.entering(options.pivot_rule) else {
716                return Ok(End::Optimal);
717            };
718            let (ratios, leaving_row) = self.ratio_test(entering);
719            let Some(leaving_row) = leaving_row else {
720                return Ok(End::Unbounded);
721            };
722            if iteration == options.max_iterations {
723                return Err(Error::IterationLimit(options.max_iterations));
724            }
725
726            let leaving = self.basis[leaving_row];
727            trace.push(Step::Pivot {
728                phase,
729                iteration,
730                entering,
731                entering_name: self.names[entering].clone(),
732                leaving_row,
733                leaving,
734                leaving_name: self.names[leaving].clone(),
735                pivot_value: self.rows[leaving_row][entering].clone(),
736                ratios,
737            });
738            self.pivot(leaving_row, entering);
739            iteration += 1;
740        }
741    }
742
743    /// Dual simplex iterations from a dual feasible tableau (objective row with the optimal
744    /// sign) until every right-hand side is non-negative, or a row proves infeasibility.
745    fn optimize_dual(&mut self, options: &Options, trace: &mut Trace) -> Result<End, Error> {
746        let phase = Phase::Dual;
747        let mut iteration = 0;
748        loop {
749            trace.push(Step::Tableau {
750                phase,
751                iteration,
752                snapshot: self.snapshot(),
753            });
754
755            let Some(leaving_row) = self.leaving_row(options.pivot_rule) else {
756                return Ok(End::Optimal);
757            };
758            let leaving = self.basis[leaving_row];
759            let (ratios, entering) = self.dual_ratio_test(leaving_row);
760            let Some(entering) = entering else {
761                trace.push(Step::InfeasibleRow {
762                    row: leaving_row,
763                    basic: NamedValue::new(
764                        self.names[leaving].clone(),
765                        self.rhs[leaving_row].clone(),
766                    ),
767                });
768                return Ok(End::Infeasible);
769            };
770            if iteration == options.max_iterations {
771                return Err(Error::IterationLimit(options.max_iterations));
772            }
773
774            trace.push(Step::DualPivot {
775                phase,
776                iteration,
777                leaving_row,
778                leaving,
779                leaving_name: self.names[leaving].clone(),
780                entering,
781                entering_name: self.names[entering].clone(),
782                pivot_value: self.rows[leaving_row][entering].clone(),
783                ratios,
784            });
785            self.pivot(leaving_row, entering);
786            iteration += 1;
787        }
788    }
789
790    /// Dual simplex leaving row, among those with a negative right-hand side: Dantzig takes
791    /// the most negative (ties: lowest basic variable index), Bland the lowest basic variable
792    /// index. `None` when the tableau is already feasible.
793    fn leaving_row(&self, rule: PivotRule) -> Option<usize> {
794        let negative = (0..self.rows.len()).filter(|&i| self.rhs[i].is_negative());
795        match rule {
796            PivotRule::Dantzig => negative.min_by_key(|&i| (self.rhs[i].clone(), self.basis[i])),
797            PivotRule::Bland => negative.min_by_key(|&i| self.basis[i]),
798        }
799    }
800
801    /// Dual ratio test on `row`: among the columns with a negative entry, the one with the
802    /// smallest `|z_j - c_j| / |a_rj|` enters, which keeps the objective row dual feasible
803    /// after the pivot. Ties go to the lowest column index. No eligible column means the row
804    /// cannot be satisfied.
805    fn dual_ratio_test(&self, row: usize) -> (Vec<DualRatioTest>, Option<usize>) {
806        let ratios: Vec<DualRatioTest> = (0..self.names.len())
807            .map(|j| {
808                let a = &self.rows[row][j];
809                let ratio = a.is_negative().then(|| {
810                    debug_assert!(self.z[j].m.is_zero(), "dual simplex with a symbolic M");
811                    self.z[j].c.abs() / a.abs()
812                });
813                DualRatioTest { column: j, ratio }
814            })
815            .collect();
816        let entering = ratios
817            .iter()
818            .filter_map(|r| r.ratio.as_ref().map(|v| (v, r.column)))
819            .min()
820            .map(|(_, j)| j);
821        (ratios, entering)
822    }
823
824    /// Basic artificial variables with a positive value.
825    fn positive_artificials(&self) -> Vec<NamedValue> {
826        (0..self.rows.len())
827            .filter(|&i| self.artificial[self.basis[i]] && self.rhs[i].is_positive())
828            .map(|i| NamedValue::new(self.names[self.basis[i]].clone(), self.rhs[i].clone()))
829            .collect()
830    }
831
832    /// Plain simplex (`artificial_cost == 0`, no artificials present) or Big-M (`∓M`).
833    fn single_phase(
834        &mut self,
835        sense: Sense,
836        costs: &[Rational],
837        artificial_cost: Coef,
838        options: &Options,
839        trace: &mut Trace,
840    ) -> Result<End, Error> {
841        self.set_objective(sense, "z", costs, &artificial_cost);
842        let end = self.optimize(Phase::Single, options, trace)?;
843
844        // Big-M stops in two ways: an optimal tableau, or an unbounded direction. The entering
845        // column of an unbounded direction cannot improve the `M` part (the penalty is
846        // bounded), and columns that improve the `M` part always enter first. So in both cases
847        // the penalty can no longer improve: the sum of artificials is at its minimum. If an
848        // artificial is still positive, no feasible point exists.
849        let positive = self.positive_artificials();
850        if positive.is_empty() {
851            return Ok(end);
852        }
853        trace.push(Step::ArtificialRemainsPositive {
854            artificials: positive,
855            unbounded_direction: matches!(end, End::Unbounded),
856        });
857        Ok(End::Infeasible)
858    }
859
860    fn two_phase(
861        &mut self,
862        sense: Sense,
863        costs: &[Rational],
864        options: &Options,
865        trace: &mut Trace,
866    ) -> Result<End, Error> {
867        // Phase 1: minimize r = sum of artificials (each artificial costs 1, the rest 0).
868        let zeros = vec![Rational::zero(); costs.len()];
869        self.set_objective(
870            Sense::Minimize,
871            "r",
872            &zeros,
873            &Coef::constant(Rational::one()),
874        );
875        // r is bounded below by 0, so phase 1 always ends optimal.
876        self.optimize(Phase::One, options, trace)?;
877
878        let artificial_sum = self.z_rhs.c.clone();
879        let feasible = artificial_sum.is_zero();
880        trace.push(Step::PhaseOneFinished {
881            artificial_sum,
882            feasible,
883        });
884        if !feasible {
885            return Ok(End::Infeasible);
886        }
887
888        self.drop_artificials(trace);
889        self.set_objective(sense, "z", costs, &Coef::zero());
890        self.optimize(Phase::Two, options, trace)
891    }
892
893    /// After a feasible phase 1: swaps zero-valued artificials out of the basis (or removes
894    /// their row if it is redundant), then deletes the artificial columns.
895    fn drop_artificials(&mut self, trace: &mut Trace) {
896        let mut i = 0;
897        while i < self.rows.len() {
898            if !self.artificial[self.basis[i]] {
899                i += 1;
900                continue;
901            }
902            let replacement =
903                (0..self.names.len()).find(|&j| !self.artificial[j] && !self.rows[i][j].is_zero());
904            match replacement {
905                Some(j) => {
906                    self.pivot(i, j);
907                    trace.push(Step::ArtificialPivotedOut {
908                        row: i,
909                        entering: j,
910                        entering_name: self.names[j].clone(),
911                    });
912                    i += 1;
913                }
914                None => {
915                    // The basic artificial's own constraint is the redundant one: its row
916                    // is the only one with a 1 under that artificial's column.
917                    self.removed
918                        .push(self.artificial_owner[self.basis[i]].expect("artificial column"));
919                    self.rows.remove(i);
920                    self.rhs.remove(i);
921                    self.basis.remove(i);
922                    trace.push(Step::RedundantRowRemoved { row: i });
923                }
924            }
925        }
926
927        let keep: Vec<usize> = (0..self.names.len())
928            .filter(|&j| !self.artificial[j])
929            .collect();
930        let mut new_index = vec![usize::MAX; self.names.len()];
931        for (new, &old) in keep.iter().enumerate() {
932            new_index[old] = new;
933        }
934        self.names = keep.iter().map(|&j| self.names[j].clone()).collect();
935        self.artificial = vec![false; keep.len()];
936        self.artificial_owner = vec![None; keep.len()];
937        for row in self.rows.iter_mut() {
938            *row = keep.iter().map(|&j| row[j].clone()).collect();
939        }
940        for b in self.basis.iter_mut() {
941            *b = new_index[*b];
942        }
943        self.z = vec![Coef::zero(); keep.len()];
944        self.z_rhs = Coef::zero();
945    }
946
947    /// Sensitivity of the final (optimal) basis.
948    ///
949    /// With Big-M a basic artificial may remain at zero. If its row is zero in every real
950    /// column it is a redundant equality and is dropped, as the two-phase method does.
951    /// Otherwise the basis is not one of the real problem: on a copy, the artificials are
952    /// pivoted out and the real objective re-optimized. Those pivots are all degenerate (the
953    /// solution is already optimal, so no pivot can move it), so the analysis still describes
954    /// the reported solution.
955    fn sensitivity(&self, problem: &Problem) -> Option<Sensitivity> {
956        let mut removed = self.removed.clone();
957        let mut basis = Vec::with_capacity(self.basis.len());
958        for (i, &b) in self.basis.iter().enumerate() {
959            if !self.artificial[b] {
960                basis.push(b);
961                continue;
962            }
963            let redundant =
964                (0..self.names.len()).all(|j| self.artificial[j] || self.rows[i][j].is_zero());
965            if !redundant {
966                return self.rebased_sensitivity(problem);
967            }
968            removed.push(self.artificial_owner[b].expect("artificial column"));
969        }
970        let cells = Cells::FinalTableau {
971            artificial_columns: self.has_artificials(),
972        };
973        self.analyze(problem, &basis, &removed, cells)
974    }
975
976    fn rebased_sensitivity(&self, problem: &Problem) -> Option<Sensitivity> {
977        let mut t = self.clone();
978        let mut scratch = Trace::new(problem.sense);
979        t.drop_artificials(&mut scratch);
980        t.set_objective(problem.sense, "z", &problem.objective, &Coef::zero());
981        let options = Options::default().pivot_rule(PivotRule::Bland);
982        match t.optimize(Phase::Two, &options, &mut scratch) {
983            Ok(End::Optimal) => t.analyze(problem, &t.basis, &t.removed, Cells::None),
984            _ => None, // unreachable: the starting basis is feasible and optimal in value
985        }
986    }
987
988    fn analyze(
989        &self,
990        problem: &Problem,
991        basis: &[usize],
992        removed: &[usize],
993        cells: Cells,
994    ) -> Option<Sensitivity> {
995        let start = Tableau::standard_form(problem);
996        let form = StandardForm {
997            rows: &start.rows,
998            rhs: &start.rhs,
999            names: &start.names,
1000            artificial: &start.artificial,
1001        };
1002        sensitivity::analyze(problem, form, basis, removed, cells)
1003    }
1004
1005    fn values(&self, n: usize) -> Vec<Rational> {
1006        let mut values = vec![Rational::zero(); n];
1007        for (i, &b) in self.basis.iter().enumerate() {
1008            if b < n {
1009                values[b] = self.rhs[i].clone();
1010            }
1011        }
1012        values
1013    }
1014}