Skip to main content

simplex_trace/
trace.rs

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