use num_bigint::BigInt;
use num_traits::{Signed, Zero};
use crate::api::expr::Ex;
use crate::api::poly_ex::Poly;
use crate::base::errors::SymplexError;
use crate::domains::exact_matrix::QMatrix;
use crate::domains::linprog::{LpProblem, LpStatus, Q};
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::InvalidArgument {
operation,
reason: reason.into(),
}
}
pub type BoundingBox = Vec<(Option<Q>, Option<Q>)>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HalfSpace {
pub coeffs: Vec<Q>,
pub constant: Q,
}
impl HalfSpace {
pub fn value(&self, x: &[Q]) -> Q {
self.coeffs
.iter()
.zip(x)
.fold(self.constant.clone(), |acc, (a, xi)| acc + a * xi)
}
pub fn contains(&self, x: &[Q]) -> bool {
!self.value(x).is_negative()
}
pub fn flipped(&self) -> HalfSpace {
HalfSpace {
coeffs: self.coeffs.iter().map(|c| -c).collect(),
constant: -&self.constant,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Polytope {
dim: usize,
halfspaces: Vec<HalfSpace>,
}
impl Polytope {
pub fn new(halfspaces: Vec<HalfSpace>) -> Result<Self, SymplexError> {
let Some(first) = halfspaces.first() else {
return Err(invalid(
"Polytope::new",
"at least one half-space is required",
));
};
let dim = first.coeffs.len();
if dim == 0 {
return Err(invalid(
"Polytope::new",
"half-spaces need at least one coordinate",
));
}
if let Some((i, h)) = halfspaces
.iter()
.enumerate()
.find(|(_, h)| h.coeffs.len() != dim)
{
return Err(invalid(
"Polytope::new",
format!(
"half-space {i} has {} coefficients, expected {dim}",
h.coeffs.len()
),
));
}
Ok(Polytope { dim, halfspaces })
}
pub fn from_rows(rows: &[(Vec<Q>, Q)]) -> Result<Self, SymplexError> {
Self::new(
rows.iter()
.map(|(c, k)| HalfSpace {
coeffs: c.clone(),
constant: k.clone(),
})
.collect(),
)
}
pub fn from_exprs(hyps: &[Ex], vars: &[Ex]) -> Result<Self, SymplexError> {
const OP: &str = "Polytope::from_exprs";
if vars.is_empty() {
return Err(invalid(OP, "at least one variable is required"));
}
let gens: Vec<&Ex> = vars.iter().collect();
let mut halfspaces = Vec::with_capacity(hyps.len());
for h in hyps {
let p = Poly::try_new(h, &gens).map_err(|e| match e {
SymplexError::InvalidArgument { reason, .. } => invalid(OP, reason),
other => other,
})?;
if !p.is_linear() {
return Err(invalid(OP, format!("`{h}` is not affine in the variables")));
}
let mut coeffs = vec![Q::zero(); vars.len()];
let mut constant = Q::zero();
for (m, c) in p.terms_iter() {
let Some(c) = c.as_rational() else {
return Err(invalid(OP, format!("`{h}` has a symbolic coefficient")));
};
match m.iter().position(|&e| e == 1) {
Some(i) => coeffs[i] = c,
None => constant = c,
}
}
halfspaces.push(HalfSpace { coeffs, constant });
}
Self::new(halfspaces)
}
pub fn to_exprs(&self, vars: &[Ex]) -> Result<Vec<Ex>, SymplexError> {
if vars.len() != self.dim {
return Err(invalid(
"Polytope::to_exprs",
format!(
"{} variables for a {}-dimensional polytope",
vars.len(),
self.dim
),
));
}
let Some(first) = vars.first() else {
return Err(invalid(
"Polytope::to_exprs",
"at least one variable is required",
));
};
let ctx = first.context();
Ok(self
.halfspaces
.iter()
.map(|h| {
let mut e = ctx.from_ratio(h.constant.clone());
for (a, v) in h.coeffs.iter().zip(vars) {
if !a.is_zero() {
e += ctx.from_ratio(a.clone()) * v;
}
}
e
})
.collect())
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn halfspaces(&self) -> &[HalfSpace] {
&self.halfspaces
}
pub fn num_halfspaces(&self) -> usize {
self.halfspaces.len()
}
pub fn contains(&self, x: &[Q]) -> bool {
x.len() == self.dim && self.halfspaces.iter().all(|h| h.contains(x))
}
pub fn with_halfspace(&self, coeffs: &[Q], constant: Q) -> Polytope {
match self.try_with_halfspace(coeffs, constant) {
Ok(p) => p,
Err(e) => panic!("Polytope::with_halfspace: {e}"),
}
}
pub fn try_with_halfspace(&self, coeffs: &[Q], constant: Q) -> Result<Polytope, SymplexError> {
if coeffs.len() != self.dim {
return Err(invalid(
"Polytope::with_halfspace",
format!(
"{} coefficients for a {}-dimensional polytope",
coeffs.len(),
self.dim
),
));
}
let mut hs = self.halfspaces.clone();
hs.push(HalfSpace {
coeffs: coeffs.to_vec(),
constant,
});
Ok(Polytope {
dim: self.dim,
halfspaces: hs,
})
}
pub fn split(&self, coeffs: &[Q], constant: Q) -> (Polytope, Polytope) {
let pos = self.with_halfspace(coeffs, constant.clone());
let neg_coeffs: Vec<Q> = coeffs.iter().map(|c| -c).collect();
let neg = self.with_halfspace(&neg_coeffs, -constant);
(pos, neg)
}
fn minimize(&self, c: &[Q]) -> Result<Option<(Q, Vec<Q>)>, SymplexError> {
let mut lp = LpProblem::minimize(c.to_vec());
for i in 0..self.dim {
lp = lp.free(i);
}
for h in &self.halfspaces {
lp = lp.ge(h.coeffs.clone(), -&h.constant);
}
let sol = lp.solve()?;
Ok(match sol.status {
LpStatus::Optimal => sol.objective.map(|v| (v, sol.x)),
_ => None,
})
}
pub fn is_empty(&self) -> Result<bool, SymplexError> {
let zero = vec![Q::zero(); self.dim];
Ok(self.minimize(&zero)?.is_none())
}
pub fn any_point(&self) -> Result<Option<Vec<Q>>, SymplexError> {
let zero = vec![Q::zero(); self.dim];
Ok(self.minimize(&zero)?.map(|(_, x)| x))
}
pub fn bounding_box(&self) -> Result<Option<BoundingBox>, SymplexError> {
if self.is_empty()? {
return Ok(None);
}
let mut out = Vec::with_capacity(self.dim);
for i in 0..self.dim {
let mut c = vec![Q::zero(); self.dim];
c[i] = Q::from_integer(BigInt::from(1));
let lo = self.minimize(&c)?.map(|(v, _)| v);
c[i] = Q::from_integer(BigInt::from(-1));
let hi = self.minimize(&c)?.map(|(v, _)| -v);
out.push((lo, hi));
}
Ok(Some(out))
}
pub fn is_bounded(&self) -> Result<bool, SymplexError> {
Ok(match self.bounding_box()? {
None => true,
Some(b) => b.iter().all(|(lo, hi)| lo.is_some() && hi.is_some()),
})
}
pub fn vertices(&self) -> Result<Vec<Vec<Q>>, SymplexError> {
let n = self.dim;
let m = self.halfspaces.len();
if m < n {
return Ok(Vec::new());
}
let mut found: Vec<Vec<Q>> = Vec::new();
let mut idx: Vec<usize> = (0..n).collect();
loop {
let a = QMatrix::from_fn(n, n, |r, c| self.halfspaces[idx[r]].coeffs[c].clone());
let b = QMatrix::from_fn(n, 1, |r, _| -&self.halfspaces[idx[r]].constant);
if let Ok(x) = a.solve(&b) {
let point: Vec<Q> = x.col(0);
if self.contains(&point) && !found.contains(&point) {
found.push(point);
}
}
let mut k = n;
loop {
if k == 0 {
return Ok(found);
}
k -= 1;
if idx[k] < m - n + k {
idx[k] += 1;
for j in (k + 1)..n {
idx[j] = idx[j - 1] + 1;
}
break;
}
}
}
}
pub fn irredundant(&self) -> Result<Polytope, SymplexError> {
let verts = self.vertices()?;
if verts.is_empty() {
return Err(invalid(
"Polytope::irredundant",
"the polytope has no vertices",
));
}
let kept: Vec<HalfSpace> = self
.halfspaces
.iter()
.filter(|h| verts.iter().any(|v| h.value(v).is_zero()))
.cloned()
.collect();
Polytope::new(kept)
}
pub fn vertex_centroid(&self) -> Result<Option<Vec<Q>>, SymplexError> {
let verts = self.vertices()?;
Ok(centroid(&verts, self.dim))
}
pub fn volume(&self) -> Result<Q, SymplexError> {
const OP: &str = "Polytope::volume";
if !self.is_bounded()? {
return Err(invalid(OP, "the polyhedron is unbounded"));
}
Ok(self.volume_bounded())
}
fn volume_bounded(&self) -> Q {
let n = self.dim;
let verts = match self.vertices() {
Ok(v) => v,
Err(_) => return Q::zero(),
};
if verts.len() < n + 1 {
return Q::zero(); }
if n == 1 {
let lo = verts.iter().map(|v| &v[0]).min().cloned();
let hi = verts.iter().map(|v| &v[0]).max().cloned();
return match (lo, hi) {
(Some(l), Some(h)) => h - l,
_ => Q::zero(),
};
}
let Some(o) = centroid(&verts, n) else {
return Q::zero();
};
let mut seen: Vec<HalfSpace> = Vec::new();
let mut total = Q::zero();
for h in &self.halfspaces {
let Some(norm) = normalized(h) else {
continue; };
if seen.contains(&norm) {
continue;
}
seen.push(norm);
let on: usize = verts.iter().filter(|v| h.value(v).is_zero()).count();
if on < n {
continue;
}
let Some(k) = (0..n)
.filter(|&i| !h.coeffs[i].is_zero())
.max_by(|&i, &j| h.coeffs[i].abs().cmp(&h.coeffs[j].abs()))
else {
continue;
};
let ak = &h.coeffs[k];
let mut facet_rows: Vec<HalfSpace> = Vec::new();
for g in &self.halfspaces {
let gk = &g.coeffs[k];
let mut coeffs: Vec<Q> = Vec::with_capacity(n - 1);
for i in (0..n).filter(|&i| i != k) {
coeffs.push(&g.coeffs[i] - gk * &h.coeffs[i] / ak);
}
let constant = &g.constant - gk * &h.constant / ak;
if coeffs.iter().all(Zero::is_zero) {
continue; }
facet_rows.push(HalfSpace { coeffs, constant });
}
let Ok(facet) = Polytope::new(facet_rows) else {
continue;
};
let facet_vol = facet.volume_bounded();
if facet_vol.is_zero() {
continue;
}
total += h.value(&o) / ak.abs() * facet_vol;
}
total / Q::from_integer(BigInt::from(n))
}
}
fn normalized(h: &HalfSpace) -> Option<HalfSpace> {
let first = h.coeffs.iter().find(|c| !c.is_zero())?;
Some(HalfSpace {
coeffs: h.coeffs.iter().map(|c| c / first).collect(),
constant: &h.constant / first,
})
}
fn centroid(points: &[Vec<Q>], dim: usize) -> Option<Vec<Q>> {
if points.is_empty() {
return None;
}
let n = Q::from_integer(BigInt::from(points.len()));
Some(
(0..dim)
.map(|i| points.iter().fold(Q::zero(), |acc, p| acc + &p[i]) / &n)
.collect(),
)
}
#[derive(Clone, Debug)]
pub struct ParametricPolytope {
hyps: Vec<Poly>,
vars: Vec<Ex>,
param: Ex,
cache: std::collections::BTreeMap<Q, Instance>,
}
type Instance = (Polytope, Option<Vec<Vec<Q>>>);
impl ParametricPolytope {
pub fn new(hyps: &[Ex], vars: &[Ex], param: &Ex) -> Result<Self, SymplexError> {
const OP: &str = "ParametricPolytope::new";
if hyps.is_empty() {
return Err(invalid(OP, "at least one hypothesis is required"));
}
if vars.is_empty() {
return Err(invalid(OP, "at least one variable is required"));
}
if vars.contains(param) {
return Err(invalid(
OP,
"the parameter must not be one of the variables",
));
}
let mut gens: Vec<&Ex> = vars.iter().collect();
gens.push(param);
let n = vars.len();
let mut polys = Vec::with_capacity(hyps.len());
for h in hyps {
let p = Poly::try_new(h, &gens).map_err(|e| match e {
SymplexError::InvalidArgument { reason, .. } => invalid(OP, reason),
other => other,
})?;
if !p.has_rational_coeffs() {
return Err(invalid(OP, format!("`{h}` has a symbolic coefficient")));
}
if p.terms_iter().any(|(m, _)| m[..n].iter().sum::<u32>() > 1) {
return Err(invalid(OP, format!("`{h}` is not affine in the variables")));
}
polys.push(p);
}
Ok(ParametricPolytope {
hyps: polys,
vars: vars.to_vec(),
param: param.clone(),
cache: std::collections::BTreeMap::new(),
})
}
pub fn vars(&self) -> &[Ex] {
&self.vars
}
pub fn param(&self) -> &Ex {
&self.param
}
pub fn hyps(&self) -> &[Poly] {
&self.hyps
}
pub fn at(&self, value: &Q) -> Result<Polytope, SymplexError> {
let ctx = self.param.context();
let val = ctx.from_ratio(value.clone());
let n = self.vars.len();
let mut rows = Vec::with_capacity(self.hyps.len());
for h in &self.hyps {
let p = h.eval_gen(&self.param, &val)?;
let mut coeffs = vec![Q::zero(); n];
let mut constant = Q::zero();
for (m, c) in p.terms_iter() {
let c = c.as_rational().ok_or_else(|| {
invalid(
"ParametricPolytope::at",
"internal: non-rational coefficient",
)
})?;
match m.iter().position(|&e| e == 1) {
Some(i) => coeffs[i] = c,
None => constant = c,
}
}
rows.push(HalfSpace { coeffs, constant });
}
Polytope::new(rows)
}
fn entry(&mut self, value: &Q) -> Result<&mut Instance, SymplexError> {
if !self.cache.contains_key(value) {
let p = self.at(value)?;
self.cache.insert(value.clone(), (p, None));
}
self.cache
.get_mut(value)
.ok_or_else(|| invalid("ParametricPolytope::at", "internal: cache"))
}
pub fn polytope_at(&mut self, value: &Q) -> Result<&Polytope, SymplexError> {
Ok(&self.entry(value)?.0)
}
pub fn vertices_at(&mut self, value: &Q) -> Result<&[Vec<Q>], SymplexError> {
let e = self.entry(value)?;
if e.1.is_none() {
e.1 = Some(e.0.vertices()?);
}
Ok(e.1.as_deref().unwrap_or(&[]))
}
pub fn volume_at(&mut self, value: &Q) -> Result<Q, SymplexError> {
self.polytope_at(value)?.volume()
}
pub fn is_empty_at(&mut self, value: &Q) -> Result<bool, SymplexError> {
self.polytope_at(value)?.is_empty()
}
pub fn contains_at(&mut self, value: &Q, x: &[Q]) -> Result<bool, SymplexError> {
Ok(self.polytope_at(value)?.contains(x))
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domains::linprog::{q, qi};
fn cube() -> Polytope {
let mut rows = Vec::new();
for i in 0..3 {
let mut e = vec![qi(0); 3];
e[i] = qi(1);
rows.push((e.clone(), qi(0)));
let mut f = vec![qi(0); 3];
f[i] = qi(-1);
rows.push((f, qi(1)));
}
Polytope::from_rows(&rows).unwrap()
}
#[test]
fn cube_vertices_volume_box() {
let c = cube();
let v = c.vertices().unwrap();
assert_eq!(v.len(), 8);
assert_eq!(c.volume().unwrap(), qi(1));
assert!(c.is_bounded().unwrap());
assert!(!c.is_empty().unwrap());
let bb = c.bounding_box().unwrap().unwrap();
assert_eq!(bb, vec![(Some(qi(0)), Some(qi(1))); 3]);
assert_eq!(c.vertex_centroid().unwrap().unwrap(), vec![q(1, 2); 3]);
assert!(c.contains(&[q(1, 2), q(1, 3), q(1, 4)]));
assert!(!c.contains(&[q(3, 2), q(1, 3), q(1, 4)]));
assert!(!c.contains(&[q(1, 2), q(1, 3)]));
let (lo, hi) = c.split(&[qi(0), qi(0), qi(-1)], q(1, 3));
assert_eq!(lo.volume().unwrap(), q(1, 3));
assert_eq!(hi.volume().unwrap(), q(2, 3));
assert_eq!(lo.vertices().unwrap().len(), 8);
}
#[test]
fn simplex_and_degenerate_vertex() {
let mut rows = vec![
(vec![qi(1), qi(0), qi(0)], qi(0)),
(vec![qi(0), qi(1), qi(0)], qi(0)),
(vec![qi(0), qi(0), qi(1)], qi(0)),
(vec![qi(-1), qi(-1), qi(-1)], qi(1)),
];
let t = Polytope::from_rows(&rows).unwrap();
assert_eq!(t.vertices().unwrap().len(), 4);
assert_eq!(t.volume().unwrap(), q(1, 6));
rows.push((vec![qi(-1), qi(-1), qi(0)], qi(1)));
let t2 = Polytope::from_rows(&rows).unwrap();
assert_eq!(t2.vertices().unwrap().len(), 4);
assert_eq!(t2.volume().unwrap(), q(1, 6));
assert_eq!(t2.irredundant().unwrap().num_halfspaces(), 5); }
#[test]
fn empty_and_unbounded() {
let empty = Polytope::from_rows(&[(vec![qi(1)], qi(0)), (vec![qi(-1)], qi(-1))]).unwrap();
assert!(empty.is_empty().unwrap());
assert!(empty.vertices().unwrap().is_empty());
assert_eq!(empty.volume().unwrap(), qi(0));
assert!(empty.bounding_box().unwrap().is_none());
assert!(empty.any_point().unwrap().is_none());
assert!(empty.irredundant().is_err());
let quadrant =
Polytope::from_rows(&[(vec![qi(1), qi(0)], qi(0)), (vec![qi(0), qi(1)], qi(0))])
.unwrap();
assert!(!quadrant.is_bounded().unwrap());
assert_eq!(quadrant.vertices().unwrap(), vec![vec![qi(0), qi(0)]]);
assert!(quadrant.volume().is_err());
let bb = quadrant.bounding_box().unwrap().unwrap();
assert_eq!(bb[0], (Some(qi(0)), None));
assert!(quadrant.any_point().unwrap().is_some());
}
#[test]
fn segment_and_polygon() {
let seg = Polytope::from_rows(&[(vec![qi(1)], q(1, 3)), (vec![qi(-1)], qi(2))]).unwrap();
assert_eq!(seg.volume().unwrap(), q(7, 3));
let diamond = Polytope::from_rows(&[
(vec![qi(-1), qi(-1)], qi(1)),
(vec![qi(1), qi(-1)], qi(1)),
(vec![qi(-1), qi(1)], qi(1)),
(vec![qi(1), qi(1)], qi(1)),
])
.unwrap();
assert_eq!(diamond.vertices().unwrap().len(), 4);
assert_eq!(diamond.volume().unwrap(), qi(2));
assert_eq!(diamond.irredundant().unwrap(), diamond);
}
#[test]
fn from_exprs_and_errors() {
let ctx = crate::api::context::Context::new();
let (r, t) = (ctx.symbol("r"), ctx.symbol("t"));
let p =
Polytope::from_exprs(&[r.clone(), &t - &r, 1 - &t], &[r.clone(), t.clone()]).unwrap();
assert_eq!(p.volume().unwrap(), q(1, 2));
assert_eq!(
p.halfspaces()[1],
HalfSpace {
coeffs: vec![qi(-1), qi(1)],
constant: qi(0)
}
);
let back = p.to_exprs(&[r.clone(), t.clone()]).unwrap();
assert_eq!(back, vec![r.clone(), &t - &r, 1 - &t]);
assert!(p.to_exprs(std::slice::from_ref(&r)).is_err());
assert!(Polytope::from_exprs(&[r.powi(2)], std::slice::from_ref(&r)).is_err());
assert!(Polytope::from_exprs(&[&r * ctx.symbol("a")], std::slice::from_ref(&r)).is_err());
assert!(Polytope::from_exprs(std::slice::from_ref(&r), &[]).is_err());
assert!(Polytope::new(vec![]).is_err());
assert!(Polytope::from_rows(&[(vec![qi(1)], qi(0)), (vec![qi(1), qi(2)], qi(0))]).is_err());
assert!(p.try_with_halfspace(&[qi(1)], qi(0)).is_err());
let four = Polytope::from_rows(&[(vec![qi(1); 4], qi(0))]).unwrap();
assert!(four.volume().is_err());
assert_eq!(
HalfSpace {
coeffs: vec![qi(2)],
constant: qi(-1)
}
.flipped(),
HalfSpace {
coeffs: vec![qi(-2)],
constant: qi(1)
}
);
}
}