1use std::cell::Ref;
4use std::ops::Deref;
5
6use oximo_expr::{Expr, VarId};
7use smol_str::SmolStr;
8use thiserror::Error;
9
10use crate::constraint::{ConstraintId, Relate};
11use crate::domain::Domain;
12use crate::model::Model;
13use crate::sos::{SosConstraint, SosConstraintId, SosMember, SosType};
14use crate::var::Variable;
15
16#[derive(Copy, Clone, Debug, Default, PartialEq)]
18pub struct SosReformulationOptions {
19 fallback_big_m: Option<f64>,
20}
21
22impl SosReformulationOptions {
23 #[must_use]
26 pub const fn with_fallback_big_m(mut self, big_m: f64) -> Self {
27 self.fallback_big_m = Some(big_m);
28 self
29 }
30
31 #[must_use]
32 pub const fn fallback_big_m(self) -> Option<f64> {
33 self.fallback_big_m
34 }
35}
36
37#[derive(Clone, Debug, Error, PartialEq)]
39pub enum ReformulationError {
40 #[error("SOS constraint #{0} does not exist on this model")]
41 UnknownSosConstraint(usize),
42 #[error("fallback Big-M must be finite and positive, got {0}")]
43 InvalidFallbackBigM(f64),
44 #[error(
45 "cannot reformulate SOS constraint {constraint:?}: variable {variable:?} has no finite \
46 {side} bound; provide SosReformulationOptions::with_fallback_big_m(...)"
47 )]
48 MissingFiniteBound { constraint: SmolStr, variable: SmolStr, side: &'static str },
49 #[error("indicator constraint #{0} does not exist on this model")]
50 UnknownIndicatorConstraint(usize),
51 #[error(
52 "cannot reformulate indicator constraint {constraint:?}: its body depends on a parameter"
53 )]
54 ParameterDependentIndicator { constraint: SmolStr },
55 #[error(
56 "cannot reformulate indicator constraint {constraint:?}: body is not a finite affine expression"
57 )]
58 InvalidIndicatorExpression { constraint: SmolStr },
59 #[error(
60 "cannot reformulate indicator constraint {constraint:?}: lower bound is +infinity or upper bound is -infinity"
61 )]
62 InvalidIndicatorBounds { constraint: SmolStr },
63 #[error(
64 "cannot derive finite Big-M for indicator constraint {constraint:?} {side} side; provide IndicatorReformulationOptions::with_fallback_big_m(...)"
65 )]
66 MissingIndicatorBigM { constraint: SmolStr, side: &'static str },
67}
68
69#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct SosReformulationArtifacts {
72 pub source: SosConstraintId,
73 pub variables: Vec<VarId>,
74 pub constraints: Vec<ConstraintId>,
75}
76
77#[derive(Debug)]
79pub struct ReformulatedModel {
80 pub(crate) model: Model,
81}
82
83impl ReformulatedModel {
84 #[must_use]
85 pub fn model(&self) -> &Model {
86 &self.model
87 }
88
89 #[must_use]
92 pub fn sos_reformulations(&self) -> Ref<'_, [SosReformulationArtifacts]> {
93 Ref::map(self.model.sos_reformulations.borrow(), Vec::as_slice)
96 }
97
98 #[must_use]
99 pub fn into_model(self) -> Model {
100 self.model
101 }
102}
103
104impl Deref for ReformulatedModel {
105 type Target = Model;
106
107 fn deref(&self) -> &Self::Target {
108 &self.model
109 }
110}
111
112impl AsRef<Model> for ReformulatedModel {
113 fn as_ref(&self) -> &Model {
114 &self.model
115 }
116}
117
118#[derive(Copy, Clone, Debug)]
119struct PlannedMember {
120 member_index: usize,
121 member: SosMember,
122 lower: f64,
123 upper: f64,
124}
125
126impl PlannedMember {
127 fn gate_count(self) -> usize {
128 usize::from(self.lower < 0.0) + usize::from(self.upper > 0.0)
129 }
130}
131
132#[derive(Debug)]
133enum PlannedSosForm {
134 Trivial,
135 Sos1 { members: Vec<PlannedMember> },
136 Sos2 { members: Vec<PlannedMember> },
137}
138
139#[derive(Debug)]
140struct PlannedSos {
141 source: SosConstraintId,
142 form: PlannedSosForm,
143}
144
145#[derive(Debug)]
146struct SosReformulationPlan {
147 entries: Vec<PlannedSos>,
148 additional_variables: usize,
149 additional_constraints: usize,
150 additional_expr_nodes: usize,
151}
152
153impl Model {
154 pub fn to_reformulated_sos_constraint_model(
163 &self,
164 id: SosConstraintId,
165 options: SosReformulationOptions,
166 ) -> Result<ReformulatedModel, ReformulationError> {
167 let plan = SosReformulationPlan::for_one(self, id, options)?;
168 let model = self.clone_preserving_ids_with_capacity(
169 plan.additional_variables,
170 plan.additional_constraints,
171 plan.additional_expr_nodes,
172 );
173 plan.apply(&model);
174 Ok(ReformulatedModel { model })
175 }
176
177 pub fn reformulate_sos_constraint(
190 &self,
191 id: SosConstraintId,
192 options: SosReformulationOptions,
193 ) -> Result<Option<SosReformulationArtifacts>, ReformulationError> {
194 let plan = SosReformulationPlan::for_one(self, id, options)?;
195 Ok(plan.apply(self).pop())
196 }
197
198 pub fn to_reformulated_sos_model(
208 &self,
209 options: SosReformulationOptions,
210 ) -> Result<ReformulatedModel, ReformulationError> {
211 let plan = SosReformulationPlan::for_all(self, options)?;
212 let model = self.clone_preserving_ids_with_capacity(
213 plan.additional_variables,
214 plan.additional_constraints,
215 plan.additional_expr_nodes,
216 );
217 plan.apply(&model);
218 Ok(ReformulatedModel { model })
219 }
220
221 pub fn reformulate_sos(
234 &self,
235 options: SosReformulationOptions,
236 ) -> Result<Vec<SosReformulationArtifacts>, ReformulationError> {
237 let plan = SosReformulationPlan::for_all(self, options)?;
238 Ok(plan.apply(self))
239 }
240
241 #[must_use]
247 pub fn sos_reformulations(&self) -> Ref<'_, [SosReformulationArtifacts]> {
248 Ref::map(self.sos_reformulations.borrow(), Vec::as_slice)
249 }
250}
251
252fn validate_options(options: SosReformulationOptions) -> Result<(), ReformulationError> {
253 if let Some(big_m) = options.fallback_big_m
254 && (!big_m.is_finite() || big_m <= 0.0)
255 {
256 return Err(ReformulationError::InvalidFallbackBigM(big_m));
257 }
258 Ok(())
259}
260
261impl SosReformulationPlan {
262 fn for_one(
263 model: &Model,
264 id: SosConstraintId,
265 options: SosReformulationOptions,
266 ) -> Result<Self, ReformulationError> {
267 validate_options(options)?;
268 let variables = model.variables.borrow();
269 let constraints = model.sos_constraints.borrow();
270 let source = constraints
271 .get(id.index())
272 .ok_or(ReformulationError::UnknownSosConstraint(id.index()))?;
273 let entries = if source.active {
274 vec![plan_one(id, source, &variables, options)?]
275 } else {
276 Vec::new()
277 };
278 Ok(Self::new(entries))
279 }
280
281 fn for_all(
282 model: &Model,
283 options: SosReformulationOptions,
284 ) -> Result<Self, ReformulationError> {
285 validate_options(options)?;
286 let variables = model.variables.borrow();
287 let constraints = model.sos_constraints.borrow();
288 let mut entries =
289 Vec::with_capacity(constraints.iter().filter(|constraint| constraint.active).count());
290 for (index, source) in constraints.iter().enumerate() {
291 if !source.active {
292 continue;
293 }
294 let id = SosConstraintId(
295 u32::try_from(index).expect("SOS registration guarantees IDs fit in u32"),
296 );
297 entries.push(plan_one(id, source, &variables, options)?);
298 }
299 Ok(Self::new(entries))
300 }
301
302 fn new(entries: Vec<PlannedSos>) -> Self {
303 let mut additional_variables = 0;
304 let mut additional_constraints = 0;
305 let mut additional_expr_nodes = 0;
306 for entry in &entries {
307 let (variables, constraints, expr_nodes) = entry.capacity();
308 additional_variables += variables;
309 additional_constraints += constraints;
310 additional_expr_nodes += expr_nodes;
311 }
312 Self { entries, additional_variables, additional_constraints, additional_expr_nodes }
313 }
314
315 fn apply(self, model: &Model) -> Vec<SosReformulationArtifacts> {
316 model.variables.borrow_mut().reserve_exact(self.additional_variables);
317 model.var_names.borrow_mut().reserve(self.additional_variables);
318 model.constraints.borrow_mut().reserve_exact(self.additional_constraints);
319 model.constraint_names.borrow_mut().reserve(self.additional_constraints);
320 model.arena.borrow_mut().__reserve_nodes(self.additional_expr_nodes);
321
322 let mut artifacts = Vec::with_capacity(self.entries.len());
323 for entry in self.entries {
324 artifacts.push(entry.apply(model));
325 }
326 if !artifacts.is_empty() {
327 model.invalidate_kind();
328 }
329 model.sos_reformulations.borrow_mut().extend(artifacts.iter().cloned());
330 artifacts
331 }
332}
333
334impl PlannedSos {
335 fn capacity(&self) -> (usize, usize, usize) {
336 let (activation_count, members, sos2) = match &self.form {
337 PlannedSosForm::Trivial => return (0, 0, 0),
338 PlannedSosForm::Sos1 { members } => (members.len(), members.as_slice(), false),
339 PlannedSosForm::Sos2 { members } => (members.len() - 1, members.as_slice(), true),
340 };
341 let gate_count: usize = members.iter().copied().map(PlannedMember::gate_count).sum();
342 let gated_members = members.iter().filter(|member| member.gate_count() > 0).count();
343 let adjacent_sums = if sos2 {
344 members
345 .iter()
346 .enumerate()
347 .filter(|(index, member)| {
348 *index > 0 && *index + 1 < members.len() && member.gate_count() > 0
349 })
350 .count()
351 } else {
352 0
353 };
354 let constraints = 1 + gate_count;
355 let expr_nodes = activation_count
356 + activation_count.saturating_sub(1)
357 + gated_members
358 + 4 * gate_count
359 + adjacent_sums;
360 (activation_count, constraints, expr_nodes)
361 }
362
363 fn apply(self, model: &Model) -> SosReformulationArtifacts {
364 let (variable_capacity, constraint_capacity, _) = self.capacity();
365 let mut generated_variables = Vec::with_capacity(variable_capacity);
366 let mut generated_constraints = Vec::with_capacity(constraint_capacity);
367 match self.form {
368 PlannedSosForm::Trivial => {}
369 PlannedSosForm::Sos1 { members } => {
370 let activations = add_binary_activations(
371 model,
372 self.source,
373 members.len(),
374 "member",
375 &mut generated_variables,
376 );
377 add_at_most_one(model, self.source, &activations, &mut generated_constraints);
378 for (member, activation) in members.iter().zip(activations.iter().copied()) {
379 add_planned_member_gates(
380 model,
381 self.source,
382 member,
383 activation,
384 &mut generated_constraints,
385 );
386 }
387 }
388 PlannedSosForm::Sos2 { members } => {
389 let intervals = add_binary_activations(
390 model,
391 self.source,
392 members.len() - 1,
393 "interval",
394 &mut generated_variables,
395 );
396 add_at_most_one(model, self.source, &intervals, &mut generated_constraints);
397 for (index, member) in members.iter().enumerate() {
398 if member.gate_count() == 0 {
399 continue;
400 }
401 let activation = match index {
402 0 => intervals[0],
403 i if i + 1 == members.len() => intervals[i - 1],
404 i => intervals[i - 1] + intervals[i],
405 };
406 add_planned_member_gates(
407 model,
408 self.source,
409 member,
410 activation,
411 &mut generated_constraints,
412 );
413 }
414 }
415 }
416 model.sos_constraints.borrow_mut()[self.source.index()].active = false;
417 SosReformulationArtifacts {
418 source: self.source,
419 variables: generated_variables,
420 constraints: generated_constraints,
421 }
422 }
423}
424
425fn plan_one(
426 id: SosConstraintId,
427 source: &SosConstraint,
428 variables: &[Variable],
429 options: SosReformulationOptions,
430) -> Result<PlannedSos, ReformulationError> {
431 let form = match source.sos_type {
432 SosType::Sos1
433 if source.members.len() >= 2
434 && potential_nonzero_count(variables, &source.members) >= 2 =>
435 {
436 let members = source
437 .members
438 .iter()
439 .copied()
440 .enumerate()
441 .filter(|(_, member)| {
442 raw_effective_bounds(&variables[member.variable.index()]) != (0.0, 0.0)
443 })
444 .map(|(member_index, member)| {
445 let (lower, upper) = effective_bounds(
446 &variables[member.variable.index()],
447 &source.name,
448 options,
449 )?;
450 Ok(PlannedMember { member_index, member, lower, upper })
451 })
452 .collect::<Result<Vec<_>, ReformulationError>>()?;
453 PlannedSosForm::Sos1 { members }
454 }
455 SosType::Sos2 if source.members.len() >= 3 => {
456 let mut ordered = source.members.clone();
457 ordered.sort_by(|left, right| left.weight.total_cmp(&right.weight));
458 if sos2_requires_reformulation(variables, &ordered) {
459 let members = ordered
460 .into_iter()
461 .enumerate()
462 .map(|(member_index, member)| {
463 let (lower, upper) = effective_bounds(
464 &variables[member.variable.index()],
465 &source.name,
466 options,
467 )?;
468 Ok(PlannedMember { member_index, member, lower, upper })
469 })
470 .collect::<Result<Vec<_>, ReformulationError>>()?;
471 PlannedSosForm::Sos2 { members }
472 } else {
473 PlannedSosForm::Trivial
474 }
475 }
476 SosType::Sos1 | SosType::Sos2 => PlannedSosForm::Trivial,
477 };
478 Ok(PlannedSos { source: id, form })
479}
480
481fn add_binary_activations<'a>(
482 model: &'a Model,
483 sos_id: SosConstraintId,
484 count: usize,
485 label: &str,
486 generated: &mut Vec<VarId>,
487) -> Vec<Expr<'a>> {
488 (0..count)
489 .map(|index| {
490 let base = format!("__oximo_sos{}_{}_{}", sos_id.index(), label, index);
491 let name = unique_variable_name(model, &base);
492 let expression = model.__var(name).binary().build();
493 generated.push(expression.var_id().expect("new activation is a variable"));
494 expression
495 })
496 .collect()
497}
498
499fn add_at_most_one(
500 model: &Model,
501 sos_id: SosConstraintId,
502 activations: &[Expr<'_>],
503 generated: &mut Vec<ConstraintId>,
504) {
505 let sum = activations
506 .iter()
507 .copied()
508 .reduce(|left, right| left + right)
509 .expect("nontrivial SOS has at least one activation");
510 let name = unique_constraint_name(model, &format!("__oximo_sos{}_select", sos_id.index()));
511 generated.push(model.__add_constraint(name, sum.le(1.0)).id());
512}
513
514fn add_planned_member_gates(
515 model: &Model,
516 sos_id: SosConstraintId,
517 planned: &PlannedMember,
518 activation: Expr<'_>,
519 generated: &mut Vec<ConstraintId>,
520) {
521 if planned.gate_count() == 0 {
522 return;
523 }
524 let variable = Expr::from_var(&model.arena, planned.member.variable);
525 if planned.lower < 0.0 {
530 let lower_name = unique_constraint_name(
531 model,
532 &format!("__oximo_sos{}_member_{}_lower", sos_id.index(), planned.member_index),
533 );
534 generated
535 .push(model.__add_constraint(lower_name, variable.ge(planned.lower * activation)).id());
536 }
537 if planned.upper > 0.0 {
538 let upper_name = unique_constraint_name(
539 model,
540 &format!("__oximo_sos{}_member_{}_upper", sos_id.index(), planned.member_index),
541 );
542 generated
543 .push(model.__add_constraint(upper_name, variable.le(planned.upper * activation)).id());
544 }
545}
546
547fn effective_bounds(
548 variable: &Variable,
549 constraint_name: &SmolStr,
550 options: SosReformulationOptions,
551) -> Result<(f64, f64), ReformulationError> {
552 let (lower, upper) = raw_effective_bounds(variable);
553 let lower = finite_or_fallback(lower, -1.0, options, constraint_name, &variable.name, "lower")?;
554 let upper = finite_or_fallback(upper, 1.0, options, constraint_name, &variable.name, "upper")?;
555 Ok((lower, upper))
556}
557
558fn raw_effective_bounds(variable: &Variable) -> (f64, f64) {
559 match variable.domain {
560 Domain::SemiContinuous { threshold } | Domain::SemiInteger { threshold } => {
561 (threshold.min(0.0), variable.ub.max(0.0))
562 }
563 Domain::Real | Domain::Integer | Domain::Binary => (variable.lb, variable.ub),
564 }
565}
566
567fn potential_nonzero_count(variables: &[Variable], members: &[SosMember]) -> usize {
568 members
569 .iter()
570 .filter(|member| raw_effective_bounds(&variables[member.variable.index()]) != (0.0, 0.0))
571 .count()
572}
573
574fn sos2_requires_reformulation(variables: &[Variable], ordered_members: &[SosMember]) -> bool {
575 let mut potential = ordered_members.iter().enumerate().filter_map(|(index, member)| {
576 (raw_effective_bounds(&variables[member.variable.index()]) != (0.0, 0.0)).then_some(index)
577 });
578 let Some(first) = potential.next() else {
579 return false;
580 };
581 let Some(second) = potential.next() else {
582 return false;
583 };
584 potential.next().is_some() || second != first + 1
585}
586
587fn finite_or_fallback(
588 bound: f64,
589 sign: f64,
590 options: SosReformulationOptions,
591 constraint: &SmolStr,
592 variable: &SmolStr,
593 side: &'static str,
594) -> Result<f64, ReformulationError> {
595 if bound.is_finite() {
596 Ok(bound)
597 } else if let Some(big_m) = options.fallback_big_m {
598 Ok(sign * big_m)
599 } else {
600 Err(ReformulationError::MissingFiniteBound {
601 constraint: constraint.clone(),
602 variable: variable.clone(),
603 side,
604 })
605 }
606}
607
608fn unique_variable_name(model: &Model, base: &str) -> SmolStr {
609 unique_name(base, |candidate| model.variable_id(candidate).is_some())
610}
611
612fn unique_constraint_name(model: &Model, base: &str) -> SmolStr {
613 unique_name(base, |candidate| model.constraint_id(candidate).is_some())
614}
615
616fn unique_name(base: &str, exists: impl Fn(&str) -> bool) -> SmolStr {
617 if !exists(base) {
618 return base.into();
619 }
620 for suffix in 1_u64.. {
621 let candidate = format!("{base}_{suffix}");
622 if !exists(&candidate) {
623 return candidate.into();
624 }
625 }
626 unreachable!("u64 name suffix space exhausted")
627}