oximo_core/reformulation/
indicator.rs1use std::cell::Ref;
4
5use oximo_expr::{Expr, ExprId, ExprNode, extract_linear};
6use smol_str::SmolStr;
7
8use crate::constraint::{ConstraintId, Relate};
9use crate::domain::Domain;
10use crate::indicator::{IndicatorConstraint, IndicatorConstraintHandle, IndicatorConstraintId};
11use crate::model::Model;
12use crate::reformulation::sos::{ReformulatedModel, ReformulationError};
13use crate::var::Variable;
14
15#[derive(Copy, Clone, Debug, Default, PartialEq)]
17pub struct IndicatorReformulationOptions {
18 fallback_big_m: Option<f64>,
19}
20
21impl IndicatorReformulationOptions {
22 #[must_use]
24 pub const fn with_fallback_big_m(mut self, big_m: f64) -> Self {
25 self.fallback_big_m = Some(big_m);
26 self
27 }
28
29 #[must_use]
30 pub const fn fallback_big_m(self) -> Option<f64> {
31 self.fallback_big_m
32 }
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct IndicatorReformulationArtifacts {
38 pub source: IndicatorConstraintId,
39 pub constraints: Vec<ConstraintId>,
40}
41
42#[derive(Clone, Debug)]
43struct PlannedIndicator {
44 source: IndicatorConstraintId,
45 lower_m: Option<f64>,
46 upper_m: Option<f64>,
47}
48
49#[derive(Debug)]
50struct IndicatorPlan(Vec<PlannedIndicator>);
51
52impl IndicatorPlan {
53 fn one(
54 model: &Model,
55 id: IndicatorConstraintId,
56 options: IndicatorReformulationOptions,
57 ) -> Result<Self, ReformulationError> {
58 validate_options(options)?;
59 let constraints = model.indicator_constraints.borrow();
60 let source = constraints
61 .get(id.index())
62 .ok_or(ReformulationError::UnknownIndicatorConstraint(id.index()))?;
63 let entries =
64 if source.active { vec![plan_one(model, id, source, options)?] } else { Vec::new() };
65 Ok(Self(entries))
66 }
67
68 fn all(
69 model: &Model,
70 options: IndicatorReformulationOptions,
71 ) -> Result<Self, ReformulationError> {
72 validate_options(options)?;
73 let constraints = model.indicator_constraints.borrow();
74 let mut entries = Vec::new();
75 for (index, source) in constraints.iter().enumerate() {
76 if source.active {
77 entries.push(plan_one(
78 model,
79 IndicatorConstraintId(u32::try_from(index).expect("indicator ID overflow")),
80 source,
81 options,
82 )?);
83 }
84 }
85 Ok(Self(entries))
86 }
87
88 fn apply(self, model: &Model) -> Vec<IndicatorReformulationArtifacts> {
89 let mut artifacts = Vec::with_capacity(self.0.len());
90 for planned in self.0 {
91 artifacts.push(planned.apply(model));
92 }
93 if !artifacts.is_empty() {
94 model.invalidate_kind();
95 }
96 model.indicator_reformulations.borrow_mut().extend(artifacts.iter().cloned());
97 artifacts
98 }
99}
100
101fn validate_options(options: IndicatorReformulationOptions) -> Result<(), ReformulationError> {
102 if let Some(big_m) = options.fallback_big_m
103 && (!big_m.is_finite() || big_m <= 0.0)
104 {
105 return Err(ReformulationError::InvalidFallbackBigM(big_m));
106 }
107 Ok(())
108}
109
110fn plan_one(
111 model: &Model,
112 id: IndicatorConstraintId,
113 source: &IndicatorConstraint,
114 options: IndicatorReformulationOptions,
115) -> Result<PlannedIndicator, ReformulationError> {
116 if source.lower == f64::INFINITY || source.upper == f64::NEG_INFINITY {
117 return Err(ReformulationError::InvalidIndicatorBounds { constraint: source.name.clone() });
118 }
119 let arena = model.arena.borrow();
120 if contains_parameter(&arena, source.lhs) {
121 return Err(ReformulationError::ParameterDependentIndicator {
122 constraint: source.name.clone(),
123 });
124 }
125 let terms = extract_linear(&arena, source.lhs).ok_or_else(|| {
126 ReformulationError::InvalidIndicatorExpression { constraint: source.name.clone() }
127 })?;
128 if !terms.constant.is_finite() || terms.coeffs.iter().any(|(_, c)| !c.is_finite()) {
129 return Err(ReformulationError::InvalidIndicatorExpression {
130 constraint: source.name.clone(),
131 });
132 }
133 let variables = model.variables.borrow();
134 let inactive = if source.active_value { 0.0 } else { 1.0 };
135 let mut minimum = terms.constant;
136 let mut maximum = terms.constant;
137 for &(variable, coefficient) in terms.coeffs.iter() {
138 if coefficient == 0.0 {
139 continue;
140 }
141 let (lower, upper) = effective_bounds(&variables[variable.index()]);
142 let (lower, upper) =
143 if variable == source.trigger { (inactive, inactive) } else { (lower, upper) };
144 let (min_bound, max_bound) =
145 if coefficient >= 0.0 { (lower, upper) } else { (upper, lower) };
146 minimum += coefficient * min_bound;
147 maximum += coefficient * max_bound;
148 }
149 if minimum.is_nan() || maximum.is_nan() {
150 return Err(ReformulationError::InvalidIndicatorExpression {
151 constraint: source.name.clone(),
152 });
153 }
154 let lower_m = if source.lower.is_finite() {
155 Some(m_for_side(source.lower - minimum, source, "lower", options)?)
156 } else {
157 None
158 };
159 let upper_m = if source.upper.is_finite() {
160 Some(m_for_side(maximum - source.upper, source, "upper", options)?)
161 } else {
162 None
163 };
164 Ok(PlannedIndicator { source: id, lower_m, upper_m })
165}
166
167fn m_for_side(
168 required: f64,
169 source: &IndicatorConstraint,
170 side: &'static str,
171 options: IndicatorReformulationOptions,
172) -> Result<f64, ReformulationError> {
173 if required.is_finite() {
174 Ok(required.max(0.0))
175 } else if let Some(big_m) = options.fallback_big_m {
176 Ok(big_m)
177 } else {
178 Err(ReformulationError::MissingIndicatorBigM { constraint: source.name.clone(), side })
179 }
180}
181
182fn effective_bounds(variable: &Variable) -> (f64, f64) {
183 match variable.domain {
184 Domain::SemiContinuous { threshold } | Domain::SemiInteger { threshold } => {
185 (threshold.min(0.0), variable.ub.max(0.0))
186 }
187 Domain::Real | Domain::Integer | Domain::Binary => (variable.lb, variable.ub),
188 }
189}
190
191fn contains_parameter(arena: &oximo_expr::ExprArena, root: ExprId) -> bool {
192 let mut stack = vec![root];
193 while let Some(id) = stack.pop() {
194 match arena.get(id) {
195 ExprNode::Param(_) => return true,
196 ExprNode::Add(children)
197 | ExprNode::Mul(children)
198 | ExprNode::Min(children)
199 | ExprNode::Max(children) => stack.extend(children.iter().copied()),
200 ExprNode::Unary(_, child) => stack.push(*child),
201 ExprNode::Pow(left, right)
202 | ExprNode::Div(left, right)
203 | ExprNode::Atan2(left, right) => {
204 stack.push(*left);
205 stack.push(*right);
206 }
207 ExprNode::Const(_) | ExprNode::Var(_) | ExprNode::Linear { .. } => {}
208 }
209 }
210 false
211}
212
213impl PlannedIndicator {
214 fn apply(self, model: &Model) -> IndicatorReformulationArtifacts {
215 let source = model.indicator_constraints.borrow()[self.source.index()].clone();
216 let lhs = Expr::new(source.lhs, &model.arena);
217 let trigger = Expr::from_var(&model.arena, source.trigger);
218 let inactive = if source.active_value { 1.0 - trigger } else { trigger };
219 let mut generated = Vec::with_capacity(2);
220 if let Some(big_m) = self.lower_m {
222 let name = unique_constraint_name(
223 model,
224 &format!("__oximo_indicator{}_lower", self.source.index()),
225 );
226 generated
227 .push(model.__add_constraint(name, lhs.ge(source.lower - big_m * inactive)).id());
228 }
229 if let Some(big_m) = self.upper_m {
230 let name = unique_constraint_name(
231 model,
232 &format!("__oximo_indicator{}_upper", self.source.index()),
233 );
234 generated
235 .push(model.__add_constraint(name, lhs.le(source.upper + big_m * inactive)).id());
236 }
237 model.indicator_constraints.borrow_mut()[self.source.index()].active = false;
238 IndicatorReformulationArtifacts { source: self.source, constraints: generated }
239 }
240}
241
242fn unique_constraint_name(model: &Model, base: &str) -> SmolStr {
243 if model.constraint_id(base).is_none() {
244 return base.into();
245 }
246 for suffix in 1_u64.. {
247 let candidate = format!("{base}_{suffix}");
248 if model.constraint_id(&candidate).is_none() {
249 return candidate.into();
250 }
251 }
252 unreachable!("u64 name suffix space exhausted")
253}
254
255impl IndicatorConstraintHandle<'_> {
256 pub fn to_reformulated_model(
261 self,
262 options: IndicatorReformulationOptions,
263 ) -> Result<ReformulatedModel, ReformulationError> {
264 self.model.to_reformulated_indicator_constraint_model(self.id, options)
265 }
266
267 pub fn reformulate(
272 self,
273 options: IndicatorReformulationOptions,
274 ) -> Result<Option<IndicatorReformulationArtifacts>, ReformulationError> {
275 self.model.reformulate_indicator_constraint(self.id, options)
276 }
277}
278
279impl Model {
280 pub fn to_reformulated_indicator_constraint_model(
285 &self,
286 id: IndicatorConstraintId,
287 options: IndicatorReformulationOptions,
288 ) -> Result<ReformulatedModel, ReformulationError> {
289 let plan = IndicatorPlan::one(self, id, options)?;
290 let model = self.clone_preserving_ids_with_capacity(0, plan.0.len() * 2, plan.0.len() * 8);
291 plan.apply(&model);
292 Ok(ReformulatedModel { model })
293 }
294
295 pub fn reformulate_indicator_constraint(
301 &self,
302 id: IndicatorConstraintId,
303 options: IndicatorReformulationOptions,
304 ) -> Result<Option<IndicatorReformulationArtifacts>, ReformulationError> {
305 let plan = IndicatorPlan::one(self, id, options)?;
306 Ok(plan.apply(self).pop())
307 }
308
309 pub fn to_reformulated_indicator_model(
314 &self,
315 options: IndicatorReformulationOptions,
316 ) -> Result<ReformulatedModel, ReformulationError> {
317 let plan = IndicatorPlan::all(self, options)?;
318 let model = self.clone_preserving_ids_with_capacity(0, plan.0.len() * 2, plan.0.len() * 8);
319 plan.apply(&model);
320 Ok(ReformulatedModel { model })
321 }
322
323 pub fn reformulate_indicators(
328 &self,
329 options: IndicatorReformulationOptions,
330 ) -> Result<Vec<IndicatorReformulationArtifacts>, ReformulationError> {
331 let plan = IndicatorPlan::all(self, options)?;
332 Ok(plan.apply(self))
333 }
334
335 #[must_use]
337 pub fn indicator_reformulations(&self) -> Ref<'_, [IndicatorReformulationArtifacts]> {
338 Ref::map(self.indicator_reformulations.borrow(), Vec::as_slice)
339 }
340}