1use std::fmt;
2
3use crate::num::{Coef, Rational};
4use crate::problem::Sense;
5
6#[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 Single,
16 One,
18 Two,
20 Dual,
22}
23
24impl fmt::Display for Phase {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Phase::Single => write!(f, "simplex"),
28 Phase::One => write!(f, "phase 1"),
29 Phase::Two => write!(f, "phase 2"),
30 Phase::Dual => write!(f, "dual simplex"),
31 }
32 }
33}
34
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43#[derive(Clone, Debug, PartialEq, Eq)]
44#[non_exhaustive]
45pub struct Snapshot {
46 pub sense: Sense,
49 pub objective_label: String,
51 pub columns: Vec<String>,
53 pub basis: Vec<usize>,
55 pub rows: Vec<Vec<Rational>>,
57 pub rhs: Vec<Rational>,
59 pub z: Vec<Coef>,
61 pub z_rhs: Coef,
63}
64
65impl Snapshot {
66 pub fn is_optimal(&self) -> bool {
69 match self.sense {
70 Sense::Maximize => !self.z.iter().any(Coef::is_negative),
71 Sense::Minimize => !self.z.iter().any(Coef::is_positive),
72 }
73 }
74
75 pub fn is_feasible(&self) -> bool {
79 !self.rhs.iter().any(Rational::is_negative)
80 }
81}
82
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85#[derive(Clone, Debug, PartialEq, Eq)]
86#[non_exhaustive]
87pub struct RatioTest {
88 pub row: usize,
89 pub ratio: Option<Rational>,
91}
92
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95#[derive(Clone, Debug, PartialEq, Eq)]
96#[non_exhaustive]
97pub struct DualRatioTest {
98 pub column: usize,
99 pub ratio: Option<Rational>,
101}
102
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105#[derive(Clone, Debug, PartialEq, Eq)]
106#[non_exhaustive]
107pub struct NamedValue {
108 pub name: String,
109 pub value: Rational,
110}
111
112impl NamedValue {
113 pub(crate) fn new(name: String, value: Rational) -> Self {
114 NamedValue { name, value }
115 }
116}
117
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum Termination {
123 Optimal,
124 Infeasible,
125 Unbounded,
126}
127
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
135#[derive(Clone, Debug, PartialEq, Eq)]
136#[non_exhaustive]
137pub enum Step {
138 #[non_exhaustive]
140 Tableau {
141 phase: Phase,
142 iteration: usize,
143 snapshot: Snapshot,
144 },
145 #[non_exhaustive]
147 Pivot {
148 phase: Phase,
149 iteration: usize,
150 entering: usize,
151 entering_name: String,
152 leaving_row: usize,
153 leaving: usize,
154 leaving_name: String,
155 pivot_value: Rational,
156 ratios: Vec<RatioTest>,
157 },
158 #[non_exhaustive]
160 PhaseOneFinished {
161 artificial_sum: Rational,
162 feasible: bool,
163 },
164 #[non_exhaustive]
166 ArtificialPivotedOut {
167 row: usize,
168 entering: usize,
169 entering_name: String,
170 },
171 #[non_exhaustive]
173 RedundantRowRemoved { row: usize },
174 #[non_exhaustive]
178 ArtificialRemainsPositive {
179 artificials: Vec<NamedValue>,
181 unbounded_direction: bool,
182 },
183 #[non_exhaustive]
186 DualPivot {
187 phase: Phase,
188 iteration: usize,
189 leaving_row: usize,
190 leaving: usize,
191 leaving_name: String,
192 entering: usize,
193 entering_name: String,
194 pivot_value: Rational,
195 ratios: Vec<DualRatioTest>,
196 },
197 #[non_exhaustive]
201 InfeasibleRow {
202 row: usize,
203 basic: NamedValue,
205 },
206 #[non_exhaustive]
210 ConstraintAdded {
211 constraint: usize,
212 row: usize,
213 satisfied: bool,
214 },
215 #[non_exhaustive]
217 Finished { termination: Termination },
218}
219
220#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
222#[derive(Clone, Debug, PartialEq, Eq)]
223#[non_exhaustive]
224pub struct Trace {
225 pub sense: Sense,
226 pub steps: Vec<Step>,
227}
228
229impl Trace {
230 pub(crate) fn new(sense: Sense) -> Self {
231 Trace {
232 sense,
233 steps: Vec::new(),
234 }
235 }
236
237 pub(crate) fn push(&mut self, step: Step) {
238 self.steps.push(step);
239 }
240
241 pub fn pivot_count(&self) -> usize {
243 self.steps
244 .iter()
245 .filter(|s| {
246 matches!(
247 s,
248 Step::Pivot { .. } | Step::DualPivot { .. } | Step::ArtificialPivotedOut { .. }
249 )
250 })
251 .count()
252 }
253
254 pub fn tableaus(&self) -> impl Iterator<Item = &Snapshot> {
256 self.steps.iter().filter_map(|s| match s {
257 Step::Tableau { snapshot, .. } => Some(snapshot),
258 _ => None,
259 })
260 }
261
262 pub fn termination(&self) -> Option<Termination> {
264 match self.steps.last() {
265 Some(Step::Finished { termination }) => Some(*termination),
266 _ => None,
267 }
268 }
269}
270
271impl fmt::Display for Snapshot {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 let mut table: Vec<Vec<String>> = Vec::new();
274
275 let mut header = vec!["basis".to_string()];
276 header.extend(self.columns.iter().cloned());
277 header.push("rhs".to_string());
278 table.push(header);
279
280 for (i, row) in self.rows.iter().enumerate() {
281 let mut line = vec![self.columns[self.basis[i]].clone()];
282 line.extend(row.iter().map(|v| v.to_string()));
283 line.push(self.rhs[i].to_string());
284 table.push(line);
285 }
286
287 let mut z_line = vec![self.objective_label.clone()];
288 z_line.extend(self.z.iter().map(|v| v.to_string()));
289 z_line.push(self.z_rhs.to_string());
290 table.push(z_line);
291 write_table(f, &table)
292 }
293}
294
295pub(crate) fn write_table(f: &mut fmt::Formatter<'_>, table: &[Vec<String>]) -> fmt::Result {
298 let widths: Vec<usize> = (0..table[0].len())
299 .map(|c| {
300 table
301 .iter()
302 .map(|r| r[c].chars().count())
303 .max()
304 .unwrap_or(0)
305 })
306 .collect();
307
308 for (r, line) in table.iter().enumerate() {
309 let cells: Vec<String> = line
310 .iter()
311 .zip(&widths)
312 .enumerate()
313 .map(|(c, (cell, w))| {
314 if c == 0 {
315 format!("{cell:<w$}")
316 } else {
317 format!("{cell:>w$}")
318 }
319 })
320 .collect();
321 writeln!(f, "{}", cells.join(" | "))?;
322 if r == 0 {
323 let rule: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
324 writeln!(f, "{}", rule.join("-+-"))?;
325 }
326 }
327 Ok(())
328}
329
330impl fmt::Display for Trace {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 let mut columns: &[String] = &[];
334 for step in &self.steps {
335 match step {
336 Step::Tableau {
337 phase,
338 iteration,
339 snapshot,
340 } => {
341 columns = &snapshot.columns;
342 writeln!(f, "== {phase}, iteration {iteration} ==")?;
343 write!(f, "{snapshot}")?;
344 }
345 Step::Pivot {
346 entering_name,
347 leaving_name,
348 leaving_row,
349 pivot_value,
350 ratios,
351 ..
352 } => {
353 let tests: Vec<String> = ratios
354 .iter()
355 .map(|r| match &r.ratio {
356 Some(v) => format!("row {}: {v}", r.row + 1),
357 None => format!("row {}: -", r.row + 1),
358 })
359 .collect();
360 writeln!(
361 f,
362 "-> {entering_name} enters, {leaving_name} leaves (row {}, pivot {pivot_value}); ratios [{}]",
363 leaving_row + 1,
364 tests.join(", ")
365 )?;
366 }
367 Step::PhaseOneFinished {
368 artificial_sum,
369 feasible,
370 } => {
371 writeln!(
372 f,
373 "== phase 1 finished: sum of artificials = {artificial_sum} ({}) ==",
374 if *feasible { "feasible" } else { "infeasible" }
375 )?;
376 }
377 Step::ArtificialPivotedOut {
378 row, entering_name, ..
379 } => {
380 writeln!(
381 f,
382 "-> artificial variable in row {} replaced by {entering_name}",
383 row + 1
384 )?;
385 }
386 Step::RedundantRowRemoved { row } => {
387 writeln!(f, "-> row {} is redundant and was removed", row + 1)?;
388 }
389 Step::ArtificialRemainsPositive {
390 artificials,
391 unbounded_direction,
392 } => {
393 let list: Vec<String> = artificials
394 .iter()
395 .map(|a| format!("{} = {}", a.name, a.value))
396 .collect();
397 let stop = if *unbounded_direction {
398 "an unbounded direction was found"
399 } else {
400 "the tableau is optimal"
401 };
402 writeln!(
403 f,
404 "-> {stop}, but the M row cannot improve and {} still positive: no feasible solution",
405 if list.len() == 1 {
406 format!("{} is", list[0])
407 } else {
408 format!("{} are", list.join(", "))
409 }
410 )?;
411 }
412 Step::DualPivot {
413 leaving_name,
414 leaving_row,
415 entering_name,
416 pivot_value,
417 ratios,
418 ..
419 } => {
420 let tests: Vec<String> = ratios
421 .iter()
422 .filter_map(|r| r.ratio.as_ref().map(|v| (r.column, v)))
423 .map(|(j, v)| {
424 format!("{}: {v}", columns.get(j).map_or("?", String::as_str))
425 })
426 .collect();
427 writeln!(
428 f,
429 "-> {leaving_name} leaves (row {}), {entering_name} enters (pivot {pivot_value}); ratios [{}]",
430 leaving_row + 1,
431 tests.join(", ")
432 )?;
433 }
434 Step::InfeasibleRow { row, basic } => {
435 writeln!(
436 f,
437 "-> row {} has {} = {} but no negative entry: no feasible solution",
438 row + 1,
439 basic.name,
440 basic.value
441 )?;
442 }
443 Step::ConstraintAdded {
444 constraint,
445 row,
446 satisfied,
447 } => {
448 writeln!(
449 f,
450 "-> constraint {} added as row {} ({} by the current solution)",
451 constraint + 1,
452 row + 1,
453 if *satisfied { "satisfied" } else { "violated" }
454 )?;
455 }
456 Step::Finished { termination } => writeln!(f, "== {termination:?} ==")?,
457 }
458 writeln!(f)?;
459 }
460 Ok(())
461 }
462}