Skip to main content

ommx/
bound.rs

1use crate::{
2    macros::{impl_add_inverse, impl_mul_inverse},
3    parse::{Parse, ParseError, RawParseError},
4    v1, ATol, VariableID,
5};
6use approx::AbsDiffEq;
7use num::Zero;
8use proptest::prelude::*;
9use std::{collections::BTreeMap, ops::*};
10use thiserror::Error;
11
12#[non_exhaustive]
13#[derive(Debug, Error)]
14pub enum BoundError {
15    #[error("lower({lower}) or upper({upper}) never be NAN")]
16    NotANumber { lower: f64, upper: f64 },
17    #[error("lower({lower}) = +inf or upper({upper}) = -inf is not allowed")]
18    InvalidInfinity { lower: f64, upper: f64 },
19    #[error("Lower is larger than Upper: lower({lower}) > upper({upper})")]
20    UpperSmallerThanLower { lower: f64, upper: f64 },
21}
22
23impl BoundError {
24    fn check(lower: f64, upper: f64) -> Result<(), BoundError> {
25        if lower.is_nan() || upper.is_nan() {
26            return Err(BoundError::NotANumber { lower, upper });
27        }
28        if lower == f64::INFINITY || upper == f64::NEG_INFINITY {
29            return Err(BoundError::InvalidInfinity { lower, upper });
30        }
31        if lower > upper {
32            return Err(BoundError::UpperSmallerThanLower { lower, upper });
33        }
34        Ok(())
35    }
36}
37
38impl From<BoundError> for ParseError {
39    fn from(e: BoundError) -> Self {
40        RawParseError::from(e).into()
41    }
42}
43
44/// Bound for each decision variable
45///
46/// This uses `BTreeMap` to keep the order of decision variables by their IDs
47/// for intuitive debugging.
48pub type Bounds = BTreeMap<VariableID, Bound>;
49
50/// Bound of a decision variable
51///
52/// Invariant
53/// ---------
54/// - `lower <= upper`
55/// - `lower` and `upper` never become `NaN`
56/// - `lower` is not `+inf` and `upper` is not `-inf`
57///
58/// Examples
59/// --------
60///
61/// ```rust
62/// use ommx::Bound;
63///
64/// // Usual
65/// let bound = Bound::try_from([1.0, 2.0]).unwrap();
66/// // Single point `[1.0, 1.0]`
67/// let bound = Bound::try_from(1.0).unwrap();
68/// // Default is `(-inf, inf)`
69/// assert_eq!(Bound::default(), Bound::try_from([f64::NEG_INFINITY, f64::INFINITY]).unwrap());
70/// ```
71#[derive(Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
72pub struct Bound {
73    lower: f64,
74    upper: f64,
75}
76
77impl std::fmt::Debug for Bound {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        write!(f, "Bound{self}")
80    }
81}
82
83impl std::fmt::Display for Bound {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        let lower_bracket = if self.lower == f64::NEG_INFINITY {
86            "("
87        } else {
88            "["
89        };
90        let upper_bracket = if self.upper == f64::INFINITY {
91            ")"
92        } else {
93            "]"
94        };
95
96        let lower_str = if self.lower == f64::NEG_INFINITY {
97            "-inf".to_string()
98        } else {
99            self.lower.to_string()
100        };
101
102        let upper_str = if self.upper == f64::INFINITY {
103            "inf".to_string()
104        } else {
105            self.upper.to_string()
106        };
107
108        write!(f, "{lower_bracket}{lower_str}, {upper_str}{upper_bracket}")
109    }
110}
111
112impl Default for Bound {
113    fn default() -> Self {
114        Self {
115            lower: f64::NEG_INFINITY,
116            upper: f64::INFINITY,
117        }
118    }
119}
120
121impl Parse for v1::Bound {
122    type Output = Bound;
123    type Context = ();
124    fn parse(self, _: &Self::Context) -> Result<Self::Output, ParseError> {
125        let out = Bound::new(self.lower, self.upper)?;
126        Ok(out)
127    }
128}
129
130impl TryFrom<v1::Bound> for Bound {
131    type Error = BoundError;
132    fn try_from(value: v1::Bound) -> Result<Self, Self::Error> {
133        Self::new(value.lower, value.upper)
134    }
135}
136
137impl TryFrom<&v1::DecisionVariable> for Bound {
138    type Error = BoundError;
139    fn try_from(v: &v1::DecisionVariable) -> Result<Self, Self::Error> {
140        if let Some(bound) = &v.bound {
141            Self::try_from(bound.clone())
142        } else if v.kind() == v1::decision_variable::Kind::Binary {
143            Self::new(0.0, 1.0)
144        } else {
145            Ok(Self::default())
146        }
147    }
148}
149
150impl From<Bound> for v1::Bound {
151    fn from(bound: Bound) -> Self {
152        Self {
153            lower: bound.lower,
154            upper: bound.upper,
155        }
156    }
157}
158
159impl TryFrom<f64> for Bound {
160    type Error = BoundError;
161    fn try_from(value: f64) -> Result<Self, Self::Error> {
162        Self::new(value, value)
163    }
164}
165
166impl TryFrom<[f64; 2]> for Bound {
167    type Error = BoundError;
168    fn try_from([lower, upper]: [f64; 2]) -> Result<Self, Self::Error> {
169        Self::new(lower, upper)
170    }
171}
172
173impl Add for Bound {
174    type Output = Bound;
175    fn add(self, rhs: Self) -> Self::Output {
176        Self::new(self.lower + rhs.lower, self.upper + rhs.upper).unwrap()
177    }
178}
179impl Add<f64> for Bound {
180    type Output = Bound;
181    fn add(self, rhs: f64) -> Self::Output {
182        Bound::new(self.lower + rhs, self.upper + rhs).unwrap()
183    }
184}
185impl_add_inverse!(f64, Bound);
186
187impl AddAssign for Bound {
188    fn add_assign(&mut self, rhs: Self) {
189        *self = *self + rhs;
190    }
191}
192
193impl AddAssign<f64> for Bound {
194    fn add_assign(&mut self, rhs: f64) {
195        *self = *self + rhs;
196    }
197}
198
199impl Zero for Bound {
200    fn zero() -> Self {
201        Self::try_from(0.0).unwrap()
202    }
203    fn is_zero(&self) -> bool {
204        self.lower == 0.0 && self.upper == 0.0
205    }
206}
207
208impl Mul for Bound {
209    type Output = Bound;
210    fn mul(self, rhs: Self) -> Self::Output {
211        // [0, 0] x (-inf, inf) = [0, 0]
212        if self == Bound::zero() || rhs == Bound::zero() {
213            return Bound::zero();
214        }
215        let a = self.lower * rhs.lower;
216        let b = self.lower * rhs.upper;
217        let c = self.upper * rhs.lower;
218        let d = self.upper * rhs.upper;
219        Bound::new(a.min(b).min(c).min(d), a.max(b).max(c).max(d)).unwrap()
220    }
221}
222
223impl Mul<f64> for Bound {
224    type Output = Bound;
225    fn mul(self, rhs: f64) -> Self::Output {
226        if rhs >= 0.0 {
227            Bound::new(self.lower * rhs, self.upper * rhs).unwrap()
228        } else {
229            Bound::new(self.upper * rhs, self.lower * rhs).unwrap()
230        }
231    }
232}
233impl_mul_inverse!(f64, Bound);
234
235impl MulAssign for Bound {
236    fn mul_assign(&mut self, rhs: Self) {
237        *self = *self * rhs;
238    }
239}
240impl MulAssign<f64> for Bound {
241    fn mul_assign(&mut self, rhs: f64) {
242        *self = *self * rhs;
243    }
244}
245
246impl PartialEq<f64> for Bound {
247    fn eq(&self, other: &f64) -> bool {
248        self.lower == *other && self.upper == *other
249    }
250}
251
252impl PartialEq<Bound> for f64 {
253    fn eq(&self, other: &Bound) -> bool {
254        other == self
255    }
256}
257
258/// - `a <= [b, c]` means `a <= b`, i.e. `a <= x (forall x \in [b, c])`
259/// - `a >= [b, c]` means `a >= c`, i.e. `a >= x (forall x \in [b, c])`
260/// - If `a` is in `[b, c]`, return `None`
261impl PartialOrd<f64> for Bound {
262    fn partial_cmp(&self, other: &f64) -> Option<std::cmp::Ordering> {
263        debug_assert!(
264            self.lower <= self.upper,
265            "lower({}) <= upper({})",
266            self.lower,
267            self.upper
268        );
269        if other <= &self.lower {
270            Some(std::cmp::Ordering::Greater)
271        } else if other >= &self.upper {
272            Some(std::cmp::Ordering::Less)
273        } else {
274            None
275        }
276    }
277}
278
279impl PartialOrd<Bound> for f64 {
280    fn partial_cmp(&self, other: &Bound) -> Option<std::cmp::Ordering> {
281        other.partial_cmp(self).map(|o| o.reverse())
282    }
283}
284
285impl AbsDiffEq for Bound {
286    type Epsilon = ATol;
287
288    fn default_epsilon() -> Self::Epsilon {
289        ATol::default()
290    }
291
292    fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
293        // Since `abs_diff_eq` for f64::INFINITY returns false, check it first
294        (self.lower == other.lower || self.lower.abs_diff_eq(&other.lower, *epsilon))
295            && (self.upper == other.upper || self.upper.abs_diff_eq(&other.upper, *epsilon))
296    }
297}
298
299impl Bound {
300    /// Positive or zero, `[0, inf)`
301    pub fn positive() -> Self {
302        Self::new(0.0, f64::INFINITY).unwrap()
303    }
304
305    /// Negative or zero, `(-inf, 0]`
306    pub fn negative() -> Self {
307        Self::new(f64::NEG_INFINITY, 0.0).unwrap()
308    }
309
310    /// Unbounded, `(-inf, inf)`, which equals to `Bound::default()`
311    pub fn unbounded() -> Self {
312        Self::default()
313    }
314
315    pub fn of_binary() -> Self {
316        Self::new(0.0, 1.0).unwrap()
317    }
318
319    pub fn new(lower: f64, upper: f64) -> Result<Self, BoundError> {
320        BoundError::check(lower, upper)?;
321        Ok(Self { lower, upper })
322    }
323
324    pub fn lower(&self) -> f64 {
325        self.lower
326    }
327
328    pub fn upper(&self) -> f64 {
329        self.upper
330    }
331
332    pub fn width(&self) -> f64 {
333        self.upper - self.lower
334    }
335
336    pub fn set_lower(&mut self, lower: f64) -> Result<(), BoundError> {
337        BoundError::check(lower, self.upper)?;
338        self.lower = lower;
339        Ok(())
340    }
341
342    pub fn set_upper(&mut self, upper: f64) -> Result<(), BoundError> {
343        BoundError::check(self.lower, upper)?;
344        self.upper = upper;
345        Ok(())
346    }
347
348    /// Strengthen the bound for integer decision variables
349    ///
350    /// - Since the bound evaluation may be inaccurate due to floating-point arithmetic error,
351    ///   this method rounds to `[ceil(lower-atol), floor(upper+atol)]`
352    /// - If no integer value is in the bound, return `None`
353    ///
354    /// Examples
355    /// ---------
356    ///
357    /// ```rust
358    /// use ommx::{Bound, BoundError, ATol};
359    ///
360    /// // Rounding with absolute tolerance
361    /// let bound = Bound::new(1.000000000001, 1.99999999999).unwrap();
362    /// assert_eq!(bound.as_integer_bound(ATol::default()).unwrap(), Bound::new(1.0, 2.0).unwrap());
363    ///
364    /// // No integer value exists between 1.1 and 1.9
365    /// let bound = Bound::new(1.1, 1.9).unwrap();
366    /// assert!(bound.as_integer_bound(ATol::default()).is_none());
367    ///
368    /// // infinite bound are kept as is
369    /// let bound = Bound::new(f64::NEG_INFINITY, f64::INFINITY).unwrap();
370    /// assert_eq!(bound.as_integer_bound(ATol::default()), Some(bound));
371    ///
372    /// let bound = Bound::new(1.1, f64::INFINITY).unwrap();
373    /// assert_eq!(bound.as_integer_bound(ATol::default()).unwrap(), Bound::new(2.0, f64::INFINITY).unwrap());
374    /// ```
375    pub fn as_integer_bound(&self, atol: crate::ATol) -> Option<Self> {
376        let lower = if self.lower.is_finite() {
377            let out = (self.lower - atol).ceil();
378            if out == 0.0 {
379                0.0 // Avoid negative zero
380            } else {
381                out
382            }
383        } else {
384            self.lower
385        };
386        let upper = if self.upper.is_finite() {
387            (self.upper + atol).floor()
388        } else {
389            self.upper
390        };
391        if upper < lower {
392            None
393        } else {
394            Some(Self { lower, upper })
395        }
396    }
397
398    /// Check if the bound is a point, i.e. `lower == upper`
399    pub fn is_point(&self, atol: ATol) -> Option<f64> {
400        if self.lower.abs_diff_eq(&self.upper, atol.into_inner()) {
401            Some(self.lower)
402        } else {
403            None
404        }
405    }
406
407    /// `[lower, upper]` with finite `lower` and `upper`
408    pub fn is_finite(&self) -> bool {
409        self.lower.is_finite() && self.upper.is_finite()
410    }
411
412    /// Take the intersection of two bounds, `None` if the intersection is empty
413    pub fn intersection(&self, other: &Self) -> Option<Self> {
414        Self::new(self.lower.max(other.lower), self.upper.min(other.upper)).ok()
415    }
416
417    pub fn pow(&self, exp: u8) -> Self {
418        if exp.is_multiple_of(2) {
419            if self.lower >= 0.0 {
420                // 0 <= lower <= upper
421                Bound::new(self.lower.powi(exp as i32), self.upper.powi(exp as i32)).unwrap()
422            } else if self.upper <= 0.0 {
423                // lower <= upper <= 0
424                Bound::new(self.upper.powi(exp as i32), self.lower.powi(exp as i32)).unwrap()
425            } else {
426                // lower <= 0 <= upper
427                Bound::new(
428                    0.0,
429                    self.upper
430                        .abs()
431                        .powi(exp as i32)
432                        .max(self.lower.abs().powi(exp as i32)),
433                )
434                .unwrap()
435            }
436        } else {
437            // pow is monotonic for odd exponents
438            Bound::new(self.lower.powi(exp as i32), self.upper.powi(exp as i32)).unwrap()
439        }
440    }
441
442    /// Check the `value` is in the bound with absolute tolerance
443    pub fn contains(&self, value: f64, atol: crate::ATol) -> bool {
444        self.lower - atol <= value && value <= self.upper + atol
445    }
446
447    pub fn as_range(&self) -> RangeInclusive<f64> {
448        self.lower..=self.upper
449    }
450
451    pub fn nearest_to_zero(&self) -> f64 {
452        if self.lower >= 0.0 {
453            self.lower
454        } else if self.upper <= 0.0 {
455            self.upper
456        } else {
457            0.0
458        }
459    }
460
461    /// Arbitrary *finite* value within the bound
462    ///
463    /// `max_abs` is the maximum absolute value of the generated value
464    /// to keep floating point arithmetic stable.
465    pub fn arbitrary_containing(&self, max_abs: f64) -> BoxedStrategy<f64> {
466        assert!(max_abs > 0.0);
467        // RangeInclusive::arbitrary() does not support infinite range
468        match (self.lower, self.upper) {
469            (f64::NEG_INFINITY, f64::INFINITY) => (-max_abs..=max_abs).boxed(),
470            (f64::NEG_INFINITY, upper) => {
471                let upper = upper.min(max_abs);
472                prop_oneof![(-max_abs..=upper).boxed(), Just(upper)].boxed()
473            }
474            (lower, f64::INFINITY) => {
475                let lower = lower.max(-max_abs);
476                prop_oneof![(lower..=max_abs).boxed(), Just(lower)].boxed()
477            }
478            (lower, upper) => {
479                let lower = lower.max(-max_abs);
480                let upper = upper.min(max_abs);
481                if lower == upper {
482                    Just(lower).boxed()
483                } else {
484                    prop_oneof![Just(upper), Just(lower), (lower..=upper)].boxed()
485                }
486            }
487        }
488    }
489
490    pub fn arbitrary_containing_integer(&self, max_abs: u64) -> BoxedStrategy<i64> {
491        let lower = self.lower.max(-(max_abs as f64)).ceil() as i64;
492        let upper = self.upper.min(max_abs as f64).floor() as i64;
493        if lower == upper {
494            Just(lower).boxed()
495        } else {
496            (lower..=upper).boxed()
497        }
498    }
499}
500
501impl Arbitrary for Bound {
502    type Parameters = ();
503    type Strategy = BoxedStrategy<Self>;
504    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
505        (
506            prop_oneof![
507                Just(f64::NEG_INFINITY),
508                Just(0.0),
509                (-10..=10).prop_map(|x| x as f64),
510                -10.0..=10.0
511            ],
512            prop_oneof![
513                Just(f64::INFINITY),
514                Just(0.0),
515                (-10..=10).prop_map(|x| x as f64),
516                -10.0..=10.0
517            ],
518        )
519            .prop_map(|(lower, upper)| {
520                if lower <= upper {
521                    Bound::new(lower, upper).unwrap()
522                } else {
523                    Bound::new(upper, lower).unwrap()
524                }
525            })
526            .boxed()
527    }
528}
529
530mod logical_memory;
531
532#[cfg(test)]
533mod tests {
534    use approx::assert_abs_diff_eq;
535
536    use super::*;
537
538    #[test]
539    fn partial_ord() {
540        assert!(1.0 <= Bound::new(2.0, 3.0).unwrap());
541        assert!(2.0 <= Bound::new(2.0, 3.0).unwrap());
542        assert!(3.0 >= Bound::new(2.0, 3.0).unwrap());
543        assert!(4.0 >= Bound::new(2.0, 3.0).unwrap());
544        assert!(f64::NEG_INFINITY <= Bound::new(2.0, 3.0).unwrap());
545        assert!(f64::INFINITY >= Bound::new(2.0, 3.0).unwrap());
546    }
547
548    #[test]
549    fn eq() {
550        assert_eq!(
551            Bound::new(f64::NEG_INFINITY, f64::INFINITY).unwrap(),
552            Bound::default()
553        );
554        assert_eq!(Bound::new(0.0, f64::INFINITY).unwrap(), Bound::positive());
555        assert_eq!(
556            Bound::new(f64::NEG_INFINITY, 0.0).unwrap(),
557            Bound::negative()
558        );
559
560        assert_abs_diff_eq!(
561            Bound::new(1.0, 2.0).unwrap(),
562            Bound::new(1.0, 2.00000001).unwrap(),
563        );
564        assert_abs_diff_eq!(
565            Bound::new(f64::NEG_INFINITY, f64::INFINITY).unwrap(),
566            Bound::default()
567        );
568        assert_abs_diff_eq!(Bound::new(0.0, f64::INFINITY).unwrap(), Bound::positive());
569        assert_abs_diff_eq!(
570            Bound::new(f64::NEG_INFINITY, 0.0).unwrap(),
571            Bound::negative()
572        );
573    }
574
575    #[test]
576    fn intersection() {
577        assert_eq!(
578            Bound::new(1.0, 2.0)
579                .unwrap()
580                .intersection(&Bound::new(1.5, 3.0).unwrap()),
581            Some(Bound::new(1.5, 2.0).unwrap())
582        );
583        assert_eq!(
584            Bound::new(1.0, 2.0)
585                .unwrap()
586                .intersection(&Bound::new(2.5, 3.0).unwrap()),
587            None
588        );
589        assert_eq!(
590            Bound::positive().intersection(&Bound::negative()).unwrap(),
591            0.0
592        );
593    }
594
595    #[test]
596    fn minus_zero() {
597        let bound = Bound::new(-0.1, 1.1).unwrap();
598        insta::assert_snapshot!(bound.as_integer_bound(ATol::default()).unwrap(), @"[0, 1]");
599    }
600
601    #[test]
602    fn bound_pow() {
603        insta::assert_debug_snapshot!(Bound::new(2.0, 3.0).unwrap().pow(2), @"Bound[4, 9]");
604        insta::assert_debug_snapshot!(Bound::new(2.0, 3.0).unwrap().pow(3), @"Bound[8, 27]");
605        insta::assert_debug_snapshot!(Bound::new(-2.0, 3.0).unwrap().pow(2), @"Bound[0, 9]");
606        insta::assert_debug_snapshot!(Bound::new(-2.0, 3.0).unwrap().pow(3), @"Bound[-8, 27]");
607        insta::assert_debug_snapshot!(Bound::default().pow(2), @"Bound[0, inf)");
608        insta::assert_debug_snapshot!(Bound::default().pow(3), @"Bound(-inf, inf)");
609    }
610
611    fn bound_and_containing() -> BoxedStrategy<(Bound, f64)> {
612        Bound::arbitrary()
613            .prop_flat_map(|bound| (Just(bound), bound.arbitrary_containing(1e5)))
614            .boxed()
615    }
616
617    proptest! {
618        #[test]
619        fn contains((bound, value) in bound_and_containing()) {
620            prop_assert!(bound.contains(value, crate::ATol::default()));
621        }
622
623        #[test]
624        fn add((b1, v1) in bound_and_containing(), (b2, v2) in bound_and_containing()) {
625            prop_assert!((b1 + b2).contains(v1 + v2, crate::ATol::default()));
626        }
627
628        #[test]
629        fn mul((b1, v1) in bound_and_containing(), (b2, v2) in bound_and_containing()) {
630            prop_assert!((b1 * b2).contains(v1 * v2, crate::ATol::default()));
631        }
632
633        #[test]
634        fn pow((b, v) in bound_and_containing()) {
635            prop_assert!(b.pow(2).contains(v.powi(2), crate::ATol::default()));
636            prop_assert!(b.pow(3).contains(v.powi(3), crate::ATol::default()));
637        }
638    }
639}