use std::sync::OnceLock;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, Zero};
use crate::api::expr::Ex;
use crate::api::poly_ex::Poly;
use crate::base::errors::SymplexError;
use crate::domains::exact_matrix::fraction_free_gauss_jordan;
use crate::domains::linprog::{LpProblem, LpStatus, Q};
use crate::poly::multipoly::{GrevLex, MultiPoly};
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 value_sign(&self, x: &[Q]) -> std::cmp::Ordering {
let mut num = self.constant.numer().clone();
let mut den = self.constant.denom().clone();
for (a, xi) in self.coeffs.iter().zip(x) {
if a.is_zero() || xi.is_zero() {
continue;
}
let tn = a.numer() * xi.numer();
let td = a.denom() * xi.denom();
num = num * &td + tn * &den;
den *= td;
}
num.sign().cmp(&num_bigint::Sign::NoSign)
}
pub fn contains(&self, x: &[Q]) -> bool {
self.value_sign(x) != std::cmp::Ordering::Less
}
pub fn is_tight(&self, x: &[Q]) -> bool {
self.value_sign(x) == std::cmp::Ordering::Equal
}
pub fn flipped(&self) -> HalfSpace {
HalfSpace {
coeffs: self.coeffs.iter().map(|c| -c).collect(),
constant: -&self.constant,
}
}
pub fn is_trivial(&self) -> bool {
self.coeffs.iter().all(Zero::is_zero)
}
pub fn normalized(&self) -> HalfSpace {
match self.coeffs.iter().find(|c| !c.is_zero()) {
None => self.clone(),
Some(lead) => HalfSpace {
coeffs: self.coeffs.iter().map(|c| c / lead).collect(),
constant: &self.constant / lead,
},
}
}
pub fn same_hyperplane(&self, other: &HalfSpace) -> bool {
self.coeffs.len() == other.coeffs.len() && self.normalized() == other.normalized()
}
}
#[derive(Clone)]
pub struct Polytope {
dim: usize,
halfspaces: Vec<HalfSpace>,
vertex_cache: OnceLock<Vec<Vec<Q>>>,
}
impl PartialEq for Polytope {
fn eq(&self, other: &Self) -> bool {
self.dim == other.dim && self.halfspaces == other.halfspaces
}
}
impl Eq for Polytope {}
impl std::fmt::Debug for Polytope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Polytope")
.field("dim", &self.dim)
.field("halfspaces", &self.halfspaces)
.finish()
}
}
struct IntHalfSpace {
a: Vec<BigInt>,
b: BigInt,
}
impl IntHalfSpace {
fn new(h: &HalfSpace) -> Self {
let s = h
.coeffs
.iter()
.chain(std::iter::once(&h.constant))
.fold(BigInt::one(), |l, q| l.lcm(q.denom()));
let scale = |q: &Q| q.numer() * (&s / q.denom());
IntHalfSpace {
a: h.coeffs.iter().map(scale).collect(),
b: scale(&h.constant),
}
}
fn value_at(&self, x: &[BigInt], d: &BigInt) -> BigInt {
self.a
.iter()
.zip(x)
.fold(&self.b * d, |acc, (a, xi)| acc + a * xi)
}
fn hyperplane_key(&self) -> Option<Vec<BigInt>> {
let lead = self.a.iter().find(|c| !c.is_zero())?;
let mut g = self
.a
.iter()
.chain(std::iter::once(&self.b))
.fold(BigInt::zero(), |g, c| g.gcd(c));
if lead.is_negative() {
g = -g;
}
Some(
self.a
.iter()
.chain(std::iter::once(&self.b))
.map(|c| c / &g)
.collect(),
)
}
}
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,
vertex_cache: OnceLock::new(),
})
}
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,
vertex_cache: OnceLock::new(),
})
}
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> {
if self.has_box_rows() {
return Ok(true);
}
Ok(match self.bounding_box()? {
None => true,
Some(b) => b.iter().all(|(lo, hi)| lo.is_some() && hi.is_some()),
})
}
fn has_box_rows(&self) -> bool {
let mut lo = vec![false; self.dim];
let mut hi = vec![false; self.dim];
for h in &self.halfspaces {
let mut nonzero = h.coeffs.iter().enumerate().filter(|(_, c)| !c.is_zero());
if let (Some((i, c)), None) = (nonzero.next(), nonzero.next()) {
if c.is_positive() {
lo[i] = true;
} else {
hi[i] = true;
}
}
}
lo.iter().all(|&b| b) && hi.iter().all(|&b| b)
}
fn max_slack(&self) -> Result<Option<(Q, Vec<Q>)>, SymplexError> {
let n = self.dim;
let mut c = vec![Q::zero(); n + 1];
c[n] = Q::one();
let mut lp = LpProblem::maximize(c);
for i in 0..n {
lp = lp.free(i);
}
lp = lp.bounds(n, Some(Q::zero()), Some(Q::one()));
for h in &self.halfspaces {
if h.is_trivial() {
if h.constant.is_negative() {
return Ok(None);
}
continue;
}
let mut row = h.coeffs.clone();
row.push(-Q::one());
lp = lp.ge(row, -&h.constant);
}
let sol = lp.solve()?;
Ok(match sol.status {
LpStatus::Optimal => {
let mut x = sol.x;
let t = x.pop().unwrap_or_else(Q::zero);
Some((t, x))
}
_ => None,
})
}
pub fn is_full_dimensional(&self) -> Result<bool, SymplexError> {
Ok(self.max_slack()?.is_some_and(|(t, _)| t.is_positive()))
}
pub fn interior_point(&self) -> Result<Option<Vec<Q>>, SymplexError> {
Ok(self
.max_slack()?
.filter(|(t, _)| t.is_positive())
.map(|(_, x)| x))
}
pub fn vertices(&self) -> Result<Vec<Vec<Q>>, SymplexError> {
Ok(self
.vertex_cache
.get_or_init(|| self.compute_vertices())
.clone())
}
fn compute_vertices(&self) -> Vec<Vec<Q>> {
let n = self.dim;
let rows: Vec<IntHalfSpace> = self.halfspaces.iter().map(IntHalfSpace::new).collect();
let mut planes: Vec<usize> = Vec::new();
let mut keys: Vec<Vec<BigInt>> = Vec::new();
for (i, r) in rows.iter().enumerate() {
if let Some(key) = r.hyperplane_key()
&& !keys.contains(&key)
{
keys.push(key);
planes.push(i);
}
}
let m = planes.len();
if m < n {
return Vec::new();
}
let mut found: Vec<(Vec<BigInt>, BigInt)> = Vec::new();
let width = n + 1;
let mut aug: Vec<BigInt> = Vec::with_capacity(n * width);
let mut idx: Vec<usize> = (0..n).collect();
loop {
aug.clear();
for &pi in &idx {
let r = &rows[planes[pi]];
aug.extend(r.a.iter().cloned());
aug.push(-&r.b);
}
let (pivots, d) = fraction_free_gauss_jordan(&mut aug, n, width, n);
if pivots.len() == n && !d.is_zero() {
let negative = d.is_negative();
let dd = if negative { -&d } else { d.clone() };
let x: Vec<BigInt> = (0..n)
.map(|i| {
let v = &aug[i * width + n];
if negative { -v } else { v.clone() }
})
.collect();
if rows.iter().all(|r| !r.value_at(&x, &dd).is_negative()) {
let g = x.iter().fold(dd.clone(), |g, xi| g.gcd(xi));
let canon = (x.iter().map(|xi| xi / &g).collect::<Vec<_>>(), &dd / &g);
if !found.contains(&canon) {
found.push(canon);
}
}
}
let mut k = n;
loop {
if k == 0 {
return found
.into_iter()
.map(|(x, d)| x.into_iter().map(|xi| Q::new(xi, d.clone())).collect())
.collect();
}
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 {
match self.vertices() {
Ok(verts) => volume_from_vertices(&self.halfspaces, self.dim, &verts),
Err(_) => Q::zero(),
}
}
}
fn volume_from_vertices(halfspaces: &[HalfSpace], n: usize, verts: &[Vec<Q>]) -> Q {
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 halfspaces {
let Some(norm) = normalized(h) else {
continue; };
if seen.contains(&norm) {
continue;
}
seen.push(norm);
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 on: Vec<Vec<Q>> = verts
.iter()
.filter(|v| h.is_tight(v))
.map(|v| {
v.iter()
.enumerate()
.filter(|(i, _)| *i != k)
.map(|(_, x)| x.clone())
.collect()
})
.collect();
if on.len() < n {
continue;
}
let ak = &h.coeffs[k];
let mut facet_rows: Vec<HalfSpace> = Vec::new();
for g in 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 facet_vol = volume_from_vertices(&facet_rows, n - 1, &on);
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>,
exact: Vec<MultiPoly<GrevLex>>,
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);
}
let exact = polys
.iter()
.map(|p| {
p.to_multipoly()
.ok_or_else(|| invalid(OP, "internal: non-rational coefficient"))
})
.collect::<Result<Vec<_>, _>>()?;
Ok(ParametricPolytope {
hyps: polys,
exact,
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 n = self.vars.len();
let mut rows = Vec::with_capacity(self.exact.len());
for h in &self.exact {
let (coeffs, constant) = h
.substitute(n, value)
.affine_form()
.ok_or_else(|| invalid("ParametricPolytope::at", "internal: not affine"))?;
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)
}
);
}
}