Skip to main content

oximo_core/
sos.rs

1use oximo_expr::VarId;
2use rustc_hash::{FxBuildHasher, FxHashSet};
3use smol_str::SmolStr;
4
5use crate::model::Model;
6use crate::reformulation::{
7    ReformulatedModel, ReformulationError, SosReformulationArtifacts, SosReformulationOptions,
8};
9
10/// The two standard special ordered set types.
11#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
12pub enum SosType {
13    /// At most one member may be nonzero.
14    Sos1,
15    /// At most two adjacent (by weight) members may be nonzero.
16    Sos2,
17}
18
19impl SosType {
20    #[must_use]
21    pub const fn label(self) -> &'static str {
22        match self {
23            Self::Sos1 => "SOS1",
24            Self::Sos2 => "SOS2",
25        }
26    }
27}
28
29impl std::fmt::Display for SosType {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.write_str(self.label())
32    }
33}
34
35#[derive(Copy, Clone, Debug, PartialEq)]
36pub struct SosMember {
37    pub variable: VarId,
38    pub weight: f64,
39}
40
41#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
42pub struct SosConstraintId(pub u32);
43
44impl SosConstraintId {
45    #[inline]
46    pub fn index(self) -> usize {
47        self.0 as usize
48    }
49}
50
51/// A model-bound handle to an SOS constraint.
52///
53/// Single-constraint forms of [`crate::sos_constraint!`] and the programmatic
54/// SOS registration methods return this handle.
55#[derive(Copy, Clone)]
56pub struct SosConstraintHandle<'a> {
57    pub(crate) model: &'a Model,
58    pub(crate) id: SosConstraintId,
59}
60
61impl std::fmt::Debug for SosConstraintHandle<'_> {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("SosConstraintHandle").field("id", &self.id).finish()
64    }
65}
66
67impl SosConstraintHandle<'_> {
68    #[must_use]
69    pub const fn id(self) -> SosConstraintId {
70        self.id
71    }
72
73    #[must_use]
74    pub fn index(self) -> usize {
75        self.id.index()
76    }
77
78    /// Produce an independent model in which this SOS constraint is replaced
79    /// by mixed-integer algebraic constraints.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`ReformulationError`] when a fallback Big-M is invalid or a
84    /// member lacks a finite bound and no fallback was supplied.
85    pub fn to_reformulated_model(
86        self,
87        options: SosReformulationOptions,
88    ) -> Result<ReformulatedModel, ReformulationError> {
89        self.model.to_reformulated_sos_constraint_model(self.id, options)
90    }
91
92    /// Replace this SOS constraint on its source model without cloning the
93    /// model. Returns `Ok(None)` if it was already reformulated.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`ReformulationError`] before modifying the model when the
98    /// fallback Big-M or a required member bound is invalid.
99    pub fn reformulate(
100        self,
101        options: SosReformulationOptions,
102    ) -> Result<Option<SosReformulationArtifacts>, ReformulationError> {
103        self.model.reformulate_sos_constraint(self.id, options)
104    }
105}
106
107impl From<SosConstraintHandle<'_>> for SosConstraintId {
108    fn from(value: SosConstraintHandle<'_>) -> Self {
109        value.id
110    }
111}
112
113/// An explicit SOS1 or SOS2 constraint.
114#[derive(Clone, Debug)]
115pub struct SosConstraint {
116    pub name: SmolStr,
117    pub sos_type: SosType,
118    pub members: Vec<SosMember>,
119    pub active: bool,
120}
121
122/// Validate an SOS member list before it is stored in a model.
123pub(crate) fn validate_members(name: &str, members: &[SosMember]) {
124    assert!(!members.is_empty(), "SOS constraint {name:?} has no members");
125    let mut vars = FxHashSet::with_capacity_and_hasher(members.len(), FxBuildHasher);
126    let mut weights = FxHashSet::with_capacity_and_hasher(members.len(), FxBuildHasher);
127    for member in members {
128        assert!(member.weight.is_finite(), "SOS constraint {name:?} has a non-finite weight");
129        assert!(
130            vars.insert(member.variable),
131            "SOS constraint {name:?} contains a duplicate variable"
132        );
133        let key = match member.weight.to_bits() {
134            0 | 0x8000_0000_0000_0000 => 0,
135            bits => bits,
136        };
137        assert!(weights.insert(key), "SOS constraint {name:?} contains duplicate weights");
138    }
139}