Skip to main content

oximo_core/
domain.rs

1use std::fmt;
2
3/// The domain of a variable, which determines the type of values it can take.
4///
5/// Real: any real number.
6/// Integer: any integer.
7/// Binary: 0 or 1.
8/// SemiContinuous: either 0 or any value >= threshold.
9/// SemiInteger: either 0 or any integer >= threshold.
10#[derive(Copy, Clone, Debug, Default, PartialEq)]
11pub enum Domain {
12    #[default]
13    Real,
14    Integer,
15    Binary,
16    SemiContinuous {
17        threshold: f64,
18    },
19    SemiInteger {
20        threshold: f64,
21    },
22}
23
24impl fmt::Display for Domain {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::Real => f.write_str("real"),
28            Self::Integer => f.write_str("integer"),
29            Self::Binary => f.write_str("binary"),
30            Self::SemiContinuous { threshold } => write!(f, "semi-continuous({threshold})"),
31            Self::SemiInteger { threshold } => write!(f, "semi-integer({threshold})"),
32        }
33    }
34}
35
36impl Domain {
37    /// Whether this domain is integer-valued (Integer, Binary, SemiInteger)
38    #[must_use]
39    pub fn is_integer(self) -> bool {
40        matches!(self, Self::Integer | Self::Binary | Self::SemiInteger { .. })
41    }
42
43    /// The semicontinuity gap floor: `Some(threshold)` for `SemiContinuous` /
44    /// `SemiInteger`, `None` otherwise. Such a variable takes either 0 or a
45    /// value `>= threshold`, so backends emit `threshold` as the lower bound.
46    #[must_use]
47    pub fn semi_threshold(self) -> Option<f64> {
48        match self {
49            Self::SemiContinuous { threshold } | Self::SemiInteger { threshold } => Some(threshold),
50            _ => None,
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::Domain;
58
59    #[test]
60    fn semi_threshold_only_for_semi_domains() {
61        assert_eq!(Domain::Real.semi_threshold(), None);
62        assert_eq!(Domain::Integer.semi_threshold(), None);
63        assert_eq!(Domain::Binary.semi_threshold(), None);
64        assert_eq!(Domain::SemiContinuous { threshold: 2.0 }.semi_threshold(), Some(2.0));
65        assert_eq!(Domain::SemiInteger { threshold: 1.0 }.semi_threshold(), Some(1.0));
66    }
67
68    #[test]
69    fn display_uses_user_facing_ascii_labels() {
70        let labels = [
71            Domain::Real.to_string(),
72            Domain::Integer.to_string(),
73            Domain::Binary.to_string(),
74            Domain::SemiContinuous { threshold: 3.0 }.to_string(),
75            Domain::SemiInteger { threshold: 2.5 }.to_string(),
76        ];
77        assert_eq!(
78            labels,
79            ["real", "integer", "binary", "semi-continuous(3)", "semi-integer(2.5)"]
80        );
81        assert!(labels.iter().all(|label| label.is_ascii()));
82    }
83
84    #[test]
85    fn default_domain_is_real() {
86        assert_eq!(Domain::default(), Domain::Real);
87    }
88
89    #[test]
90    fn integer_domains_are_integer() {
91        assert!(Domain::Integer.is_integer());
92        assert!(Domain::Binary.is_integer());
93        assert!(Domain::SemiInteger { threshold: 2.0 }.is_integer());
94    }
95
96    #[test]
97    fn non_integer_domains_are_not_integer() {
98        assert!(!Domain::Real.is_integer());
99        assert!(!Domain::SemiContinuous { threshold: 2.0 }.is_integer());
100    }
101}