1use std::fmt;
2
3use num_traits::Signed;
4
5use crate::num::{Coef, Rational};
6use crate::problem::Sense;
7
8#[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 Single,
18 One,
20 Two,
22 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#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[derive(Clone, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct Snapshot {
48 pub sense: Sense,
51 pub objective_label: String,
53 pub columns: Vec<String>,
55 pub basis: Vec<usize>,
57 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::vec_vec"))]
59 pub rows: Vec<Vec<Rational>>,
60 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::vec"))]
62 pub rhs: Vec<Rational>,
63 pub z: Vec<Coef>,
65 pub z_rhs: Coef,
67}
68
69impl Snapshot {
70 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 pub fn is_feasible(&self) -> bool {
83 !self.rhs.iter().any(Signed::is_negative)
84 }
85}
86
87#[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 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::option"))]
95 pub ratio: Option<Rational>,
96}
97
98#[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 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::option"))]
106 pub ratio: Option<Rational>,
107}
108
109#[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#[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#[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 #[non_exhaustive]
147 Tableau {
148 phase: Phase,
149 iteration: usize,
150 snapshot: Snapshot,
151 },
152 #[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 #[non_exhaustive]
168 PhaseOneFinished {
169 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
170 artificial_sum: Rational,
171 feasible: bool,
172 },
173 #[non_exhaustive]
175 ArtificialPivotedOut {
176 row: usize,
177 entering: usize,
178 entering_name: String,
179 },
180 #[non_exhaustive]
182 RedundantRowRemoved { row: usize },
183 #[non_exhaustive]
187 ArtificialRemainsPositive {
188 artificials: Vec<NamedValue>,
190 unbounded_direction: bool,
191 },
192 #[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 #[non_exhaustive]
211 InfeasibleRow {
212 row: usize,
213 basic: NamedValue,
215 },
216 #[non_exhaustive]
220 ConstraintAdded {
221 constraint: usize,
222 row: usize,
223 satisfied: bool,
224 },
225 #[non_exhaustive]
227 Finished { termination: Termination },
228}
229
230#[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 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 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 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
305pub(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 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}