use std::fmt;
use num_traits::{One, Signed, Zero};
use crate::num::Rational;
use crate::problem::{Problem, Relation, Sense};
use crate::trace::write_table;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Sensitivity {
pub basic_variables: Vec<String>,
pub from_final_tableau: bool,
pub constraints: Vec<ConstraintSensitivity>,
pub variables: Vec<VariableSensitivity>,
pub degenerate: bool,
pub alternative_optima: bool,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ConstraintSensitivity {
#[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
pub shadow_price: Rational,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
pub slack: Rational,
pub binding: bool,
pub rhs_range: Interval,
pub redundant: bool,
pub source: Option<Source>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct VariableSensitivity {
pub name: String,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
pub value: Rational,
pub basic: bool,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
pub reduced_cost: Rational,
pub cost_range: Interval,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Interval {
#[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::option"))]
pub lower: Option<Rational>,
#[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::option"))]
pub upper: Option<Rational>,
}
impl Interval {
pub fn contains(&self, value: &Rational) -> bool {
self.lower.as_ref().is_none_or(|l| l <= value)
&& self.upper.as_ref().is_none_or(|u| value <= u)
}
fn around(
value: &Rational,
lower_delta: Option<Rational>,
upper_delta: Option<Rational>,
) -> Self {
Interval {
lower: lower_delta.map(|d| value + d),
upper: upper_delta.map(|d| value + d),
}
}
}
impl fmt::Display for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.lower {
Some(l) => write!(f, "[{l}, ")?,
None => write!(f, "(-inf, ")?,
}
match &self.upper {
Some(u) => write!(f, "{u}]"),
None => write!(f, "+inf)"),
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Source {
pub column: usize,
pub name: String,
pub negated: bool,
}
impl fmt::Display for Sensitivity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = vec![
[
"constraint",
"shadow price",
"slack",
"rhs range",
"read from",
]
.map(String::from)
.to_vec(),
];
for (i, c) in self.constraints.iter().enumerate() {
let source = match &c.source {
Some(s) if s.negated => format!("-({})", s.name),
Some(s) => s.name.clone(),
None if c.redundant => "redundant".to_string(),
None => "computed".to_string(),
};
table.push(vec![
format!("#{}", i + 1),
c.shadow_price.to_string(),
c.slack.to_string(),
c.rhs_range.to_string(),
source,
]);
}
write_table(f, &table)?;
writeln!(f)?;
let mut table = vec![
["variable", "value", "reduced cost", "cost range"]
.map(String::from)
.to_vec(),
];
for v in &self.variables {
table.push(vec![
v.name.clone(),
v.value.to_string(),
v.reduced_cost.to_string(),
v.cost_range.to_string(),
]);
}
write_table(f, &table)?;
if self.degenerate {
writeln!(
f,
"\nnote: degenerate basis, shadow prices hold in one direction only"
)?;
}
if self.alternative_optima {
writeln!(
f,
"\nnote: a nonbasic column has zero reduced cost, other optima exist"
)?;
}
Ok(())
}
}
pub(crate) enum Cells {
FinalTableau { artificial_columns: bool },
None,
}
pub(crate) struct StandardForm<'a> {
pub rows: &'a [Vec<Rational>],
pub rhs: &'a [Rational],
pub names: &'a [String],
pub artificial: &'a [bool],
}
pub(crate) fn analyze(
problem: &Problem,
start: StandardForm<'_>,
basis: &[usize],
removed: &[usize],
cells: Cells,
) -> Option<Sensitivity> {
let n = problem.num_vars();
let m = problem.constraints.len();
let a = start.rows;
let real = start
.artificial
.iter()
.position(|&art| art)
.unwrap_or(start.artificial.len());
let kept: Vec<usize> = (0..m).filter(|i| !removed.contains(i)).collect();
if kept.len() != basis.len() {
return None;
}
let b_matrix: Vec<Vec<Rational>> = kept
.iter()
.map(|&i| basis.iter().map(|&j| a[i][j].clone()).collect())
.collect();
let binv = inverse(b_matrix)?;
let cost = |j: usize| {
if j < n {
problem.objective[j].clone()
} else {
Rational::zero()
}
};
let xb: Vec<Rational> = binv
.iter()
.map(|row| row.iter().zip(&kept).map(|(v, &i)| v * &start.rhs[i]).sum())
.collect();
let mut w = vec![Rational::zero(); m];
for (k, &i) in kept.iter().enumerate() {
w[i] = basis
.iter()
.enumerate()
.map(|(r, &j)| cost(j) * &binv[r][k])
.sum();
}
let d: Vec<Rational> = (0..real)
.map(|j| (0..m).map(|i| &w[i] * &a[i][j]).sum::<Rational>() - cost(j))
.collect();
let mut row_of = vec![None; real];
for (r, &j) in basis.iter().enumerate() {
row_of[j] = Some(r);
}
let nonbasic: Vec<usize> = (0..real).filter(|&j| row_of[j].is_none()).collect();
let alpha = |r: usize, l: usize| -> Rational {
kept.iter()
.enumerate()
.map(|(k, &i)| &binv[r][k] * &a[i][l])
.sum()
};
let mut pinned = vec![false; kept.len()];
for &r in removed {
for (k, pin) in pinned.iter_mut().enumerate() {
let lambda: Rational = basis
.iter()
.enumerate()
.map(|(row, &j)| &a[r][j] * &binv[row][k])
.sum();
*pin |= !lambda.is_zero();
}
}
let values: Vec<Rational> = (0..n)
.map(|j| row_of[j].map_or_else(Rational::zero, |r| xb[r].clone()))
.collect();
let constraints = problem
.constraints
.iter()
.enumerate()
.map(|(i, c)| {
let flip = if c.rhs.is_negative() {
-Rational::one()
} else {
Rational::one()
};
let activity: Rational = c.coeffs.iter().zip(&values).map(|(a, x)| a * x).sum();
let slack = match c.relation {
Relation::Le => &c.rhs - &activity,
Relation::Ge => &activity - &c.rhs,
Relation::Eq => Rational::zero(),
};
let point = || Interval::around(&c.rhs, Some(Rational::zero()), Some(Rational::zero()));
let Some(k) = kept.iter().position(|&kept| kept == i) else {
return ConstraintSensitivity {
shadow_price: Rational::zero(),
binding: true,
slack,
rhs_range: point(),
redundant: true,
source: None,
};
};
let mut lower: Option<Rational> = None;
let mut upper: Option<Rational> = None;
for (r, x) in xb.iter().enumerate() {
let u = &binv[r][k];
if u.is_zero() {
continue;
}
let limit = -x / u;
if u.is_positive() {
lower = Some(lower.map_or(limit.clone(), |l| l.max(limit)));
} else {
upper = Some(upper.map_or(limit.clone(), |v| v.min(limit)));
}
}
let rhs_range = if pinned[k] {
point()
} else if flip.is_one() {
Interval::around(&c.rhs, lower, upper)
} else {
Interval::around(&c.rhs, upper.map(|v| -v), lower.map(|v| -v))
};
let slack_column = (n..real)
.find(|&j| !a[i][j].is_zero())
.map(|j| (j, a[i][j].clone()));
let artificial_column = || {
(real..start.names.len())
.find(|&j| !a[i][j].is_zero())
.map(|j| (j, Rational::one()))
};
let column = match cells {
Cells::FinalTableau { artificial_columns } => {
slack_column.or_else(|| artificial_columns.then(artificial_column).flatten())
}
Cells::None => None,
};
let source = column.map(|(j, sign)| Source {
column: j,
name: start.names[j].clone(),
negated: (sign * &flip).is_negative(),
});
ConstraintSensitivity {
shadow_price: &flip * &w[i],
binding: slack.is_zero(),
slack,
rhs_range,
redundant: false,
source,
}
})
.collect();
let variables = (0..n)
.map(|j| {
let c = &problem.objective[j];
let (lower, upper) = match row_of[j] {
None => match problem.sense {
Sense::Maximize => (None, Some(d[j].clone())),
Sense::Minimize => (Some(d[j].clone()), None),
},
Some(r) => {
let mut lower: Option<Rational> = None;
let mut upper: Option<Rational> = None;
for &l in &nonbasic {
let al = alpha(r, l);
if al.is_zero() {
continue;
}
let limit = -&d[l] / &al;
let is_lower = match problem.sense {
Sense::Maximize => al.is_positive(),
Sense::Minimize => al.is_negative(),
};
if is_lower {
lower = Some(lower.map_or(limit.clone(), |v| v.max(limit)));
} else {
upper = Some(upper.map_or(limit.clone(), |v| v.min(limit)));
}
}
(lower, upper)
}
};
VariableSensitivity {
name: start.names[j].clone(),
value: values[j].clone(),
basic: row_of[j].is_some(),
reduced_cost: d[j].clone(),
cost_range: Interval::around(c, lower, upper),
}
})
.collect();
Some(Sensitivity {
basic_variables: basis.iter().map(|&j| start.names[j].clone()).collect(),
from_final_tableau: matches!(cells, Cells::FinalTableau { .. }),
constraints,
variables,
degenerate: xb.iter().any(Zero::is_zero),
alternative_optima: nonbasic.iter().any(|&l| d[l].is_zero()),
})
}
fn inverse(mut a: Vec<Vec<Rational>>) -> Option<Vec<Vec<Rational>>> {
let size = a.len();
let mut inv: Vec<Vec<Rational>> = (0..size)
.map(|i| {
(0..size)
.map(|j| {
if i == j {
Rational::one()
} else {
Rational::zero()
}
})
.collect()
})
.collect();
for col in 0..size {
let pivot = (col..size).find(|&r| !a[r][col].is_zero())?;
a.swap(col, pivot);
inv.swap(col, pivot);
let p = a[col][col].clone();
for v in a[col].iter_mut().chain(inv[col].iter_mut()) {
*v /= &p;
}
for r in 0..size {
if r == col || a[r][col].is_zero() {
continue;
}
let factor = a[r][col].clone();
for c in 0..size {
let (sub_a, sub_inv) = (&factor * &a[col][c], &factor * &inv[col][c]);
a[r][c] -= sub_a;
inv[r][c] -= sub_inv;
}
}
}
Some(inv)
}