1use std::fmt;
2
3use num_traits::{One, Signed, Zero};
4
5use crate::num::Rational;
6use crate::problem::{Problem, Relation, Sense};
7use crate::trace::write_table;
8
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[derive(Clone, Debug, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct Sensitivity {
37 pub basic_variables: Vec<String>,
39 pub from_final_tableau: bool,
48 pub constraints: Vec<ConstraintSensitivity>,
50 pub variables: Vec<VariableSensitivity>,
52 pub degenerate: bool,
55 pub alternative_optima: bool,
58}
59
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62#[derive(Clone, Debug, PartialEq, Eq)]
63#[non_exhaustive]
64pub struct ConstraintSensitivity {
65 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
73 pub shadow_price: Rational,
74 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
77 pub slack: Rational,
78 pub binding: bool,
80 pub rhs_range: Interval,
85 pub redundant: bool,
89 pub source: Option<Source>,
91}
92
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95#[derive(Clone, Debug, PartialEq, Eq)]
96#[non_exhaustive]
97pub struct VariableSensitivity {
98 pub name: String,
99 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
100 pub value: Rational,
101 pub basic: bool,
103 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::one"))]
106 pub reduced_cost: Rational,
107 pub cost_range: Interval,
110}
111
112#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
116#[derive(Clone, Debug, PartialEq, Eq)]
117#[non_exhaustive]
118pub struct Interval {
119 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::option"))]
121 pub lower: Option<Rational>,
122 #[cfg_attr(feature = "serde", serde(with = "crate::serde_rational::option"))]
124 pub upper: Option<Rational>,
125}
126
127impl Interval {
128 pub fn contains(&self, value: &Rational) -> bool {
129 self.lower.as_ref().is_none_or(|l| l <= value)
130 && self.upper.as_ref().is_none_or(|u| value <= u)
131 }
132
133 fn around(
135 value: &Rational,
136 lower_delta: Option<Rational>,
137 upper_delta: Option<Rational>,
138 ) -> Self {
139 Interval {
140 lower: lower_delta.map(|d| value + d),
141 upper: upper_delta.map(|d| value + d),
142 }
143 }
144}
145
146impl fmt::Display for Interval {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 match &self.lower {
149 Some(l) => write!(f, "[{l}, ")?,
150 None => write!(f, "(-inf, ")?,
151 }
152 match &self.upper {
153 Some(u) => write!(f, "{u}]"),
154 None => write!(f, "+inf)"),
155 }
156 }
157}
158
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
167#[derive(Clone, Debug, PartialEq, Eq)]
168#[non_exhaustive]
169pub struct Source {
170 pub column: usize,
172 pub name: String,
174 pub negated: bool,
176}
177
178impl fmt::Display for Sensitivity {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 let mut table = vec![
181 [
182 "constraint",
183 "shadow price",
184 "slack",
185 "rhs range",
186 "read from",
187 ]
188 .map(String::from)
189 .to_vec(),
190 ];
191 for (i, c) in self.constraints.iter().enumerate() {
192 let source = match &c.source {
193 Some(s) if s.negated => format!("-({})", s.name),
194 Some(s) => s.name.clone(),
195 None if c.redundant => "redundant".to_string(),
196 None => "computed".to_string(),
197 };
198 table.push(vec![
199 format!("#{}", i + 1),
200 c.shadow_price.to_string(),
201 c.slack.to_string(),
202 c.rhs_range.to_string(),
203 source,
204 ]);
205 }
206 write_table(f, &table)?;
207 writeln!(f)?;
208
209 let mut table = vec![
210 ["variable", "value", "reduced cost", "cost range"]
211 .map(String::from)
212 .to_vec(),
213 ];
214 for v in &self.variables {
215 table.push(vec![
216 v.name.clone(),
217 v.value.to_string(),
218 v.reduced_cost.to_string(),
219 v.cost_range.to_string(),
220 ]);
221 }
222 write_table(f, &table)?;
223
224 if self.degenerate {
225 writeln!(
226 f,
227 "\nnote: degenerate basis, shadow prices hold in one direction only"
228 )?;
229 }
230 if self.alternative_optima {
231 writeln!(
232 f,
233 "\nnote: a nonbasic column has zero reduced cost, other optima exist"
234 )?;
235 }
236 Ok(())
237 }
238}
239
240pub(crate) enum Cells {
242 FinalTableau { artificial_columns: bool },
244 None,
246}
247
248pub(crate) struct StandardForm<'a> {
250 pub rows: &'a [Vec<Rational>],
251 pub rhs: &'a [Rational],
252 pub names: &'a [String],
253 pub artificial: &'a [bool],
254}
255
256pub(crate) fn analyze(
264 problem: &Problem,
265 start: StandardForm<'_>,
266 basis: &[usize],
267 removed: &[usize],
268 cells: Cells,
269) -> Option<Sensitivity> {
270 let n = problem.num_vars();
271 let m = problem.constraints.len();
272 let a = start.rows;
273 let real = start
275 .artificial
276 .iter()
277 .position(|&art| art)
278 .unwrap_or(start.artificial.len());
279 let kept: Vec<usize> = (0..m).filter(|i| !removed.contains(i)).collect();
280 if kept.len() != basis.len() {
281 return None;
282 }
283
284 let b_matrix: Vec<Vec<Rational>> = kept
285 .iter()
286 .map(|&i| basis.iter().map(|&j| a[i][j].clone()).collect())
287 .collect();
288 let binv = inverse(b_matrix)?;
290 let cost = |j: usize| {
291 if j < n {
292 problem.objective[j].clone()
293 } else {
294 Rational::zero()
295 }
296 };
297
298 let xb: Vec<Rational> = binv
300 .iter()
301 .map(|row| row.iter().zip(&kept).map(|(v, &i)| v * &start.rhs[i]).sum())
302 .collect();
303 let mut w = vec![Rational::zero(); m];
304 for (k, &i) in kept.iter().enumerate() {
305 w[i] = basis
306 .iter()
307 .enumerate()
308 .map(|(r, &j)| cost(j) * &binv[r][k])
309 .sum();
310 }
311 let d: Vec<Rational> = (0..real)
313 .map(|j| (0..m).map(|i| &w[i] * &a[i][j]).sum::<Rational>() - cost(j))
314 .collect();
315 let mut row_of = vec![None; real];
316 for (r, &j) in basis.iter().enumerate() {
317 row_of[j] = Some(r);
318 }
319 let nonbasic: Vec<usize> = (0..real).filter(|&j| row_of[j].is_none()).collect();
320 let alpha = |r: usize, l: usize| -> Rational {
322 kept.iter()
323 .enumerate()
324 .map(|(k, &i)| &binv[r][k] * &a[i][l])
325 .sum()
326 };
327
328 let mut pinned = vec![false; kept.len()];
331 for &r in removed {
332 for (k, pin) in pinned.iter_mut().enumerate() {
333 let lambda: Rational = basis
334 .iter()
335 .enumerate()
336 .map(|(row, &j)| &a[r][j] * &binv[row][k])
337 .sum();
338 *pin |= !lambda.is_zero();
339 }
340 }
341
342 let values: Vec<Rational> = (0..n)
343 .map(|j| row_of[j].map_or_else(Rational::zero, |r| xb[r].clone()))
344 .collect();
345
346 let constraints = problem
347 .constraints
348 .iter()
349 .enumerate()
350 .map(|(i, c)| {
351 let flip = if c.rhs.is_negative() {
353 -Rational::one()
354 } else {
355 Rational::one()
356 };
357 let activity: Rational = c.coeffs.iter().zip(&values).map(|(a, x)| a * x).sum();
358 let slack = match c.relation {
359 Relation::Le => &c.rhs - &activity,
360 Relation::Ge => &activity - &c.rhs,
361 Relation::Eq => Rational::zero(),
362 };
363
364 let point = || Interval::around(&c.rhs, Some(Rational::zero()), Some(Rational::zero()));
365 let Some(k) = kept.iter().position(|&kept| kept == i) else {
366 return ConstraintSensitivity {
367 shadow_price: Rational::zero(),
368 binding: true,
369 slack,
370 rhs_range: point(),
371 redundant: true,
372 source: None,
373 };
374 };
375
376 let mut lower: Option<Rational> = None;
379 let mut upper: Option<Rational> = None;
380 for (r, x) in xb.iter().enumerate() {
381 let u = &binv[r][k];
382 if u.is_zero() {
383 continue;
384 }
385 let limit = -x / u;
386 if u.is_positive() {
387 lower = Some(lower.map_or(limit.clone(), |l| l.max(limit)));
388 } else {
389 upper = Some(upper.map_or(limit.clone(), |v| v.min(limit)));
390 }
391 }
392 let rhs_range = if pinned[k] {
393 point()
394 } else if flip.is_one() {
395 Interval::around(&c.rhs, lower, upper)
396 } else {
397 Interval::around(&c.rhs, upper.map(|v| -v), lower.map(|v| -v))
398 };
399
400 let slack_column = (n..real)
403 .find(|&j| !a[i][j].is_zero())
404 .map(|j| (j, a[i][j].clone()));
405 let artificial_column = || {
406 (real..start.names.len())
407 .find(|&j| !a[i][j].is_zero())
408 .map(|j| (j, Rational::one()))
409 };
410 let column = match cells {
411 Cells::FinalTableau { artificial_columns } => {
412 slack_column.or_else(|| artificial_columns.then(artificial_column).flatten())
413 }
414 Cells::None => None,
415 };
416 let source = column.map(|(j, sign)| Source {
417 column: j,
418 name: start.names[j].clone(),
419 negated: (sign * &flip).is_negative(),
420 });
421
422 ConstraintSensitivity {
423 shadow_price: &flip * &w[i],
424 binding: slack.is_zero(),
425 slack,
426 rhs_range,
427 redundant: false,
428 source,
429 }
430 })
431 .collect();
432
433 let variables = (0..n)
434 .map(|j| {
435 let c = &problem.objective[j];
436 let (lower, upper) = match row_of[j] {
440 None => match problem.sense {
441 Sense::Maximize => (None, Some(d[j].clone())),
442 Sense::Minimize => (Some(d[j].clone()), None),
443 },
444 Some(r) => {
445 let mut lower: Option<Rational> = None;
446 let mut upper: Option<Rational> = None;
447 for &l in &nonbasic {
448 let al = alpha(r, l);
449 if al.is_zero() {
450 continue;
451 }
452 let limit = -&d[l] / &al;
453 let is_lower = match problem.sense {
454 Sense::Maximize => al.is_positive(),
455 Sense::Minimize => al.is_negative(),
456 };
457 if is_lower {
458 lower = Some(lower.map_or(limit.clone(), |v| v.max(limit)));
459 } else {
460 upper = Some(upper.map_or(limit.clone(), |v| v.min(limit)));
461 }
462 }
463 (lower, upper)
464 }
465 };
466 VariableSensitivity {
467 name: start.names[j].clone(),
468 value: values[j].clone(),
469 basic: row_of[j].is_some(),
470 reduced_cost: d[j].clone(),
471 cost_range: Interval::around(c, lower, upper),
472 }
473 })
474 .collect();
475
476 Some(Sensitivity {
477 basic_variables: basis.iter().map(|&j| start.names[j].clone()).collect(),
478 from_final_tableau: matches!(cells, Cells::FinalTableau { .. }),
479 constraints,
480 variables,
481 degenerate: xb.iter().any(Zero::is_zero),
482 alternative_optima: nonbasic.iter().any(|&l| d[l].is_zero()),
483 })
484}
485
486fn inverse(mut a: Vec<Vec<Rational>>) -> Option<Vec<Vec<Rational>>> {
488 let size = a.len();
489 let mut inv: Vec<Vec<Rational>> = (0..size)
490 .map(|i| {
491 (0..size)
492 .map(|j| {
493 if i == j {
494 Rational::one()
495 } else {
496 Rational::zero()
497 }
498 })
499 .collect()
500 })
501 .collect();
502 for col in 0..size {
503 let pivot = (col..size).find(|&r| !a[r][col].is_zero())?;
504 a.swap(col, pivot);
505 inv.swap(col, pivot);
506 let p = a[col][col].clone();
507 for v in a[col].iter_mut().chain(inv[col].iter_mut()) {
508 *v /= &p;
509 }
510 for r in 0..size {
511 if r == col || a[r][col].is_zero() {
512 continue;
513 }
514 let factor = a[r][col].clone();
515 for c in 0..size {
516 let (sub_a, sub_inv) = (&factor * &a[col][c], &factor * &inv[col][c]);
517 a[r][c] -= sub_a;
518 inv[r][c] -= sub_inv;
519 }
520 }
521 }
522 Some(inv)
523}