use std::fmt;
use crate::num::{Coef, Rational};
use crate::problem::Sense;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Phase {
Single,
One,
Two,
Dual,
}
impl fmt::Display for Phase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Phase::Single => write!(f, "simplex"),
Phase::One => write!(f, "phase 1"),
Phase::Two => write!(f, "phase 2"),
Phase::Dual => write!(f, "dual simplex"),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Snapshot {
pub sense: Sense,
pub objective_label: String,
pub columns: Vec<String>,
pub basis: Vec<usize>,
pub rows: Vec<Vec<Rational>>,
pub rhs: Vec<Rational>,
pub z: Vec<Coef>,
pub z_rhs: Coef,
}
impl Snapshot {
pub fn is_optimal(&self) -> bool {
match self.sense {
Sense::Maximize => !self.z.iter().any(Coef::is_negative),
Sense::Minimize => !self.z.iter().any(Coef::is_positive),
}
}
pub fn is_feasible(&self) -> bool {
!self.rhs.iter().any(Rational::is_negative)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct RatioTest {
pub row: usize,
pub ratio: Option<Rational>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct DualRatioTest {
pub column: usize,
pub ratio: Option<Rational>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct NamedValue {
pub name: String,
pub value: Rational,
}
impl NamedValue {
pub(crate) fn new(name: String, value: Rational) -> Self {
NamedValue { name, value }
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Termination {
Optimal,
Infeasible,
Unbounded,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type", rename_all = "snake_case"))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Step {
#[non_exhaustive]
Tableau {
phase: Phase,
iteration: usize,
snapshot: Snapshot,
},
#[non_exhaustive]
Pivot {
phase: Phase,
iteration: usize,
entering: usize,
entering_name: String,
leaving_row: usize,
leaving: usize,
leaving_name: String,
pivot_value: Rational,
ratios: Vec<RatioTest>,
},
#[non_exhaustive]
PhaseOneFinished {
artificial_sum: Rational,
feasible: bool,
},
#[non_exhaustive]
ArtificialPivotedOut {
row: usize,
entering: usize,
entering_name: String,
},
#[non_exhaustive]
RedundantRowRemoved { row: usize },
#[non_exhaustive]
ArtificialRemainsPositive {
artificials: Vec<NamedValue>,
unbounded_direction: bool,
},
#[non_exhaustive]
DualPivot {
phase: Phase,
iteration: usize,
leaving_row: usize,
leaving: usize,
leaving_name: String,
entering: usize,
entering_name: String,
pivot_value: Rational,
ratios: Vec<DualRatioTest>,
},
#[non_exhaustive]
InfeasibleRow {
row: usize,
basic: NamedValue,
},
#[non_exhaustive]
ConstraintAdded {
constraint: usize,
row: usize,
satisfied: bool,
},
#[non_exhaustive]
Finished { termination: Termination },
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Trace {
pub sense: Sense,
pub steps: Vec<Step>,
}
impl Trace {
pub(crate) fn new(sense: Sense) -> Self {
Trace {
sense,
steps: Vec::new(),
}
}
pub(crate) fn push(&mut self, step: Step) {
self.steps.push(step);
}
pub fn pivot_count(&self) -> usize {
self.steps
.iter()
.filter(|s| {
matches!(
s,
Step::Pivot { .. } | Step::DualPivot { .. } | Step::ArtificialPivotedOut { .. }
)
})
.count()
}
pub fn tableaus(&self) -> impl Iterator<Item = &Snapshot> {
self.steps.iter().filter_map(|s| match s {
Step::Tableau { snapshot, .. } => Some(snapshot),
_ => None,
})
}
pub fn termination(&self) -> Option<Termination> {
match self.steps.last() {
Some(Step::Finished { termination }) => Some(*termination),
_ => None,
}
}
}
impl fmt::Display for Snapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table: Vec<Vec<String>> = Vec::new();
let mut header = vec!["basis".to_string()];
header.extend(self.columns.iter().cloned());
header.push("rhs".to_string());
table.push(header);
for (i, row) in self.rows.iter().enumerate() {
let mut line = vec![self.columns[self.basis[i]].clone()];
line.extend(row.iter().map(|v| v.to_string()));
line.push(self.rhs[i].to_string());
table.push(line);
}
let mut z_line = vec![self.objective_label.clone()];
z_line.extend(self.z.iter().map(|v| v.to_string()));
z_line.push(self.z_rhs.to_string());
table.push(z_line);
write_table(f, &table)
}
}
pub(crate) fn write_table(f: &mut fmt::Formatter<'_>, table: &[Vec<String>]) -> fmt::Result {
let widths: Vec<usize> = (0..table[0].len())
.map(|c| {
table
.iter()
.map(|r| r[c].chars().count())
.max()
.unwrap_or(0)
})
.collect();
for (r, line) in table.iter().enumerate() {
let cells: Vec<String> = line
.iter()
.zip(&widths)
.enumerate()
.map(|(c, (cell, w))| {
if c == 0 {
format!("{cell:<w$}")
} else {
format!("{cell:>w$}")
}
})
.collect();
writeln!(f, "{}", cells.join(" | "))?;
if r == 0 {
let rule: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
writeln!(f, "{}", rule.join("-+-"))?;
}
}
Ok(())
}
impl fmt::Display for Trace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut columns: &[String] = &[];
for step in &self.steps {
match step {
Step::Tableau {
phase,
iteration,
snapshot,
} => {
columns = &snapshot.columns;
writeln!(f, "== {phase}, iteration {iteration} ==")?;
write!(f, "{snapshot}")?;
}
Step::Pivot {
entering_name,
leaving_name,
leaving_row,
pivot_value,
ratios,
..
} => {
let tests: Vec<String> = ratios
.iter()
.map(|r| match &r.ratio {
Some(v) => format!("row {}: {v}", r.row + 1),
None => format!("row {}: -", r.row + 1),
})
.collect();
writeln!(
f,
"-> {entering_name} enters, {leaving_name} leaves (row {}, pivot {pivot_value}); ratios [{}]",
leaving_row + 1,
tests.join(", ")
)?;
}
Step::PhaseOneFinished {
artificial_sum,
feasible,
} => {
writeln!(
f,
"== phase 1 finished: sum of artificials = {artificial_sum} ({}) ==",
if *feasible { "feasible" } else { "infeasible" }
)?;
}
Step::ArtificialPivotedOut {
row, entering_name, ..
} => {
writeln!(
f,
"-> artificial variable in row {} replaced by {entering_name}",
row + 1
)?;
}
Step::RedundantRowRemoved { row } => {
writeln!(f, "-> row {} is redundant and was removed", row + 1)?;
}
Step::ArtificialRemainsPositive {
artificials,
unbounded_direction,
} => {
let list: Vec<String> = artificials
.iter()
.map(|a| format!("{} = {}", a.name, a.value))
.collect();
let stop = if *unbounded_direction {
"an unbounded direction was found"
} else {
"the tableau is optimal"
};
writeln!(
f,
"-> {stop}, but the M row cannot improve and {} still positive: no feasible solution",
if list.len() == 1 {
format!("{} is", list[0])
} else {
format!("{} are", list.join(", "))
}
)?;
}
Step::DualPivot {
leaving_name,
leaving_row,
entering_name,
pivot_value,
ratios,
..
} => {
let tests: Vec<String> = ratios
.iter()
.filter_map(|r| r.ratio.as_ref().map(|v| (r.column, v)))
.map(|(j, v)| {
format!("{}: {v}", columns.get(j).map_or("?", String::as_str))
})
.collect();
writeln!(
f,
"-> {leaving_name} leaves (row {}), {entering_name} enters (pivot {pivot_value}); ratios [{}]",
leaving_row + 1,
tests.join(", ")
)?;
}
Step::InfeasibleRow { row, basic } => {
writeln!(
f,
"-> row {} has {} = {} but no negative entry: no feasible solution",
row + 1,
basic.name,
basic.value
)?;
}
Step::ConstraintAdded {
constraint,
row,
satisfied,
} => {
writeln!(
f,
"-> constraint {} added as row {} ({} by the current solution)",
constraint + 1,
row + 1,
if *satisfied { "satisfied" } else { "violated" }
)?;
}
Step::Finished { termination } => writeln!(f, "== {termination:?} ==")?,
}
writeln!(f)?;
}
Ok(())
}
}