Skip to main content

simplex_trace/
trace.rs

1use std::fmt;
2
3use num_traits::Signed;
4
5use crate::num::{Coef, Rational};
6use crate::problem::Sense;
7
8/// Stage of the algorithm a step belongs to.
9///
10/// New stages (for example from future methods) may be added in minor releases.
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum Phase {
16    /// Plain simplex or Big-M: one optimization.
17    Single,
18    /// Two-phase method, phase 1: minimize the sum of artificial variables.
19    One,
20    /// Two-phase method, phase 2: optimize the real objective.
21    Two,
22    /// Dual simplex: restore feasibility while keeping the objective row optimal.
23    Dual,
24}
25
26impl fmt::Display for Phase {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            Phase::Single => write!(f, "simplex"),
30            Phase::One => write!(f, "phase 1"),
31            Phase::Two => write!(f, "phase 2"),
32            Phase::Dual => write!(f, "dual simplex"),
33        }
34    }
35}
36
37/// A full copy of the tableau at one point in time.
38///
39/// The tableau is always expressed in the sense of the objective being optimized, as in
40/// Taha's textbook: the objective row holds `z_j - c_j` with the real costs `c_j`, and
41/// `z_rhs` is the real objective value. A maximization tableau is optimal when every
42/// `z_j - c_j >= 0`; a minimization tableau when every `z_j - c_j <= 0` (see
43/// [`Snapshot::is_optimal`]).
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[derive(Clone, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct Snapshot {
48    /// Sense of the objective in this tableau. Phase 1 of the two-phase method is always
49    /// [`Sense::Minimize`] (it minimizes `r`, the sum of the artificial variables).
50    pub sense: Sense,
51    /// Name of the objective row: `"z"`, or `"r"` in phase 1 of the two-phase method.
52    pub objective_label: String,
53    /// Column names (original variables, then slack/surplus, then artificial).
54    pub columns: Vec<String>,
55    /// For each constraint row, the index of its basic column.
56    pub basis: Vec<usize>,
57    /// Constraint rows, without the right-hand side.
58    #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::vec_vec"))]
59    pub rows: Vec<Vec<Rational>>,
60    /// Right-hand side of each constraint row.
61    #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::vec"))]
62    pub rhs: Vec<Rational>,
63    /// Objective row, `z_j - c_j` per column (may contain the symbolic `M`).
64    pub z: Vec<Coef>,
65    /// Current value of the objective (may contain the symbolic `M` in Big-M).
66    pub z_rhs: Coef,
67}
68
69impl Snapshot {
70    /// `true` when no column can improve the objective: all `z_j - c_j >= 0` when
71    /// maximizing, all `<= 0` when minimizing.
72    pub fn is_optimal(&self) -> bool {
73        match self.sense {
74            Sense::Maximize => !self.z.iter().any(Coef::is_negative),
75            Sense::Minimize => !self.z.iter().any(Coef::is_positive),
76        }
77    }
78
79    /// `true` when every right-hand side is `>= 0`. Primal methods keep this at every
80    /// iteration; the dual simplex works towards it, so its intermediate tableaus are
81    /// optimal but not feasible.
82    pub fn is_feasible(&self) -> bool {
83        !self.rhs.iter().any(Signed::is_negative)
84    }
85}
86
87/// One entry of the minimum-ratio test.
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89#[derive(Clone, Debug, PartialEq, Eq)]
90#[non_exhaustive]
91pub struct RatioTest {
92    pub row: usize,
93    /// `rhs / pivot-column entry`, or `None` when the entry is `<= 0` (row not eligible).
94    #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::option"))]
95    pub ratio: Option<Rational>,
96}
97
98/// One entry of the dual simplex ratio test, which runs over the columns of the leaving row.
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100#[derive(Clone, Debug, PartialEq, Eq)]
101#[non_exhaustive]
102pub struct DualRatioTest {
103    pub column: usize,
104    /// `|z_j - c_j| / |a_rj|`, or `None` when the entry `a_rj` is `>= 0` (column not eligible).
105    #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::option"))]
106    pub ratio: Option<Rational>,
107}
108
109/// A variable name with its value.
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111#[derive(Clone, Debug, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct NamedValue {
114    pub name: String,
115    #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
116    pub value: Rational,
117}
118
119impl NamedValue {
120    pub(crate) fn new(name: String, value: Rational) -> Self {
121        NamedValue { name, value }
122    }
123}
124
125/// How a run ended.
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum Termination {
130    Optimal,
131    Infeasible,
132    Unbounded,
133}
134
135/// One recorded event.
136///
137/// New kinds of steps (for example from future methods) may be added in minor releases, so
138/// matches on `Step` need a wildcard arm. With the `serde` feature, a step is a JSON object
139/// whose `"type"` field is the variant name in `snake_case` (`"tableau"`, `"pivot"`, ...).
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
142#[derive(Clone, Debug, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum Step {
145    /// The tableau at the start of an iteration (`iteration` starts at 0).
146    #[non_exhaustive]
147    Tableau {
148        phase: Phase,
149        iteration: usize,
150        snapshot: Snapshot,
151    },
152    /// A pivot: which column enters, which row leaves, and the ratio test behind the choice.
153    #[non_exhaustive]
154    Pivot {
155        phase: Phase,
156        iteration: usize,
157        entering: usize,
158        entering_name: String,
159        leaving_row: usize,
160        leaving: usize,
161        leaving_name: String,
162        #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
163        pivot_value: Rational,
164        ratios: Vec<RatioTest>,
165    },
166    /// Phase 1 of the two-phase method finished.
167    #[non_exhaustive]
168    PhaseOneFinished {
169        #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
170        artificial_sum: Rational,
171        feasible: bool,
172    },
173    /// A zero-valued artificial variable was swapped out of the basis.
174    #[non_exhaustive]
175    ArtificialPivotedOut {
176        row: usize,
177        entering: usize,
178        entering_name: String,
179    },
180    /// A row whose basic variable was artificial and had no usable column: it was redundant.
181    #[non_exhaustive]
182    RedundantRowRemoved { row: usize },
183    /// Big-M: the penalty row (`M` part) can no longer improve, yet some artificial variables
184    /// are still positive, so the problem is infeasible. `unbounded_direction` is `true` when
185    /// the run stopped at an unbounded direction rather than at an optimal tableau.
186    #[non_exhaustive]
187    ArtificialRemainsPositive {
188        /// Each positive artificial variable and its value.
189        artificials: Vec<NamedValue>,
190        unbounded_direction: bool,
191    },
192    /// A dual simplex pivot: which row leaves (it has a negative right-hand side), which
193    /// column enters, and the ratio test over the columns behind the choice.
194    #[non_exhaustive]
195    DualPivot {
196        phase: Phase,
197        iteration: usize,
198        leaving_row: usize,
199        leaving: usize,
200        leaving_name: String,
201        entering: usize,
202        entering_name: String,
203        #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
204        pivot_value: Rational,
205        ratios: Vec<DualRatioTest>,
206    },
207    /// Dual simplex: the leaving row has a negative right-hand side but no negative entry, so
208    /// its equation (non-negative terms adding up to a negative number) cannot hold: the
209    /// problem is infeasible.
210    #[non_exhaustive]
211    InfeasibleRow {
212        row: usize,
213        /// The basic variable of that row and its (negative) value.
214        basic: NamedValue,
215    },
216    /// [`reoptimize`](crate::reoptimize): constraint `constraint` of the extended problem was
217    /// appended to the final tableau as row `row`, expressed in the current basis with its
218    /// own slack basic. `satisfied` tells whether the previous optimum already meets it.
219    #[non_exhaustive]
220    ConstraintAdded {
221        constraint: usize,
222        row: usize,
223        satisfied: bool,
224    },
225    /// Final outcome.
226    #[non_exhaustive]
227    Finished { termination: Termination },
228}
229
230/// The ordered list of everything the solver did.
231#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
232#[derive(Clone, Debug, PartialEq, Eq)]
233#[non_exhaustive]
234pub struct Trace {
235    pub sense: Sense,
236    pub steps: Vec<Step>,
237}
238
239impl Trace {
240    pub(crate) fn new(sense: Sense) -> Self {
241        Trace {
242            sense,
243            steps: Vec::new(),
244        }
245    }
246
247    pub(crate) fn push(&mut self, step: Step) {
248        self.steps.push(step);
249    }
250
251    /// Number of pivots performed (artificial-variable swaps included).
252    pub fn pivot_count(&self) -> usize {
253        self.steps
254            .iter()
255            .filter(|s| {
256                matches!(
257                    s,
258                    Step::Pivot { .. } | Step::DualPivot { .. } | Step::ArtificialPivotedOut { .. }
259                )
260            })
261            .count()
262    }
263
264    /// All tableau snapshots, in order.
265    pub fn tableaus(&self) -> impl Iterator<Item = &Snapshot> {
266        self.steps.iter().filter_map(|s| match s {
267            Step::Tableau { snapshot, .. } => Some(snapshot),
268            _ => None,
269        })
270    }
271
272    /// How the run ended (the last step).
273    pub fn termination(&self) -> Option<Termination> {
274        match self.steps.last() {
275            Some(Step::Finished { termination }) => Some(*termination),
276            _ => None,
277        }
278    }
279}
280
281impl fmt::Display for Snapshot {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        let mut table: Vec<Vec<String>> = Vec::new();
284
285        let mut header = vec!["basis".to_string()];
286        header.extend(self.columns.iter().cloned());
287        header.push("rhs".to_string());
288        table.push(header);
289
290        for (i, row) in self.rows.iter().enumerate() {
291            let mut line = vec![self.columns[self.basis[i]].clone()];
292            line.extend(row.iter().map(|v| v.to_string()));
293            line.push(self.rhs[i].to_string());
294            table.push(line);
295        }
296
297        let mut z_line = vec![self.objective_label.clone()];
298        z_line.extend(self.z.iter().map(|v| v.to_string()));
299        z_line.push(self.z_rhs.to_string());
300        table.push(z_line);
301        write_table(f, &table)
302    }
303}
304
305/// Writes rows of cells as an aligned table: the first column left-aligned, the rest
306/// right-aligned, with a rule under the header row.
307pub(crate) fn write_table(f: &mut fmt::Formatter<'_>, table: &[Vec<String>]) -> fmt::Result {
308    let widths: Vec<usize> = (0..table[0].len())
309        .map(|c| {
310            table
311                .iter()
312                .map(|r| r[c].chars().count())
313                .max()
314                .unwrap_or(0)
315        })
316        .collect();
317
318    for (r, line) in table.iter().enumerate() {
319        let cells: Vec<String> = line
320            .iter()
321            .zip(&widths)
322            .enumerate()
323            .map(|(c, (cell, w))| {
324                if c == 0 {
325                    format!("{cell:<w$}")
326                } else {
327                    format!("{cell:>w$}")
328                }
329            })
330            .collect();
331        writeln!(f, "{}", cells.join(" | "))?;
332        if r == 0 {
333            let rule: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
334            writeln!(f, "{}", rule.join("-+-"))?;
335        }
336    }
337    Ok(())
338}
339
340impl fmt::Display for Trace {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        // Column names of the latest tableau, to label the dual ratio test.
343        let mut columns: &[String] = &[];
344        for step in &self.steps {
345            match step {
346                Step::Tableau {
347                    phase,
348                    iteration,
349                    snapshot,
350                } => {
351                    columns = &snapshot.columns;
352                    writeln!(f, "== {phase}, iteration {iteration} ==")?;
353                    write!(f, "{snapshot}")?;
354                }
355                Step::Pivot {
356                    entering_name,
357                    leaving_name,
358                    leaving_row,
359                    pivot_value,
360                    ratios,
361                    ..
362                } => {
363                    let tests: Vec<String> = ratios
364                        .iter()
365                        .map(|r| match &r.ratio {
366                            Some(v) => format!("row {}: {v}", r.row + 1),
367                            None => format!("row {}: -", r.row + 1),
368                        })
369                        .collect();
370                    writeln!(
371                        f,
372                        "-> {entering_name} enters, {leaving_name} leaves (row {}, pivot {pivot_value}); ratios [{}]",
373                        leaving_row + 1,
374                        tests.join(", ")
375                    )?;
376                }
377                Step::PhaseOneFinished {
378                    artificial_sum,
379                    feasible,
380                } => {
381                    writeln!(
382                        f,
383                        "== phase 1 finished: sum of artificials = {artificial_sum} ({}) ==",
384                        if *feasible { "feasible" } else { "infeasible" }
385                    )?;
386                }
387                Step::ArtificialPivotedOut {
388                    row, entering_name, ..
389                } => {
390                    writeln!(
391                        f,
392                        "-> artificial variable in row {} replaced by {entering_name}",
393                        row + 1
394                    )?;
395                }
396                Step::RedundantRowRemoved { row } => {
397                    writeln!(f, "-> row {} is redundant and was removed", row + 1)?;
398                }
399                Step::ArtificialRemainsPositive {
400                    artificials,
401                    unbounded_direction,
402                } => {
403                    let list: Vec<String> = artificials
404                        .iter()
405                        .map(|a| format!("{} = {}", a.name, a.value))
406                        .collect();
407                    let stop = if *unbounded_direction {
408                        "an unbounded direction was found"
409                    } else {
410                        "the tableau is optimal"
411                    };
412                    writeln!(
413                        f,
414                        "-> {stop}, but the M row cannot improve and {} still positive: no feasible solution",
415                        if list.len() == 1 {
416                            format!("{} is", list[0])
417                        } else {
418                            format!("{} are", list.join(", "))
419                        }
420                    )?;
421                }
422                Step::DualPivot {
423                    leaving_name,
424                    leaving_row,
425                    entering_name,
426                    pivot_value,
427                    ratios,
428                    ..
429                } => {
430                    let tests: Vec<String> = ratios
431                        .iter()
432                        .filter_map(|r| r.ratio.as_ref().map(|v| (r.column, v)))
433                        .map(|(j, v)| {
434                            format!("{}: {v}", columns.get(j).map_or("?", String::as_str))
435                        })
436                        .collect();
437                    writeln!(
438                        f,
439                        "-> {leaving_name} leaves (row {}), {entering_name} enters (pivot {pivot_value}); ratios [{}]",
440                        leaving_row + 1,
441                        tests.join(", ")
442                    )?;
443                }
444                Step::InfeasibleRow { row, basic } => {
445                    writeln!(
446                        f,
447                        "-> row {} has {} = {} but no negative entry: no feasible solution",
448                        row + 1,
449                        basic.name,
450                        basic.value
451                    )?;
452                }
453                Step::ConstraintAdded {
454                    constraint,
455                    row,
456                    satisfied,
457                } => {
458                    writeln!(
459                        f,
460                        "-> constraint {} added as row {} ({} by the current solution)",
461                        constraint + 1,
462                        row + 1,
463                        if *satisfied { "satisfied" } else { "violated" }
464                    )?;
465                }
466                Step::Finished { termination } => writeln!(f, "== {termination:?} ==")?,
467            }
468            writeln!(f)?;
469        }
470        Ok(())
471    }
472}