Skip to main content

oximo_core/
constraint.rs

1use oximo_expr::{Expr, ExprId};
2use smol_str::SmolStr;
3
4/// The sense of a constraint: less-than-or-equal, greater-than-or-equal, or equality.
5#[derive(Copy, Clone, Debug, PartialEq, Eq)]
6pub enum Sense {
7    Le,
8    Ge,
9    Eq,
10}
11
12#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
13pub struct ConstraintId(pub u32);
14
15impl ConstraintId {
16    #[inline]
17    pub fn index(self) -> usize {
18        self.0 as usize
19    }
20}
21
22/// A single algebraic constraint, canonicalized as the interval
23/// `lower <= lhs <= upper` with numeric bounds. RHS expressions are folded into
24/// `lhs` during construction, so backends only ever see this canonical shape.
25///
26/// The single-sided senses map onto the interval as `Le(rhs) => [-inf, rhs]`,
27/// `Ge(rhs) => [rhs, +inf]`, `Eq(rhs) => [rhs, rhs]`. A two-sided range with
28/// constant bounds is `[lo, hi]`. Use [`Constraint::as_single`] to recover the
29/// single-sided sense (for backends without native two-sided rows) and
30/// [`Constraint::is_range`] to detect a genuine range.
31#[derive(Clone, Debug)]
32pub struct Constraint {
33    pub name: SmolStr,
34    pub lhs: ExprId,
35    pub lower: f64,
36    pub upper: f64,
37    pub active: bool,
38}
39
40impl Constraint {
41    /// The two bounds are equal, i.e. this is an equality row. Uses `total_cmp`
42    /// for an exact comparison: the bounds are literals (`Eq` copies the same
43    /// value into both).
44    fn is_equality(&self) -> bool {
45        self.lower.total_cmp(&self.upper).is_eq()
46    }
47
48    /// Whether this is a genuine two-sided range (both bounds finite and not an
49    /// equality), as opposed to a single-sided `Le`/`Ge`/`Eq` row. An inverted
50    /// `[hi, lo]` (`lo > hi`, an infeasible user range) is also a range, so the
51    /// solver reports the infeasibility rather than it collapsing to an equality.
52    #[must_use]
53    pub fn is_range(&self) -> bool {
54        self.lower.is_finite() && self.upper.is_finite() && !self.is_equality()
55    }
56
57    /// Recover the single-sided `(sense, rhs)` view, or `None` for a genuine
58    /// range (or an unconstrained `[-inf, +inf]` row). Backends and writers
59    /// without native two-sided rows branch on this.
60    #[must_use]
61    pub fn as_single(&self) -> Option<(Sense, f64)> {
62        match (self.lower.is_finite(), self.upper.is_finite()) {
63            (false, true) => Some((Sense::Le, self.upper)),
64            (true, false) => Some((Sense::Ge, self.lower)),
65            (true, true) if self.is_equality() => Some((Sense::Eq, self.lower)),
66            _ => None,
67        }
68    }
69}
70
71/// In-progress constraint produced by [`Relate::le`] / [`Relate::ge`] /
72/// [`Relate::eq`]. Registered through the `constraint!` macro.
73#[derive(Copy, Clone, Debug)]
74pub struct ConstraintExpr<'a> {
75    pub lhs: Expr<'a>,
76    pub sense: Sense,
77    pub rhs: f64,
78}
79
80/// Build a constraint from an expression. Lives on `Expr` itself.
81pub trait Relate<'a> {
82    fn le<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a>;
83    fn ge<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a>;
84    fn eq<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a>;
85}
86
87/// What can appear on the right-hand side of a constraint. Numeric scalars
88/// stay as the canonical `rhs`. Expressions get subtracted into the LHS.
89pub trait IntoRhs<'a> {
90    fn fold_rhs(self, lhs: Expr<'a>) -> (Expr<'a>, f64);
91
92    /// The numeric value when this RHS is a pure constant bound (a literal),
93    /// else `None`. Used by the range-constraint registration to decide whether
94    /// `lo <= e <= hi` collapses to one interval row: expression/param bounds
95    /// return `None` so they stay two general constraints (keeping the symbolic
96    /// bound re-bindable). Defaults to `None`.
97    fn const_bound(&self) -> Option<f64> {
98        None
99    }
100}
101
102impl<'a> IntoRhs<'a> for f64 {
103    fn fold_rhs(self, lhs: Expr<'a>) -> (Expr<'a>, f64) {
104        (lhs, self)
105    }
106    fn const_bound(&self) -> Option<f64> {
107        Some(*self)
108    }
109}
110
111impl<'a> IntoRhs<'a> for i32 {
112    fn fold_rhs(self, lhs: Expr<'a>) -> (Expr<'a>, f64) {
113        (lhs, f64::from(self))
114    }
115    fn const_bound(&self) -> Option<f64> {
116        Some(f64::from(*self))
117    }
118}
119
120impl<'a> IntoRhs<'a> for Expr<'a> {
121    fn fold_rhs(self, lhs: Expr<'a>) -> (Expr<'a>, f64) {
122        (lhs - self, 0.0)
123    }
124}
125
126impl<'a> Relate<'a> for Expr<'a> {
127    fn le<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a> {
128        let (lhs, rhs) = rhs.fold_rhs(self);
129        ConstraintExpr { lhs, sense: Sense::Le, rhs }
130    }
131
132    fn ge<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a> {
133        let (lhs, rhs) = rhs.fold_rhs(self);
134        ConstraintExpr { lhs, sense: Sense::Ge, rhs }
135    }
136
137    fn eq<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a> {
138        let (lhs, rhs) = rhs.fold_rhs(self);
139        ConstraintExpr { lhs, sense: Sense::Eq, rhs }
140    }
141}