Skip to main content

oximo_core/
constraint.rs

1use std::fmt;
2
3use oximo_expr::{Expr, ExprId, ModelId};
4use smol_str::SmolStr;
5
6/// The sense of a constraint: less-than-or-equal, greater-than-or-equal, or equality.
7#[derive(Copy, Clone, Debug, PartialEq, Eq)]
8pub enum Sense {
9    Le,
10    Ge,
11    Eq,
12}
13
14impl fmt::Display for Sense {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        f.write_str(match self {
17            Self::Le => "<=",
18            Self::Ge => ">=",
19            Self::Eq => "=",
20        })
21    }
22}
23
24#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
25pub struct ConstraintId(pub u32);
26
27/// A model-bound handle to an algebraic constraint.
28///
29/// Constraint declaration macros return this type.
30/// Use [`Self::id`] when a backend-facing raw numeric ID
31/// is explicitly required.
32#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
33pub struct ConstraintHandle {
34    id: ConstraintId,
35    model_id: ModelId,
36}
37
38impl ConstraintHandle {
39    pub(crate) const fn new(id: ConstraintId, model_id: ModelId) -> Self {
40        Self { id, model_id }
41    }
42
43    #[must_use]
44    pub const fn id(self) -> ConstraintId {
45        self.id
46    }
47
48    #[must_use]
49    pub const fn model_id(self) -> ModelId {
50        self.model_id
51    }
52
53    #[must_use]
54    pub fn index(self) -> usize {
55        self.id.index()
56    }
57}
58
59impl From<ConstraintHandle> for ConstraintId {
60    fn from(value: ConstraintHandle) -> Self {
61        value.id
62    }
63}
64
65impl PartialEq<ConstraintId> for ConstraintHandle {
66    fn eq(&self, other: &ConstraintId) -> bool {
67        self.id == *other
68    }
69}
70
71impl PartialEq<ConstraintHandle> for ConstraintId {
72    fn eq(&self, other: &ConstraintHandle) -> bool {
73        *self == other.id
74    }
75}
76
77/// Model row IDs produced by a two-sided range declaration.
78///
79/// Constant bounds with a linear body produce one interval row. Other ranges
80/// produce separate lower and upper rows. You can query each row's dual individually.
81#[derive(Copy, Clone, Debug, PartialEq, Eq)]
82pub enum RangeConstraintIds {
83    Interval(ConstraintId),
84    Split { lower: ConstraintId, upper: ConstraintId },
85}
86
87/// Model-bound handles produced by a two-sided range declaration.
88#[derive(Copy, Clone, Debug, PartialEq, Eq)]
89pub enum RangeConstraintHandles {
90    Interval(ConstraintHandle),
91    Split { lower: ConstraintHandle, upper: ConstraintHandle },
92}
93
94impl RangeConstraintHandles {
95    #[must_use]
96    pub const fn ids(self) -> RangeConstraintIds {
97        match self {
98            Self::Interval(handle) => RangeConstraintIds::Interval(handle.id()),
99            Self::Split { lower, upper } => {
100                RangeConstraintIds::Split { lower: lower.id(), upper: upper.id() }
101            }
102        }
103    }
104}
105
106impl From<RangeConstraintHandles> for RangeConstraintIds {
107    fn from(value: RangeConstraintHandles) -> Self {
108        value.ids()
109    }
110}
111
112impl PartialEq<RangeConstraintIds> for RangeConstraintHandles {
113    fn eq(&self, other: &RangeConstraintIds) -> bool {
114        self.ids() == *other
115    }
116}
117
118impl PartialEq<RangeConstraintHandles> for RangeConstraintIds {
119    fn eq(&self, other: &RangeConstraintHandles) -> bool {
120        *self == other.ids()
121    }
122}
123
124impl ConstraintId {
125    #[inline]
126    pub fn index(self) -> usize {
127        self.0 as usize
128    }
129}
130
131/// A single algebraic constraint, canonicalized as the interval
132/// `lower <= lhs <= upper` with numeric bounds. RHS expressions are folded into
133/// `lhs` during construction, so backends only ever see this canonical shape.
134///
135/// The single-sided senses map onto the interval as `Le(rhs) => [-inf, rhs]`,
136/// `Ge(rhs) => [rhs, +inf]`, `Eq(rhs) => [rhs, rhs]`. A two-sided range with
137/// constant bounds is `[lo, hi]`. Use [`Constraint::as_single`] to recover the
138/// single-sided sense (for backends without native two-sided rows) and
139/// [`Constraint::is_range`] to detect a genuine range.
140#[derive(Clone, Debug)]
141pub struct Constraint {
142    pub name: SmolStr,
143    pub lhs: ExprId,
144    pub lower: f64,
145    pub upper: f64,
146    pub active: bool,
147}
148
149impl Constraint {
150    /// The two bounds are equal, i.e. this is an equality row. Uses `total_cmp`
151    /// for an exact comparison: the bounds are literals (`Eq` copies the same
152    /// value into both).
153    fn is_equality(&self) -> bool {
154        self.lower.total_cmp(&self.upper).is_eq()
155    }
156
157    /// Whether this is a genuine two-sided range (both bounds finite and not an
158    /// equality), as opposed to a single-sided `Le`/`Ge`/`Eq` row. An inverted
159    /// `[hi, lo]` (`lo > hi`, an infeasible user range) is also a range, so the
160    /// solver reports the infeasibility rather than it collapsing to an equality.
161    #[must_use]
162    pub fn is_range(&self) -> bool {
163        self.lower.is_finite() && self.upper.is_finite() && !self.is_equality()
164    }
165
166    /// Recover the single-sided `(sense, rhs)` view, or `None` for a genuine
167    /// range (or an unconstrained `[-inf, +inf]` row). Backends and writers
168    /// without native two-sided rows branch on this.
169    #[must_use]
170    pub fn as_single(&self) -> Option<(Sense, f64)> {
171        match (self.lower.is_finite(), self.upper.is_finite()) {
172            (false, true) => Some((Sense::Le, self.upper)),
173            (true, false) => Some((Sense::Ge, self.lower)),
174            (true, true) if self.is_equality() => Some((Sense::Eq, self.lower)),
175            _ => None,
176        }
177    }
178}
179
180/// In-progress constraint produced by [`Relate::le`] / [`Relate::ge`] /
181/// [`Relate::eq`]. Registered through the `constraint!` macro.
182#[derive(Copy, Clone, Debug)]
183pub struct ConstraintExpr<'a> {
184    pub lhs: Expr<'a>,
185    pub sense: Sense,
186    pub rhs: f64,
187}
188
189/// Build a constraint from an expression. Lives on `Expr` itself.
190pub trait Relate<'a> {
191    fn le<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a>;
192    fn ge<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a>;
193    fn eq<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a>;
194}
195
196/// What can appear on the right-hand side of a constraint. Numeric scalars
197/// stay as the canonical `rhs`. Expressions get subtracted into the LHS.
198pub trait IntoRhs<'a> {
199    fn fold_rhs(self, lhs: Expr<'a>) -> (Expr<'a>, f64);
200
201    /// The numeric value when this RHS is a pure constant bound (a literal),
202    /// else `None`. Used by the range-constraint registration to decide whether
203    /// `lo <= e <= hi` collapses to one interval row: expression/param bounds
204    /// return `None` so they stay two general constraints (keeping the symbolic
205    /// bound re-bindable). Defaults to `None`.
206    fn const_bound(&self) -> Option<f64> {
207        None
208    }
209}
210
211impl<'a> IntoRhs<'a> for f64 {
212    fn fold_rhs(self, lhs: Expr<'a>) -> (Expr<'a>, f64) {
213        (lhs, self)
214    }
215    fn const_bound(&self) -> Option<f64> {
216        Some(*self)
217    }
218}
219
220impl<'a> IntoRhs<'a> for i32 {
221    fn fold_rhs(self, lhs: Expr<'a>) -> (Expr<'a>, f64) {
222        (lhs, f64::from(self))
223    }
224    fn const_bound(&self) -> Option<f64> {
225        Some(f64::from(*self))
226    }
227}
228
229impl<'a> IntoRhs<'a> for Expr<'a> {
230    fn fold_rhs(self, lhs: Expr<'a>) -> (Expr<'a>, f64) {
231        (lhs - self, 0.0)
232    }
233}
234
235impl<'a> Relate<'a> for Expr<'a> {
236    fn le<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a> {
237        let (lhs, rhs) = rhs.fold_rhs(self);
238        ConstraintExpr { lhs, sense: Sense::Le, rhs }
239    }
240
241    fn ge<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a> {
242        let (lhs, rhs) = rhs.fold_rhs(self);
243        ConstraintExpr { lhs, sense: Sense::Ge, rhs }
244    }
245
246    fn eq<R: IntoRhs<'a>>(self, rhs: R) -> ConstraintExpr<'a> {
247        let (lhs, rhs) = rhs.fold_rhs(self);
248        ConstraintExpr { lhs, sense: Sense::Eq, rhs }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::Sense;
255
256    #[test]
257    fn display_uses_ascii_relations() {
258        let labels = [Sense::Le.to_string(), Sense::Ge.to_string(), Sense::Eq.to_string()];
259        assert_eq!(labels, ["<=", ">=", "="]);
260        assert!(labels.iter().all(|label| label.is_ascii()));
261    }
262}