1use std::cell::{Cell, Ref, RefCell};
2use std::fmt;
3use std::marker::PhantomData;
4
5use oximo_expr::{
6 EvalError, Expr, ExprArena, ExprArenaCell, ExprArenaSnapshot, ExprClass, ExprId, ExprIdRemap,
7 ModelId, ModelMismatchError, ParamId, VarId, classify, extract_linear,
8};
9use rayon::prelude::*;
10use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
11use smol_str::SmolStr;
12
13#[cfg(test)]
14use crate::constraint::RangeConstraintIds;
15use crate::constraint::{
16 Constraint, ConstraintExpr, ConstraintHandle, ConstraintId, IntoRhs, RangeConstraintHandles,
17 Relate, Sense,
18};
19use crate::domain::Domain;
20use crate::error::{Error, Result};
21use crate::indexed::{
22 IndexedConstraint, IndexedFamily, IndexedIndicatorConstraint, IndexedParam,
23 IndexedRangeConstraint, IndexedRangeIndicatorConstraint, IndexedVar, build_storage,
24};
25use crate::indicator::{
26 IndicatorConstraint, IndicatorConstraintHandle, IndicatorConstraintId,
27 RangeIndicatorConstraintHandles,
28};
29use crate::objective::{Objective, ObjectiveSense};
30use crate::param::Parameter;
31use crate::reformulation::{IndicatorReformulationArtifacts, SosReformulationArtifacts};
32use crate::set::{Axis, FromIndexKey, IndexKey, Set};
33use crate::soc::{SocConstraint, SocConstraintHandle, SocConstraintId, is_detected_soc};
34use crate::sos::{
35 SosConstraint, SosConstraintHandle, SosConstraintId, SosMember, SosType, validate_members,
36};
37use crate::var::{VarBuilder, Variable};
38
39const PAR_KIND_THRESHOLD: usize = 256;
40const PAR_INDEXED_METADATA_THRESHOLD: usize = 1_024;
41const PAR_INDEXED_ALGEBRAIC_THRESHOLD: usize = 512;
42const PAR_INDEXED_RANGE_THRESHOLD: usize = 512;
43const PAR_INDEXED_SOC_THRESHOLD: usize = 256;
44const PAR_INDEXED_SOS_THRESHOLD: usize = 512;
45
46#[cold]
47#[inline(never)]
48fn model_mismatch(
49 expected: ModelId,
50 actual: ModelId,
51) -> std::result::Result<(), ModelMismatchError> {
52 Err(ModelMismatchError::new(expected, actual))
53}
54
55fn indexed_parallel(len: usize, forced: Option<bool>, threshold: usize) -> bool {
56 forced.unwrap_or(len >= threshold && rayon::current_num_threads() > 1)
57}
58
59fn indexed_chunk_size(len: usize) -> usize {
60 let chunks = rayon::current_num_threads().saturating_mul(4).max(1);
61 len.div_ceil(chunks).max(1)
62}
63
64fn arena_key(arena: &ExprArenaCell) -> usize {
65 std::ptr::from_ref(arena) as usize
66}
67
68fn assert_expr_arena(expr: Expr<'_>, expected: usize) {
69 assert_eq!(arena_key(expr.arena), expected, "expression belongs to a different model");
70}
71
72fn validate_batch_names<'a, V>(
73 existing: &FxHashMap<SmolStr, V>,
74 names: impl IntoIterator<Item = &'a SmolStr>,
75 kind: &str,
76 count: usize,
77) {
78 let mut batch = FxHashSet::with_capacity_and_hasher(count, FxBuildHasher);
79 for name in names {
80 assert!(!existing.contains_key(name), "{kind} name {name:?} already registered");
81 assert!(batch.insert(name), "{kind} name {name:?} occurs more than once");
82 }
83}
84
85#[derive(Debug)]
86struct PendingVar {
87 name: SmolStr,
88 lb: f64,
89 ub: f64,
90}
91
92#[derive(Debug)]
93struct PendingParam {
94 name: SmolStr,
95 value: f64,
96}
97
98#[derive(Debug)]
99struct PendingConstraint {
100 name: SmolStr,
101 lhs: ExprId,
102 lower: f64,
103 upper: f64,
104}
105
106#[derive(Debug)]
107struct PendingIndicator {
108 name: SmolStr,
109 trigger: VarId,
110 active_value: bool,
111 lhs: ExprId,
112 lower: f64,
113 upper: f64,
114}
115
116impl PendingConstraint {
117 fn from_expr(name: SmolStr, constraint: ConstraintExpr<'_>) -> Self {
118 let (lower, upper) = match constraint.sense {
119 Sense::Le => (f64::NEG_INFINITY, constraint.rhs),
120 Sense::Ge => (constraint.rhs, f64::INFINITY),
121 Sense::Eq => (constraint.rhs, constraint.rhs),
122 };
123 assert!(
124 !lower.is_nan() && !upper.is_nan(),
125 "constraint {name:?} has NaN bound (lower={lower}, upper={upper})"
126 );
127 Self { name, lhs: constraint.lhs.id, lower, upper }
128 }
129
130 fn remap(&mut self, remap: ExprIdRemap) {
131 self.lhs = remap.apply(self.lhs);
132 }
133}
134
135#[derive(Debug)]
136struct PendingRangeBatch {
137 rows: Vec<PendingConstraint>,
138 row_counts: Vec<u8>,
139}
140
141fn prepare_range<'a, B1: IntoRhs<'a>, B2: IntoRhs<'a>>(
142 name: String,
143 mid: Expr<'a>,
144 lo: B1,
145 hi: B2,
146) -> (PendingConstraint, Option<PendingConstraint>) {
147 if let (Some(lower), Some(upper)) = (lo.const_bound(), hi.const_bound())
148 && mid.__class() == ExprClass::Linear
149 {
150 assert!(
151 !lower.is_nan() && !upper.is_nan(),
152 "constraint {name:?} has NaN bound (lower={lower}, upper={upper})"
153 );
154 (PendingConstraint { name: name.into(), lhs: mid.id, lower, upper }, None)
155 } else {
156 (
157 PendingConstraint::from_expr(format!("{name}_lo").into(), mid.ge(lo)),
158 Some(PendingConstraint::from_expr(format!("{name}_hi").into(), mid.le(hi))),
159 )
160 }
161}
162
163#[derive(Debug)]
164struct PendingSoc {
165 name: SmolStr,
166 terms: Vec<ExprId>,
167 bound: ExprId,
168}
169
170impl PendingSoc {
171 fn remap(&mut self, remap: ExprIdRemap) {
172 for term in &mut self.terms {
173 *term = remap.apply(*term);
174 }
175 self.bound = remap.apply(self.bound);
176 }
177}
178
179fn prepare_soc<'a>(
180 name: SmolStr,
181 terms: impl IntoIterator<Item = Expr<'a>>,
182 bound: Expr<'a>,
183) -> PendingSoc {
184 let terms: Vec<ExprId> = terms
185 .into_iter()
186 .map(|term| {
187 assert!(
188 term.__class() == ExprClass::Linear,
189 "SOC constraint {name:?} has a non-affine term"
190 );
191 term.id
192 })
193 .collect();
194 assert!(!terms.is_empty(), "SOC constraint {name:?} has no terms");
195 assert!(bound.__class() == ExprClass::Linear, "SOC constraint {name:?} has a non-affine bound");
196 PendingSoc { name, terms, bound: bound.id }
197}
198
199#[derive(Debug)]
200struct PendingSos {
201 name: SmolStr,
202 members: Vec<SosMember>,
203}
204
205#[derive(Copy, Clone, Debug, PartialEq, Eq)]
222pub enum ModelKind {
223 LP,
224 MILP,
225 QP,
226 MIQP,
227 QCP,
228 MIQCP,
229 SOCP,
230 MISOCP,
231 NLP,
232 MINLP,
233}
234
235impl fmt::Display for ModelKind {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 f.write_str(match self {
238 Self::LP => "LP",
239 Self::MILP => "MILP",
240 Self::QP => "QP",
241 Self::MIQP => "MIQP",
242 Self::QCP => "QCP",
243 Self::MIQCP => "MIQCP",
244 Self::SOCP => "SOCP",
245 Self::MISOCP => "MISOCP",
246 Self::NLP => "NLP",
247 Self::MINLP => "MINLP",
248 })
249 }
250}
251
252#[derive(Copy, Clone, Debug)]
258pub enum ConstraintRef<'a> {
259 Algebraic { id: ConstraintId, constraint: &'a Constraint },
260 SecondOrderCone { id: SocConstraintId, constraint: &'a SocConstraint },
261 SpecialOrderedSet { id: SosConstraintId, constraint: &'a SosConstraint },
262 Indicator { id: IndicatorConstraintId, constraint: &'a IndicatorConstraint },
263}
264
265#[derive(Debug)]
274pub struct ModelConstraints<'a> {
275 algebraic: Ref<'a, Vec<Constraint>>,
276 second_order_cones: Ref<'a, Vec<SocConstraint>>,
277 special_ordered_sets: Ref<'a, Vec<SosConstraint>>,
278 indicators: Ref<'a, Vec<IndicatorConstraint>>,
279}
280
281impl ModelConstraints<'_> {
282 pub fn algebraic(&self) -> &[Constraint] {
284 &self.algebraic
285 }
286
287 pub fn second_order_cones(&self) -> &[SocConstraint] {
289 &self.second_order_cones
290 }
291
292 pub fn special_ordered_sets(&self) -> &[SosConstraint] {
293 &self.special_ordered_sets
294 }
295
296 pub fn indicators(&self) -> &[IndicatorConstraint] {
297 &self.indicators
298 }
299
300 #[expect(
302 clippy::cast_possible_truncation,
303 reason = "registration rejects constraint counts above u32::MAX"
304 )]
305 pub fn iter(&self) -> impl DoubleEndedIterator<Item = ConstraintRef<'_>> + Clone {
306 let algebraic = self.algebraic.iter().enumerate().map(|(index, constraint)| {
307 ConstraintRef::Algebraic { id: ConstraintId(index as u32), constraint }
308 });
309 let second_order_cones =
310 self.second_order_cones.iter().enumerate().map(|(index, constraint)| {
311 ConstraintRef::SecondOrderCone { id: SocConstraintId(index as u32), constraint }
312 });
313 let special_ordered_sets =
314 self.special_ordered_sets.iter().enumerate().map(|(index, constraint)| {
315 ConstraintRef::SpecialOrderedSet { id: SosConstraintId(index as u32), constraint }
316 });
317 let indicators = self.indicators.iter().enumerate().map(|(index, constraint)| {
318 ConstraintRef::Indicator { id: IndicatorConstraintId(index as u32), constraint }
319 });
320 algebraic.chain(second_order_cones).chain(special_ordered_sets).chain(indicators)
321 }
322
323 pub fn len(&self) -> usize {
325 self.algebraic.len()
326 + self.second_order_cones.len()
327 + self.special_ordered_sets.len()
328 + self.indicators.len()
329 }
330
331 pub fn is_empty(&self) -> bool {
332 self.algebraic.is_empty()
333 && self.second_order_cones.is_empty()
334 && self.special_ordered_sets.is_empty()
335 && self.indicators.is_empty()
336 }
337}
338
339pub struct Model {
349 pub name: SmolStr,
350 pub(crate) arena: ExprArenaCell,
351 pub(crate) variables: RefCell<Vec<Variable>>,
352 pub(crate) var_names: RefCell<FxHashMap<SmolStr, VarId>>,
353 pub(crate) parameters: RefCell<Vec<Parameter>>,
354 pub(crate) param_names: RefCell<FxHashMap<SmolStr, ParamId>>,
355 pub(crate) constraints: RefCell<Vec<Constraint>>,
356 pub(crate) constraint_names: RefCell<FxHashMap<SmolStr, ConstraintId>>,
357 pub(crate) soc_constraints: RefCell<Vec<SocConstraint>>,
358 pub(crate) soc_names: RefCell<FxHashMap<SmolStr, SocConstraintId>>,
359 pub(crate) sos_constraints: RefCell<Vec<SosConstraint>>,
360 pub(crate) sos_names: RefCell<FxHashMap<SmolStr, SosConstraintId>>,
361 pub(crate) indicator_constraints: RefCell<Vec<IndicatorConstraint>>,
362 pub(crate) indicator_names: RefCell<FxHashMap<SmolStr, IndicatorConstraintId>>,
363 pub(crate) sos_reformulations: RefCell<Vec<SosReformulationArtifacts>>,
364 pub(crate) indicator_reformulations: RefCell<Vec<IndicatorReformulationArtifacts>>,
365 pub(crate) objective: RefCell<Option<Objective>>,
366 objective_declared: Cell<bool>,
367 cached_kind: Cell<Option<ModelKind>>,
368 auto_seq: Cell<u32>,
371}
372
373impl Model {
374 fn assert_expr_belongs(&self, expr: Expr<'_>) {
375 assert_expr_arena(expr, arena_key(&self.arena));
376 }
377
378 #[inline]
379 fn ensure_model_id(&self, actual: ModelId) -> std::result::Result<(), ModelMismatchError> {
380 let expected = self.id();
381 if actual == expected { Ok(()) } else { model_mismatch(expected, actual) }
382 }
383
384 #[inline]
385 fn ensure_expr_model(&self, expr: Expr<'_>) -> std::result::Result<(), ModelMismatchError> {
386 self.ensure_model_id(expr.model_id())
387 }
388
389 pub(crate) fn clone_preserving_ids_with_capacity(
391 &self,
392 additional_variables: usize,
393 additional_constraints: usize,
394 additional_expr_nodes: usize,
395 ) -> Self {
396 let variables = self.variables.borrow();
397 let mut cloned_variables =
398 Vec::with_capacity(variables.len().saturating_add(additional_variables));
399 cloned_variables.extend_from_slice(&variables);
400
401 let var_names = self.var_names.borrow();
402 let mut cloned_var_names = FxHashMap::with_capacity_and_hasher(
403 var_names.len().saturating_add(additional_variables),
404 FxBuildHasher,
405 );
406 cloned_var_names.extend(var_names.iter().map(|(name, id)| (name.clone(), *id)));
407
408 let constraints = self.constraints.borrow();
409 let mut cloned_constraints =
410 Vec::with_capacity(constraints.len().saturating_add(additional_constraints));
411 cloned_constraints.extend_from_slice(&constraints);
412
413 let constraint_names = self.constraint_names.borrow();
414 let mut cloned_constraint_names = FxHashMap::with_capacity_and_hasher(
415 constraint_names.len().saturating_add(additional_constraints),
416 FxBuildHasher,
417 );
418 cloned_constraint_names
419 .extend(constraint_names.iter().map(|(name, id)| (name.clone(), *id)));
420
421 Self {
422 name: self.name.clone(),
423 arena: ExprArenaCell::new(
424 self.arena.borrow().__clone_with_additional_capacity(additional_expr_nodes),
425 ),
426 variables: RefCell::new(cloned_variables),
427 var_names: RefCell::new(cloned_var_names),
428 parameters: RefCell::new(self.parameters.borrow().clone()),
429 param_names: RefCell::new(self.param_names.borrow().clone()),
430 constraints: RefCell::new(cloned_constraints),
431 constraint_names: RefCell::new(cloned_constraint_names),
432 soc_constraints: RefCell::new(self.soc_constraints.borrow().clone()),
433 soc_names: RefCell::new(self.soc_names.borrow().clone()),
434 sos_constraints: RefCell::new(self.sos_constraints.borrow().clone()),
435 sos_names: RefCell::new(self.sos_names.borrow().clone()),
436 indicator_constraints: RefCell::new(self.indicator_constraints.borrow().clone()),
437 indicator_names: RefCell::new(self.indicator_names.borrow().clone()),
438 sos_reformulations: RefCell::new(self.sos_reformulations.borrow().clone()),
439 indicator_reformulations: RefCell::new(self.indicator_reformulations.borrow().clone()),
440 objective: RefCell::new(self.objective.borrow().clone()),
441 objective_declared: Cell::new(self.objective_declared.get()),
442 cached_kind: Cell::new(self.cached_kind.get()),
443 auto_seq: Cell::new(self.auto_seq.get()),
444 }
445 }
446}
447
448impl std::fmt::Debug for Model {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 f.debug_struct("Model")
451 .field("name", &self.name)
452 .field("vars", &self.variables.borrow().len())
453 .field("params", &self.parameters.borrow().len())
454 .field("constraints", &self.constraints.borrow().len())
455 .field("soc_constraints", &self.soc_constraints.borrow().len())
456 .field("sos_constraints", &self.sos_constraints.borrow().len())
457 .field("indicator_constraints", &self.indicator_constraints.borrow().len())
458 .field("has_objective", &self.objective.borrow().is_some())
459 .field("feasibility", &self.is_feasibility())
460 .finish()
461 }
462}
463
464impl Model {
465 pub(crate) fn invalidate_kind(&self) {
466 self.cached_kind.set(None);
467 }
468
469 pub fn new(name: impl Into<SmolStr>) -> Self {
470 Self {
471 name: name.into(),
472 arena: ExprArenaCell::new(ExprArena::new()),
473 variables: RefCell::new(Vec::new()),
474 var_names: RefCell::new(FxHashMap::default()),
475 parameters: RefCell::new(Vec::new()),
476 param_names: RefCell::new(FxHashMap::default()),
477 constraints: RefCell::new(Vec::new()),
478 constraint_names: RefCell::new(FxHashMap::default()),
479 soc_constraints: RefCell::new(Vec::new()),
480 soc_names: RefCell::new(FxHashMap::default()),
481 sos_constraints: RefCell::new(Vec::new()),
482 sos_names: RefCell::new(FxHashMap::default()),
483 indicator_constraints: RefCell::new(Vec::new()),
484 indicator_names: RefCell::new(FxHashMap::default()),
485 sos_reformulations: RefCell::new(Vec::new()),
486 indicator_reformulations: RefCell::new(Vec::new()),
487 objective: RefCell::new(None),
488 objective_declared: Cell::new(false),
489 cached_kind: Cell::new(None),
490 auto_seq: Cell::new(0),
491 }
492 }
493
494 #[inline]
496 #[must_use]
497 pub const fn id(&self) -> ModelId {
498 self.arena.model_id()
499 }
500
501 #[doc(hidden)]
506 pub fn __var(&self, name: impl Into<SmolStr>) -> VarBuilder<'_> {
507 VarBuilder {
508 model: self,
509 name: name.into(),
510 lb: f64::NEG_INFINITY,
511 ub: f64::INFINITY,
512 domain: Domain::Real,
513 initial: None,
514 }
515 }
516
517 #[doc(hidden)]
519 pub fn __constant(&self, value: f64) -> Expr<'_> {
520 Expr::constant(&self.arena, value)
521 }
522
523 #[doc(hidden)]
526 pub fn __sum_context(&self) -> &ExprArenaCell {
527 &self.arena
528 }
529
530 pub(crate) fn register_var<'a>(&'a self, b: VarBuilder<'a>) -> Expr<'a> {
533 let mut names = self.var_names.borrow_mut();
534 assert!(
535 !names.contains_key(&b.name),
536 "variable name {:?} is already registered on this model",
537 b.name
538 );
539 let mut vars = self.variables.borrow_mut();
540 let id = VarId(u32::try_from(vars.len()).expect("variable count overflow"));
541 vars.push(Variable {
542 id,
543 name: b.name.clone(),
544 domain: b.domain,
545 lb: b.lb,
546 ub: b.ub,
547 initial: b.initial,
548 });
549 names.insert(b.name, id);
550 drop(vars);
551 drop(names);
552 self.cached_kind.set(None);
553 Expr::from_var(&self.arena, id)
554 }
555
556 fn register_vars_batch<'a>(&'a self, items: &[PendingVar], domain: Domain) -> Vec<Expr<'a>> {
557 let mut names = self.var_names.borrow_mut();
558 validate_batch_names(&names, items.iter().map(|item| &item.name), "variable", items.len());
559 let mut vars = self.variables.borrow_mut();
560 let final_count = vars.len().checked_add(items.len()).expect("variable count overflow");
561 if final_count > 0 {
562 u32::try_from(final_count - 1).expect("variable count overflow");
563 }
564 vars.reserve(items.len());
565 names.reserve(items.len());
566
567 let mut arena = self.arena.borrow_mut();
568 arena.__reserve_nodes(items.len());
569 let mut handles = Vec::with_capacity(items.len());
570 for item in items {
571 let id = VarId(u32::try_from(vars.len()).expect("variable count overflow"));
572 vars.push(Variable {
573 id,
574 name: item.name.clone(),
575 domain,
576 lb: item.lb,
577 ub: item.ub,
578 initial: None,
579 });
580 names.insert(item.name.clone(), id);
581 let node = arena.var(id);
582 handles.push(Expr::new(node, &self.arena));
583 }
584 self.cached_kind.set(None);
585 handles
586 }
587
588 #[doc(hidden)]
591 pub fn __indexed_var<'a, K>(
592 &'a self,
593 name: impl Into<String>,
594 set: &Set<K>,
595 ) -> IndexedVarBuilder<'a, K> {
596 IndexedVarBuilder {
597 model: self,
598 base_name: name.into(),
599 keys: set.iter().collect(),
600 axes: set.axes().map(Box::from),
601 lb: f64::NEG_INFINITY,
602 ub: f64::INFINITY,
603 lb_by: None,
604 ub_by: None,
605 domain: Domain::Real,
606 parallel: None,
607 _k: PhantomData,
608 }
609 }
610
611 pub fn variable_id(&self, name: &str) -> Option<VarId> {
612 self.var_names.borrow().get(name).copied()
613 }
614
615 #[must_use]
625 pub fn variable_handle(&self, id: VarId) -> Expr<'_> {
626 assert!(id.index() < self.variables.borrow().len(), "unknown variable ID {id:?}");
627 Expr::from_var(&self.arena, id)
628 }
629
630 pub fn variables(&self) -> Ref<'_, Vec<Variable>> {
631 self.variables.borrow()
632 }
633
634 pub fn arena(&self) -> ExprArenaSnapshot<'_> {
640 self.arena.borrow()
641 }
642
643 pub fn num_variables(&self) -> usize {
644 self.variables.borrow().len()
645 }
646
647 #[must_use]
651 pub fn describe_eval_error(&self, err: &EvalError) -> String {
652 match err {
653 EvalError::UnboundVar(v) => {
654 let name = crate::var::var_name(&self.variables.borrow(), *v);
655 format!("variable {name} has no value bound in the evaluation context")
656 }
657 EvalError::UnboundParam(p) => {
658 let name = self.parameters.borrow().iter().find(|par| par.id == *p).map_or_else(
659 || format!("parameter #{}", p.index()),
660 |par| par.name.to_string(),
661 );
662 format!("parameter {name} has no value bound in the evaluation context")
663 }
664 }
665 }
666
667 #[inline]
680 pub fn fix(&self, e: Expr<'_>, value: f64) -> std::result::Result<(), ModelMismatchError> {
681 self.ensure_expr_model(e)?;
682 let id = e.var_id().expect("Model::fix expects a single-variable expression");
683 self.fix_var(id, value);
684 Ok(())
685 }
686
687 pub fn fix_var(&self, id: VarId, value: f64) {
699 self.assert_reformulated_bounds_mutable(id);
700 let mut vars = self.variables.borrow_mut();
701 let v = &mut vars[id.index()];
702 crate::var::assert_fixable(&v.name, v.domain, v.lb, v.ub, value);
703 v.lb = value;
704 v.ub = value;
705 drop(vars);
706 self.cached_kind.set(None);
707 }
708
709 #[inline]
721 pub fn set_initial(
722 &self,
723 e: Expr<'_>,
724 value: f64,
725 ) -> std::result::Result<(), ModelMismatchError> {
726 self.ensure_expr_model(e)?;
727 let id = e.var_id().expect("Model::set_initial expects a single-variable expression");
728 self.variables.borrow_mut()[id.index()].initial = Some(value);
729 Ok(())
730 }
731
732 pub fn unfix_var(&self, id: VarId, lb: f64, ub: f64) {
740 self.assert_reformulated_bounds_mutable(id);
741 let mut vars = self.variables.borrow_mut();
742 let v = &mut vars[id.index()];
743 v.lb = lb;
744 v.ub = ub;
745 drop(vars);
746 self.cached_kind.set(None);
747 }
748
749 fn assert_reformulated_bounds_mutable(&self, id: VarId) {
752 if let Some(source) = self.sos_constraints.borrow().iter().find(|constraint| {
753 !constraint.active && constraint.members.iter().any(|member| member.variable == id)
754 }) {
755 panic!(
756 "cannot change bounds of variable {:?} after SOS constraint {:?} was reformulated; \
757 change bounds before reformulating or create a new reformulated model",
758 self.variables.borrow()[id.index()].name,
759 source.name,
760 );
761 }
762 let indicator_source = {
763 let sources = self.indicator_constraints.borrow();
764 let arena = self.arena.borrow();
765 sources
766 .iter()
767 .find(|source| {
768 !source.active
769 && extract_linear(&arena, source.lhs).is_some_and(|terms| {
770 id != source.trigger
771 && terms.coeffs.iter().any(|(variable, coefficient)| {
772 *variable == id && *coefficient != 0.0
773 })
774 })
775 })
776 .map(|source| source.name.clone())
777 };
778 if let Some(source) = indicator_source {
779 panic!(
780 "cannot change bounds of variable {:?} after indicator constraint {:?} was reformulated; \
781 change bounds before reformulating or create a new reformulated model",
782 self.variables.borrow()[id.index()].name,
783 source,
784 );
785 }
786 }
787
788 #[doc(hidden)]
803 pub fn __param<'a>(&'a self, name: impl Into<SmolStr>, value: f64) -> Expr<'a> {
804 self.register_param(name.into(), value)
805 }
806
807 fn register_param(&self, name: SmolStr, value: f64) -> Expr<'_> {
815 assert!(
816 !self.param_names.borrow().contains_key(&name),
817 "parameter name {name:?} is already registered on this model"
818 );
819 let (id, node) = {
820 let mut a = self.arena.borrow_mut();
821 let id = a.new_param(value);
822 (id, a.param(id))
823 };
824 self.parameters.borrow_mut().push(Parameter { id, name: name.clone() });
825 self.param_names.borrow_mut().insert(name, id);
826 self.cached_kind.set(None);
827 Expr::new(node, &self.arena)
828 }
829
830 #[doc(hidden)]
841 pub fn __indexed_param<'a, K, F>(
842 &'a self,
843 name: impl Into<String>,
844 set: &Set<K>,
845 value: F,
846 ) -> IndexedParam<'a, K>
847 where
848 K: FromIndexKey,
849 F: Fn(K) -> f64 + Send + Sync,
850 {
851 self.indexed_param_with(name.into(), set, &value, None)
852 }
853
854 fn indexed_param_with<'a, K, F>(
855 &'a self,
856 base: String,
857 set: &Set<K>,
858 value: &F,
859 forced_parallel: Option<bool>,
860 ) -> IndexedParam<'a, K>
861 where
862 K: FromIndexKey,
863 F: Fn(K) -> f64 + Send + Sync,
864 {
865 let axes = set.axes().map(Box::from);
866 let keys: Vec<IndexKey> = set.iter().collect();
867 if !indexed_parallel(keys.len(), forced_parallel, PAR_INDEXED_METADATA_THRESHOLD) {
868 let handles = keys
869 .iter()
870 .map(|key| {
871 let name: SmolStr = format_index_name(&base, key).into();
872 self.register_param(name, value(K::from_index_key(key)))
873 })
874 .collect();
875 let storage = build_storage(keys, axes, handles);
876 return IndexedFamily { storage, model_id: self.id(), _marker: PhantomData };
877 }
878 let prepare = |key: &IndexKey| PendingParam {
879 name: format_index_name(&base, key).into(),
880 value: value(K::from_index_key(key)),
881 };
882 let prepared: Vec<_> = keys.par_iter().map(prepare).collect();
883 let handles = self.register_params_batch(&prepared);
884 drop(prepared);
885 let storage = build_storage(keys, axes, handles);
886 IndexedFamily { storage, model_id: self.id(), _marker: PhantomData }
887 }
888
889 fn register_params_batch<'a>(&'a self, items: &[PendingParam]) -> Vec<Expr<'a>> {
890 let mut names = self.param_names.borrow_mut();
891 validate_batch_names(&names, items.iter().map(|item| &item.name), "parameter", items.len());
892 let mut params = self.parameters.borrow_mut();
893 let mut arena = self.arena.borrow_mut();
894 let final_count = params.len().checked_add(items.len()).expect("parameter count overflow");
895 if final_count > 0 {
896 u32::try_from(final_count - 1).expect("parameter count overflow");
897 }
898 params.reserve(items.len());
899 names.reserve(items.len());
900 arena.__reserve_nodes(items.len());
901
902 let mut handles = Vec::with_capacity(items.len());
903 for item in items {
904 let id = arena.new_param(item.value);
905 let node = arena.param(id);
906 params.push(Parameter { id, name: item.name.clone() });
907 names.insert(item.name.clone(), id);
908 handles.push(Expr::new(node, &self.arena));
909 }
910 handles
911 }
912
913 pub fn set_param_idx<K, Q: Into<IndexKey>>(
924 &self,
925 params: &IndexedParam<'_, K>,
926 key: Q,
927 value: f64,
928 ) -> std::result::Result<(), ModelMismatchError> {
929 self.ensure_model_id(params.model_id())?;
930 let e = params.get(key).expect("set_param_idx: key not present in indexed parameter");
931 let id = e.param_id().expect("indexed parameter entry is not a parameter handle");
932 self.set_param_id(id, value);
933 Ok(())
934 }
935
936 pub fn param_value_idx<K, Q: Into<IndexKey>>(
944 &self,
945 params: &IndexedParam<'_, K>,
946 key: Q,
947 ) -> std::result::Result<Option<f64>, ModelMismatchError> {
948 self.ensure_model_id(params.model_id())?;
949 params.get(key).map_or(Ok(None), |e| self.param_value_of(e))
950 }
951
952 #[inline]
963 pub fn set_param(
964 &self,
965 p: Expr<'_>,
966 value: f64,
967 ) -> std::result::Result<(), ModelMismatchError> {
968 self.ensure_expr_model(p)?;
969 let id = p.param_id().expect("Model::set_param expects a single-parameter expression");
970 self.set_param_id(id, value);
971 Ok(())
972 }
973
974 pub fn set_param_id(&self, id: ParamId, value: f64) {
981 self.arena.borrow_mut().set_param_value(id, value);
982 self.cached_kind.set(None);
983 }
984
985 pub fn param_value(&self, id: ParamId) -> f64 {
991 self.arena.borrow().param_value(id)
992 }
993
994 pub fn param_value_of(
1002 &self,
1003 p: Expr<'_>,
1004 ) -> std::result::Result<Option<f64>, ModelMismatchError> {
1005 self.ensure_expr_model(p)?;
1006 Ok(p.param_id().map(|id| self.param_value(id)))
1007 }
1008
1009 pub fn parameter_id(&self, name: &str) -> Option<ParamId> {
1010 self.param_names.borrow().get(name).copied()
1011 }
1012
1013 pub fn parameters(&self) -> Ref<'_, Vec<Parameter>> {
1014 self.parameters.borrow()
1015 }
1016
1017 pub fn num_parameters(&self) -> usize {
1018 self.parameters.borrow().len()
1019 }
1020
1021 #[doc(hidden)]
1031 pub fn __add_constraint(
1032 &self,
1033 name: impl Into<SmolStr>,
1034 c: ConstraintExpr<'_>,
1035 ) -> ConstraintHandle {
1036 self.assert_expr_belongs(c.lhs);
1037 let (lower, upper) = match c.sense {
1038 Sense::Le => (f64::NEG_INFINITY, c.rhs),
1039 Sense::Ge => (c.rhs, f64::INFINITY),
1040 Sense::Eq => (c.rhs, c.rhs),
1041 };
1042 ConstraintHandle::new(
1043 self.register_constraint(name.into(), c.lhs.id, lower, upper),
1044 self.id(),
1045 )
1046 }
1047
1048 fn register_constraint(
1056 &self,
1057 name: SmolStr,
1058 lhs: ExprId,
1059 lower: f64,
1060 upper: f64,
1061 ) -> ConstraintId {
1062 assert!(
1063 !lower.is_nan() && !upper.is_nan(),
1064 "constraint {name:?} has NaN bound (lower={lower}, upper={upper})"
1065 );
1066 let mut by_name = self.constraint_names.borrow_mut();
1067 assert!(!by_name.contains_key(&name), "constraint name {name:?} already registered");
1068 let mut all = self.constraints.borrow_mut();
1069 let id = ConstraintId(u32::try_from(all.len()).expect("constraint count overflow"));
1070 all.push(Constraint { name: name.clone(), lhs, lower, upper, active: true });
1071 by_name.insert(name, id);
1072 self.cached_kind.set(None);
1073 id
1074 }
1075
1076 fn register_prevalidated_constraints_batch(
1078 &self,
1079 items: Vec<PendingConstraint>,
1080 ) -> Vec<ConstraintId> {
1081 let mut names = self.constraint_names.borrow_mut();
1082 let mut constraints = self.constraints.borrow_mut();
1083 let final_count =
1084 constraints.len().checked_add(items.len()).expect("constraint count overflow");
1085 if final_count > 0 {
1086 u32::try_from(final_count - 1).expect("constraint count overflow");
1087 }
1088 constraints.reserve(items.len());
1089 names.reserve(items.len());
1090 let mut ids = Vec::with_capacity(items.len());
1091 for item in items {
1092 let id =
1093 ConstraintId(u32::try_from(constraints.len()).expect("constraint count overflow"));
1094 constraints.push(Constraint {
1095 name: item.name.clone(),
1096 lhs: item.lhs,
1097 lower: item.lower,
1098 upper: item.upper,
1099 active: true,
1100 });
1101 names.insert(item.name, id);
1102 ids.push(id);
1103 }
1104 self.cached_kind.set(None);
1105 ids
1106 }
1107
1108 fn next_auto_name(&self) -> SmolStr {
1110 loop {
1111 let n = self.auto_seq.get();
1112 self.auto_seq.set(n + 1);
1113 let candidate: SmolStr = format!("_c{n}").into();
1114 if !self.constraint_names.borrow().contains_key(&candidate) {
1115 break candidate;
1116 }
1117 }
1118 }
1119
1120 #[doc(hidden)]
1123 pub fn __add_constraint_auto(&self, c: ConstraintExpr<'_>) -> ConstraintHandle {
1124 self.__add_constraint(self.next_auto_name(), c)
1125 }
1126
1127 #[doc(hidden)]
1130 pub fn __add_constraint_interval(
1131 &self,
1132 name: impl Into<SmolStr>,
1133 lhs: Expr<'_>,
1134 lower: f64,
1135 upper: f64,
1136 ) -> ConstraintId {
1137 self.assert_expr_belongs(lhs);
1138 self.register_constraint(name.into(), lhs.id, lower, upper)
1139 }
1140
1141 pub fn add_constraints<'a, I>(&'a self, items: I)
1144 where
1145 I: IntoIterator<Item = (SmolStr, ConstraintExpr<'a>)>,
1146 {
1147 for (name, c) in items {
1148 self.__add_constraint(name, c);
1149 }
1150 }
1151
1152 #[doc(hidden)]
1157 pub fn __add_constraints_over<'a, K, F>(
1158 &'a self,
1159 name_prefix: &str,
1160 set: &Set<K>,
1161 rule: F,
1162 ) -> IndexedConstraint<K>
1163 where
1164 K: FromIndexKey,
1165 F: Fn(K) -> ConstraintExpr<'a> + Send + Sync,
1166 {
1167 self.add_constraints_over_with(name_prefix, set, &rule, None)
1168 }
1169
1170 fn add_constraints_over_with<'a, K, F>(
1171 &'a self,
1172 name_prefix: &str,
1173 set: &Set<K>,
1174 rule: &F,
1175 forced_parallel: Option<bool>,
1176 ) -> IndexedConstraint<K>
1177 where
1178 K: FromIndexKey,
1179 F: Fn(K) -> ConstraintExpr<'a> + Send + Sync,
1180 {
1181 let keys: Vec<IndexKey> = set.iter().collect();
1182 if !indexed_parallel(keys.len(), forced_parallel, PAR_INDEXED_ALGEBRAIC_THRESHOLD) {
1183 let mut ids = Vec::with_capacity(keys.len());
1184 for key in &keys {
1185 let constraint = rule(K::from_index_key(key));
1186 self.assert_expr_belongs(constraint.lhs);
1187 let name: SmolStr = format_index_name(name_prefix, key).into();
1188 ids.push(self.__add_constraint(name, constraint));
1189 }
1190 return IndexedConstraint::new(keys, set.axes(), ids);
1191 }
1192
1193 let arena = &self.arena;
1194 let expected_arena = arena_key(arena);
1195 let mut batch = arena.__begin_batch();
1196 let snapshot = batch.snapshot();
1197 let chunk_size = indexed_chunk_size(keys.len());
1198 let mut forks: Vec<_> = keys
1199 .par_chunks(chunk_size)
1200 .map(|chunk| {
1201 arena.__with_fork(snapshot.clone(), || {
1202 chunk
1203 .iter()
1204 .map(|key| {
1205 let name = format_index_name(name_prefix, key).into();
1206 let constraint = rule(K::from_index_key(key));
1207 assert_expr_arena(constraint.lhs, expected_arena);
1208 PendingConstraint::from_expr(name, constraint)
1209 })
1210 .collect::<Vec<_>>()
1211 })
1212 })
1213 .collect();
1214 drop(snapshot);
1215 {
1216 let existing = self.constraint_names.borrow();
1217 validate_batch_names(
1218 &existing,
1219 forks.iter().flat_map(|fork| fork.value.iter().map(|item| &item.name)),
1220 "constraint",
1221 keys.len(),
1222 );
1223 }
1224 let remaps = batch.merge(&mut forks);
1225 let mut pending = Vec::with_capacity(keys.len());
1226 for (mut fork, remap) in forks.into_iter().zip(remaps) {
1227 for item in &mut fork.value {
1228 item.remap(remap);
1229 }
1230 pending.extend(fork.value);
1231 }
1232 drop(batch);
1233 let ids = self
1234 .register_prevalidated_constraints_batch(pending)
1235 .into_iter()
1236 .map(|id| ConstraintHandle::new(id, self.id()))
1237 .collect();
1238 IndexedConstraint::new(keys, set.axes(), ids)
1239 }
1240
1241 #[doc(hidden)]
1247 pub fn __add_range<'a, B1, B2>(
1248 &'a self,
1249 name: &str,
1250 mid: Expr<'a>,
1251 lo: B1,
1252 hi: B2,
1253 ) -> RangeConstraintHandles
1254 where
1255 B1: IntoRhs<'a>,
1256 B2: IntoRhs<'a>,
1257 {
1258 self.assert_expr_belongs(mid);
1259 if let Some((lower, upper)) = self.collapse_bounds(mid.id, &lo, &hi) {
1260 RangeConstraintHandles::Interval(ConstraintHandle::new(
1261 self.register_constraint(name.into(), mid.id, lower, upper),
1262 self.id(),
1263 ))
1264 } else {
1265 let lower = self.__add_constraint(format!("{name}_lo"), mid.ge(lo));
1266 let upper = self.__add_constraint(format!("{name}_hi"), mid.le(hi));
1267 RangeConstraintHandles::Split { lower, upper }
1268 }
1269 }
1270
1271 #[doc(hidden)]
1273 pub fn __add_range_auto<'a, B1, B2>(
1274 &'a self,
1275 mid: Expr<'a>,
1276 lo: B1,
1277 hi: B2,
1278 ) -> RangeConstraintHandles
1279 where
1280 B1: IntoRhs<'a>,
1281 B2: IntoRhs<'a>,
1282 {
1283 self.assert_expr_belongs(mid);
1284 if let Some((lower, upper)) = self.collapse_bounds(mid.id, &lo, &hi) {
1285 RangeConstraintHandles::Interval(ConstraintHandle::new(
1286 self.register_constraint(self.next_auto_name(), mid.id, lower, upper),
1287 self.id(),
1288 ))
1289 } else {
1290 let lower = self.__add_constraint_auto(mid.ge(lo));
1291 let upper = self.__add_constraint_auto(mid.le(hi));
1292 RangeConstraintHandles::Split { lower, upper }
1293 }
1294 }
1295
1296 fn collapse_bounds<'a>(
1300 &self,
1301 mid: ExprId,
1302 lo: &impl IntoRhs<'a>,
1303 hi: &impl IntoRhs<'a>,
1304 ) -> Option<(f64, f64)> {
1305 let lower = lo.const_bound()?;
1306 let upper = hi.const_bound()?;
1307 (classify(&self.arena.borrow(), mid) == ExprClass::Linear).then_some((lower, upper))
1308 }
1309
1310 #[doc(hidden)]
1313 pub fn __add_range_constraints_over<'a, K, B1, B2, F>(
1314 &'a self,
1315 name: &str,
1316 set: &Set<K>,
1317 rule: F,
1318 ) -> IndexedRangeConstraint<K>
1319 where
1320 K: FromIndexKey,
1321 B1: IntoRhs<'a>,
1322 B2: IntoRhs<'a>,
1323 F: Fn(K) -> (Expr<'a>, B1, B2) + Send + Sync,
1324 {
1325 self.add_range_constraints_over_with(name, set, &rule, None)
1326 }
1327
1328 fn add_range_constraints_over_with<'a, K, B1, B2, F>(
1329 &'a self,
1330 name: &str,
1331 set: &Set<K>,
1332 rule: &F,
1333 forced_parallel: Option<bool>,
1334 ) -> IndexedRangeConstraint<K>
1335 where
1336 K: FromIndexKey,
1337 B1: IntoRhs<'a>,
1338 B2: IntoRhs<'a>,
1339 F: Fn(K) -> (Expr<'a>, B1, B2) + Send + Sync,
1340 {
1341 let keys: Vec<IndexKey> = set.iter().collect();
1342 if !indexed_parallel(keys.len(), forced_parallel, PAR_INDEXED_RANGE_THRESHOLD) {
1343 let mut ids = Vec::with_capacity(keys.len());
1344 for key in &keys {
1345 let (mid, lo, hi) = rule(K::from_index_key(key));
1346 self.assert_expr_belongs(mid);
1347 ids.push(self.__add_range(&format_index_name(name, key), mid, lo, hi));
1348 }
1349 return IndexedRangeConstraint::new(keys, set.axes(), ids);
1350 }
1351
1352 let arena = &self.arena;
1353 let expected_arena = arena_key(arena);
1354 let mut batch = arena.__begin_batch();
1355 let snapshot = batch.snapshot();
1356 let chunk_size = indexed_chunk_size(keys.len());
1357 let mut forks: Vec<_> = keys
1358 .par_chunks(chunk_size)
1359 .map(|chunk| {
1360 arena.__with_fork(snapshot.clone(), || {
1361 let mut prepared = PendingRangeBatch {
1362 rows: Vec::with_capacity(chunk.len()),
1363 row_counts: Vec::with_capacity(chunk.len()),
1364 };
1365 for key in chunk {
1366 let (mid, lo, hi) = rule(K::from_index_key(key));
1367 assert_expr_arena(mid, expected_arena);
1368 let (first, upper) =
1369 prepare_range(format_index_name(name, key), mid, lo, hi);
1370 prepared.row_counts.push(if upper.is_some() { 2 } else { 1 });
1371 prepared.rows.push(first);
1372 prepared.rows.extend(upper);
1373 }
1374 prepared
1375 })
1376 })
1377 .collect();
1378 drop(snapshot);
1379 let row_count = forks.iter().map(|fork| fork.value.rows.len()).sum();
1380 {
1381 let existing = self.constraint_names.borrow();
1382 validate_batch_names(
1383 &existing,
1384 forks.iter().flat_map(|fork| fork.value.rows.iter().map(|item| &item.name)),
1385 "constraint",
1386 row_count,
1387 );
1388 }
1389 let remaps = batch.merge(&mut forks);
1390 let mut pending = Vec::with_capacity(row_count);
1391 let mut row_counts = Vec::with_capacity(keys.len());
1392 for (mut fork, remap) in forks.into_iter().zip(remaps) {
1393 for item in &mut fork.value.rows {
1394 item.remap(remap);
1395 }
1396 row_counts.extend(fork.value.row_counts);
1397 pending.extend(fork.value.rows);
1398 }
1399 drop(batch);
1400 let mut ids = self.register_prevalidated_constraints_batch(pending).into_iter();
1401 let groups = row_counts
1402 .into_iter()
1403 .map(|count| {
1404 let first = ids.next().expect("range row ID missing");
1405 match count {
1406 1 => RangeConstraintHandles::Interval(ConstraintHandle::new(first, self.id())),
1407 2 => RangeConstraintHandles::Split {
1408 lower: ConstraintHandle::new(first, self.id()),
1409 upper: ConstraintHandle::new(
1410 ids.next().expect("upper range row ID missing"),
1411 self.id(),
1412 ),
1413 },
1414 _ => unreachable!("range must lower to one or two rows"),
1415 }
1416 })
1417 .collect();
1418 IndexedRangeConstraint::new(keys, set.axes(), groups)
1419 }
1420
1421 pub fn constraints(&self) -> ModelConstraints<'_> {
1424 ModelConstraints {
1425 algebraic: self.constraints.borrow(),
1426 second_order_cones: self.soc_constraints.borrow(),
1427 special_ordered_sets: self.sos_constraints.borrow(),
1428 indicators: self.indicator_constraints.borrow(),
1429 }
1430 }
1431
1432 pub fn num_constraints(&self) -> usize {
1434 self.constraints.borrow().len()
1435 + self.soc_constraints.borrow().len()
1436 + self.sos_constraints.borrow().len()
1437 + self.indicator_constraints.borrow().len()
1438 }
1439
1440 pub fn constraint_id(&self, name: &str) -> Option<ConstraintId> {
1441 self.constraint_names.borrow().get(name).copied()
1442 }
1443
1444 pub fn constraint_handle(&self, name: &str) -> Option<ConstraintHandle> {
1446 self.constraint_id(name).map(|id| ConstraintHandle::new(id, self.id()))
1447 }
1448
1449 pub fn constraint_handle_from_id(&self, id: ConstraintId) -> Option<ConstraintHandle> {
1451 (id.index() < self.constraints.borrow().len()).then(|| ConstraintHandle::new(id, self.id()))
1452 }
1453
1454 pub fn add_soc_constraint<'a>(
1469 &'a self,
1470 name: impl Into<SmolStr>,
1471 terms: impl IntoIterator<Item = Expr<'a>>,
1472 bound: Expr<'a>,
1473 ) -> SocConstraintHandle {
1474 let name = name.into();
1475 let arena = self.arena.borrow();
1476 let terms: Vec<ExprId> = terms
1477 .into_iter()
1478 .map(|e| {
1479 self.assert_expr_belongs(e);
1480 assert!(
1481 classify(&arena, e.id) == ExprClass::Linear,
1482 "SOC constraint {name:?} has a non-affine term"
1483 );
1484 e.id
1485 })
1486 .collect();
1487 assert!(!terms.is_empty(), "SOC constraint {name:?} has no terms");
1488 self.assert_expr_belongs(bound);
1489 assert!(
1490 classify(&arena, bound.id) == ExprClass::Linear,
1491 "SOC constraint {name:?} has a non-affine bound"
1492 );
1493 drop(arena);
1494
1495 let mut by_name = self.soc_names.borrow_mut();
1496 assert!(!by_name.contains_key(&name), "SOC constraint name {name:?} already registered");
1497 let mut all = self.soc_constraints.borrow_mut();
1498 let id = SocConstraintId(u32::try_from(all.len()).expect("SOC constraint count overflow"));
1499 all.push(SocConstraint { name: name.clone(), terms, bound: bound.id, active: true });
1500 by_name.insert(name, id);
1501 self.cached_kind.set(None);
1502 SocConstraintHandle::new(id, self.id())
1503 }
1504
1505 fn register_prevalidated_soc_batch(&self, items: Vec<PendingSoc>) {
1507 let mut names = self.soc_names.borrow_mut();
1508 let mut constraints = self.soc_constraints.borrow_mut();
1509 let final_count =
1510 constraints.len().checked_add(items.len()).expect("SOC constraint count overflow");
1511 if final_count > 0 {
1512 u32::try_from(final_count - 1).expect("SOC constraint count overflow");
1513 }
1514 constraints.reserve(items.len());
1515 names.reserve(items.len());
1516 for item in items {
1517 let id = SocConstraintId(
1518 u32::try_from(constraints.len()).expect("SOC constraint count overflow"),
1519 );
1520 constraints.push(SocConstraint {
1521 name: item.name.clone(),
1522 terms: item.terms,
1523 bound: item.bound,
1524 active: true,
1525 });
1526 names.insert(item.name, id);
1527 }
1528 self.cached_kind.set(None);
1529 }
1530
1531 fn next_auto_soc_name(&self) -> SmolStr {
1535 loop {
1536 let n = self.auto_seq.get();
1537 self.auto_seq.set(n + 1);
1538 let candidate: SmolStr = format!("_soc{n}").into();
1539 if !self.soc_names.borrow().contains_key(&candidate) {
1540 break candidate;
1541 }
1542 }
1543 }
1544
1545 #[doc(hidden)]
1549 pub fn __add_soc_constraint_auto<'a>(
1550 &'a self,
1551 terms: impl IntoIterator<Item = Expr<'a>>,
1552 bound: Expr<'a>,
1553 ) -> SocConstraintHandle {
1554 self.add_soc_constraint(self.next_auto_soc_name(), terms, bound)
1555 }
1556
1557 #[doc(hidden)]
1562 pub fn __add_soc_constraints_over<'a, K, T, F>(
1563 &'a self,
1564 name_prefix: &str,
1565 set: &Set<K>,
1566 rule: F,
1567 ) where
1568 K: FromIndexKey,
1569 T: IntoIterator<Item = Expr<'a>>,
1570 F: Fn(K) -> (T, Expr<'a>) + Send + Sync,
1571 {
1572 self.add_soc_constraints_over_with(name_prefix, set, &rule, None);
1573 }
1574
1575 fn add_soc_constraints_over_with<'a, K, T, F>(
1576 &'a self,
1577 name_prefix: &str,
1578 set: &Set<K>,
1579 rule: &F,
1580 forced_parallel: Option<bool>,
1581 ) where
1582 K: FromIndexKey,
1583 T: IntoIterator<Item = Expr<'a>>,
1584 F: Fn(K) -> (T, Expr<'a>) + Send + Sync,
1585 {
1586 let keys: Vec<IndexKey> = set.iter().collect();
1587 if !indexed_parallel(keys.len(), forced_parallel, PAR_INDEXED_SOC_THRESHOLD) {
1588 for key in &keys {
1589 let name: SmolStr = format_index_name(name_prefix, key).into();
1590 let (terms, bound) = rule(K::from_index_key(key));
1591 let terms: Vec<_> = terms.into_iter().collect();
1592 for &term in &terms {
1593 self.assert_expr_belongs(term);
1594 }
1595 self.assert_expr_belongs(bound);
1596 self.add_soc_constraint(name, terms, bound);
1597 }
1598 return;
1599 }
1600
1601 let arena = &self.arena;
1602 let expected_arena = arena_key(arena);
1603 let mut batch = arena.__begin_batch();
1604 let snapshot = batch.snapshot();
1605 let chunk_size = indexed_chunk_size(keys.len());
1606 let mut forks: Vec<_> = keys
1607 .par_chunks(chunk_size)
1608 .map(|chunk| {
1609 arena.__with_fork(snapshot.clone(), || {
1610 chunk
1611 .iter()
1612 .map(|key| {
1613 let name = format_index_name(name_prefix, key).into();
1614 let (terms, bound) = rule(K::from_index_key(key));
1615 assert_expr_arena(bound, expected_arena);
1616 let terms: Vec<_> = terms.into_iter().collect();
1617 for &term in &terms {
1618 assert_expr_arena(term, expected_arena);
1619 }
1620 prepare_soc(name, terms, bound)
1621 })
1622 .collect::<Vec<_>>()
1623 })
1624 })
1625 .collect();
1626 drop(snapshot);
1627 {
1628 let existing = self.soc_names.borrow();
1629 validate_batch_names(
1630 &existing,
1631 forks.iter().flat_map(|fork| fork.value.iter().map(|item| &item.name)),
1632 "SOC constraint",
1633 keys.len(),
1634 );
1635 }
1636 let remaps = batch.merge(&mut forks);
1637 let mut pending = Vec::with_capacity(keys.len());
1638 for (mut fork, remap) in forks.into_iter().zip(remaps) {
1639 for item in &mut fork.value {
1640 item.remap(remap);
1641 }
1642 pending.extend(fork.value);
1643 }
1644 drop(batch);
1645 self.register_prevalidated_soc_batch(pending);
1646 }
1647
1648 pub fn soc_constraints(&self) -> Ref<'_, Vec<SocConstraint>> {
1654 self.soc_constraints.borrow()
1655 }
1656
1657 pub fn num_soc_constraints(&self) -> usize {
1658 self.soc_constraints.borrow().len()
1659 }
1660
1661 pub fn soc_constraint_id(&self, name: &str) -> Option<SocConstraintId> {
1662 self.soc_names.borrow().get(name).copied()
1663 }
1664
1665 pub fn soc_constraint_handle(&self, name: &str) -> Option<SocConstraintHandle> {
1667 self.soc_constraint_id(name).map(|id| SocConstraintHandle::new(id, self.id()))
1668 }
1669
1670 pub fn soc_constraint_handle_from_id(
1672 &self,
1673 id: SocConstraintId,
1674 ) -> Option<SocConstraintHandle> {
1675 (id.index() < self.soc_constraints.borrow().len())
1676 .then(|| SocConstraintHandle::new(id, self.id()))
1677 }
1678
1679 pub fn has_cones(&self) -> bool {
1681 !self.soc_constraints.borrow().is_empty()
1682 }
1683
1684 pub fn add_sos_constraint<'a>(
1692 &'a self,
1693 name: impl Into<SmolStr>,
1694 sos_type: SosType,
1695 members: impl IntoIterator<Item = (Expr<'a>, f64)>,
1696 ) -> SosConstraintHandle<'a> {
1697 let name = name.into();
1698 let members: Vec<SosMember> = members
1699 .into_iter()
1700 .map(|(expr, weight)| {
1701 assert!(
1702 std::ptr::eq(expr.arena, &raw const self.arena),
1703 "SOS member belongs to another model"
1704 );
1705 let variable = expr.var_id().expect("SOS members must be bare variables");
1706 SosMember { variable, weight }
1707 })
1708 .collect();
1709 validate_members(&name, &members);
1710 let mut by_name = self.sos_names.borrow_mut();
1711 assert!(!by_name.contains_key(&name), "SOS constraint name {name:?} already registered");
1712 let mut all = self.sos_constraints.borrow_mut();
1713 let id = SosConstraintId(u32::try_from(all.len()).expect("SOS constraint count overflow"));
1714 all.push(SosConstraint { name: name.clone(), sos_type, members, active: true });
1715 by_name.insert(name, id);
1716 self.cached_kind.set(None);
1717 SosConstraintHandle { model: self, id }
1718 }
1719
1720 fn register_prevalidated_sos_batch(&self, sos_type: SosType, items: Vec<PendingSos>) {
1722 let mut names = self.sos_names.borrow_mut();
1723 let mut constraints = self.sos_constraints.borrow_mut();
1724 let final_count =
1725 constraints.len().checked_add(items.len()).expect("SOS constraint count overflow");
1726 if final_count > 0 {
1727 u32::try_from(final_count - 1).expect("SOS constraint count overflow");
1728 }
1729 constraints.reserve(items.len());
1730 names.reserve(items.len());
1731 for item in items {
1732 let id = SosConstraintId(
1733 u32::try_from(constraints.len()).expect("SOS constraint count overflow"),
1734 );
1735 constraints.push(SosConstraint {
1736 name: item.name.clone(),
1737 sos_type,
1738 members: item.members,
1739 active: true,
1740 });
1741 names.insert(item.name, id);
1742 }
1743 self.cached_kind.set(None);
1744 }
1745
1746 fn register_sos_one(&self, sos_type: SosType, item: PendingSos) {
1747 let mut names = self.sos_names.borrow_mut();
1748 assert!(
1749 !names.contains_key(&item.name),
1750 "SOS constraint name {:?} already registered",
1751 item.name
1752 );
1753 let mut constraints = self.sos_constraints.borrow_mut();
1754 let id = SosConstraintId(
1755 u32::try_from(constraints.len()).expect("SOS constraint count overflow"),
1756 );
1757 constraints.push(SosConstraint {
1758 name: item.name.clone(),
1759 sos_type,
1760 members: item.members,
1761 active: true,
1762 });
1763 names.insert(item.name, id);
1764 self.cached_kind.set(None);
1765 }
1766
1767 fn add_pending_sos_over(
1768 &self,
1769 keys: Vec<IndexKey>,
1770 sos_type: SosType,
1771 forced_parallel: Option<bool>,
1772 prepare: impl Fn(&IndexKey) -> PendingSos + Send + Sync,
1773 ) {
1774 if !indexed_parallel(keys.len(), forced_parallel, PAR_INDEXED_SOS_THRESHOLD) {
1775 for key in &keys {
1776 self.register_sos_one(sos_type, prepare(key));
1777 }
1778 return;
1779 }
1780
1781 let arena = &self.arena;
1782 let batch = arena.__begin_batch();
1783 let snapshot = batch.snapshot();
1784 let chunk_size = indexed_chunk_size(keys.len());
1785 let forks: Vec<_> = keys
1786 .par_chunks(chunk_size)
1787 .map(|chunk| {
1788 arena.__with_fork(snapshot.clone(), || {
1789 chunk.iter().map(&prepare).collect::<Vec<_>>()
1790 })
1791 })
1792 .collect();
1793 drop(snapshot);
1794 {
1795 let existing = self.sos_names.borrow();
1796 validate_batch_names(
1797 &existing,
1798 forks.iter().flat_map(|fork| fork.value.iter().map(|item| &item.name)),
1799 "SOS constraint",
1800 keys.len(),
1801 );
1802 }
1803 drop(batch);
1805 let pending = forks.into_iter().flat_map(|fork| fork.value).collect();
1806 self.register_prevalidated_sos_batch(sos_type, pending);
1807 }
1808
1809 pub fn add_sos_constraint_auto_weights<'a>(
1821 &'a self,
1822 name: impl Into<SmolStr>,
1823 sos_type: SosType,
1824 variables: impl IntoIterator<Item = Expr<'a>>,
1825 ) -> SosConstraintHandle<'a> {
1826 self.add_sos_constraint(
1827 name,
1828 sos_type,
1829 variables.into_iter().enumerate().map(|(index, variable)| {
1830 let weight = index
1831 .checked_add(1)
1832 .and_then(|index| u32::try_from(index).ok())
1833 .expect("SOS member count exceeds positional weight range");
1834 (variable, f64::from(weight))
1835 }),
1836 )
1837 }
1838
1839 fn next_auto_sos_name(&self) -> SmolStr {
1840 loop {
1841 let n = self.auto_seq.get();
1842 self.auto_seq.set(n + 1);
1843 let candidate: SmolStr = format!("_sos{n}").into();
1844 if !self.sos_names.borrow().contains_key(&candidate) {
1845 break candidate;
1846 }
1847 }
1848 }
1849
1850 #[doc(hidden)]
1851 pub fn __add_sos_constraint_auto<'a>(
1852 &'a self,
1853 sos_type: SosType,
1854 members: impl IntoIterator<Item = (Expr<'a>, f64)>,
1855 ) -> SosConstraintHandle<'a> {
1856 self.add_sos_constraint(self.next_auto_sos_name(), sos_type, members)
1857 }
1858
1859 #[doc(hidden)]
1860 pub fn __add_sos_constraint_auto_weights<'a>(
1861 &'a self,
1862 sos_type: SosType,
1863 variables: impl IntoIterator<Item = Expr<'a>>,
1864 ) -> SosConstraintHandle<'a> {
1865 self.add_sos_constraint_auto_weights(self.next_auto_sos_name(), sos_type, variables)
1866 }
1867
1868 #[doc(hidden)]
1869 pub fn __add_sos_constraints_over<'a, K, T, F>(
1870 &'a self,
1871 name_prefix: &str,
1872 set: &Set<K>,
1873 sos_type: SosType,
1874 rule: F,
1875 ) where
1876 K: FromIndexKey,
1877 T: IntoIterator<Item = (Expr<'a>, f64)>,
1878 F: Fn(K) -> T + Send + Sync,
1879 {
1880 self.add_sos_constraints_over_with(name_prefix, set, sos_type, &rule, None);
1881 }
1882
1883 fn add_sos_constraints_over_with<'a, K, T, F>(
1884 &'a self,
1885 name_prefix: &str,
1886 set: &Set<K>,
1887 sos_type: SosType,
1888 rule: &F,
1889 forced_parallel: Option<bool>,
1890 ) where
1891 K: FromIndexKey,
1892 T: IntoIterator<Item = (Expr<'a>, f64)>,
1893 F: Fn(K) -> T + Send + Sync,
1894 {
1895 let keys: Vec<IndexKey> = set.iter().collect();
1896 let expected_arena = arena_key(&self.arena);
1897 self.add_pending_sos_over(keys, sos_type, forced_parallel, |key| {
1898 let name: SmolStr = format_index_name(name_prefix, key).into();
1899 let members: Vec<_> = rule(K::from_index_key(key))
1900 .into_iter()
1901 .map(|(expr, weight)| {
1902 assert_expr_arena(expr, expected_arena);
1903 let variable = expr.var_id().expect("SOS members must be bare variables");
1904 SosMember { variable, weight }
1905 })
1906 .collect();
1907 validate_members(&name, &members);
1908 PendingSos { name, members }
1909 });
1910 }
1911
1912 #[doc(hidden)]
1913 pub fn __add_sos_constraints_over_auto_weights<'a, K, T, F>(
1914 &'a self,
1915 name_prefix: &str,
1916 set: &Set<K>,
1917 sos_type: SosType,
1918 rule: F,
1919 ) where
1920 K: FromIndexKey,
1921 T: IntoIterator<Item = Expr<'a>>,
1922 F: Fn(K) -> T + Send + Sync,
1923 {
1924 self.add_sos_constraints_over_auto_weights_with(name_prefix, set, sos_type, &rule, None);
1925 }
1926
1927 fn add_sos_constraints_over_auto_weights_with<'a, K, T, F>(
1928 &'a self,
1929 name_prefix: &str,
1930 set: &Set<K>,
1931 sos_type: SosType,
1932 rule: &F,
1933 forced_parallel: Option<bool>,
1934 ) where
1935 K: FromIndexKey,
1936 T: IntoIterator<Item = Expr<'a>>,
1937 F: Fn(K) -> T + Send + Sync,
1938 {
1939 let keys: Vec<IndexKey> = set.iter().collect();
1940 let expected_arena = arena_key(&self.arena);
1941 self.add_pending_sos_over(keys, sos_type, forced_parallel, |key| {
1942 let name: SmolStr = format_index_name(name_prefix, key).into();
1943 let members: Vec<_> = rule(K::from_index_key(key))
1944 .into_iter()
1945 .enumerate()
1946 .map(|(index, expr)| {
1947 let weight = index
1948 .checked_add(1)
1949 .and_then(|index| u32::try_from(index).ok())
1950 .expect("SOS member count exceeds positional weight range");
1951 assert_expr_arena(expr, expected_arena);
1952 let variable = expr.var_id().expect("SOS members must be bare variables");
1953 SosMember { variable, weight: f64::from(weight) }
1954 })
1955 .collect();
1956 validate_members(&name, &members);
1957 PendingSos { name, members }
1958 });
1959 }
1960
1961 pub fn sos_constraints(&self) -> Ref<'_, Vec<SosConstraint>> {
1962 self.sos_constraints.borrow()
1963 }
1964
1965 pub fn num_sos_constraints(&self) -> usize {
1966 self.sos_constraints.borrow().len()
1967 }
1968
1969 pub fn sos_constraint_id(&self, name: &str) -> Option<SosConstraintId> {
1970 self.sos_names.borrow().get(name).copied()
1971 }
1972
1973 pub fn has_sos_constraints(&self) -> bool {
1974 !self.sos_constraints.borrow().is_empty()
1975 }
1976
1977 pub fn has_active_sos_constraints(&self) -> bool {
1981 self.sos_constraints.borrow().iter().any(|constraint| constraint.active)
1982 }
1983
1984 fn indicator_trigger(&self, trigger: Expr<'_>) -> VarId {
1987 self.assert_expr_belongs(trigger);
1988 let id = trigger.var_id().expect("indicator trigger must be a bare binary variable");
1989 assert!(
1990 self.variables.borrow()[id.index()].domain == Domain::Binary,
1991 "indicator trigger must have binary domain"
1992 );
1993 id
1994 }
1995
1996 fn register_indicator(
1997 &self,
1998 name: SmolStr,
1999 trigger: VarId,
2000 active_value: bool,
2001 lhs: ExprId,
2002 lower: f64,
2003 upper: f64,
2004 ) -> IndicatorConstraintId {
2005 assert!(!lower.is_nan() && !upper.is_nan(), "indicator constraint {name:?} has NaN bound");
2006 assert!(
2007 classify(&self.arena.borrow(), lhs) == ExprClass::Linear,
2008 "indicator consequent must be affine"
2009 );
2010 let mut names = self.indicator_names.borrow_mut();
2011 assert!(
2012 !names.contains_key(&name),
2013 "indicator constraint name {name:?} already registered"
2014 );
2015 let mut all = self.indicator_constraints.borrow_mut();
2016 let id = IndicatorConstraintId(
2017 u32::try_from(all.len()).expect("indicator constraint count overflow"),
2018 );
2019 all.push(IndicatorConstraint {
2020 name: name.clone(),
2021 trigger,
2022 active_value,
2023 lhs,
2024 lower,
2025 upper,
2026 active: true,
2027 });
2028 names.insert(name, id);
2029 self.cached_kind.set(None);
2030 id
2031 }
2032
2033 fn prepare_indicator(
2034 &self,
2035 name: SmolStr,
2036 trigger: VarId,
2037 active_value: bool,
2038 consequent: ConstraintExpr<'_>,
2039 ) -> PendingIndicator {
2040 self.assert_expr_belongs(consequent.lhs);
2041 let (lower, upper) = match consequent.sense {
2042 Sense::Le => (f64::NEG_INFINITY, consequent.rhs),
2043 Sense::Ge => (consequent.rhs, f64::INFINITY),
2044 Sense::Eq => (consequent.rhs, consequent.rhs),
2045 };
2046 self.prepare_indicator_interval(
2047 name,
2048 trigger,
2049 active_value,
2050 consequent.lhs.id,
2051 lower,
2052 upper,
2053 )
2054 }
2055
2056 fn prepare_indicator_interval(
2057 &self,
2058 name: SmolStr,
2059 trigger: VarId,
2060 active_value: bool,
2061 lhs: ExprId,
2062 lower: f64,
2063 upper: f64,
2064 ) -> PendingIndicator {
2065 assert!(!lower.is_nan() && !upper.is_nan(), "indicator constraint {name:?} has NaN bound");
2066 assert!(
2067 classify(&self.arena.borrow(), lhs) == ExprClass::Linear,
2068 "indicator consequent must be affine"
2069 );
2070 PendingIndicator { name, trigger, active_value, lhs, lower, upper }
2071 }
2072
2073 fn register_indicators_batch(
2074 &self,
2075 items: Vec<PendingIndicator>,
2076 ) -> Vec<IndicatorConstraintId> {
2077 let mut names = self.indicator_names.borrow_mut();
2078 validate_batch_names(
2079 &names,
2080 items.iter().map(|item| &item.name),
2081 "indicator constraint",
2082 items.len(),
2083 );
2084 let mut constraints = self.indicator_constraints.borrow_mut();
2085 let final_count = constraints
2086 .len()
2087 .checked_add(items.len())
2088 .expect("indicator constraint count overflow");
2089 if final_count > 0 {
2090 u32::try_from(final_count - 1).expect("indicator constraint count overflow");
2091 }
2092 constraints.reserve(items.len());
2093 names.reserve(items.len());
2094 let mut ids = Vec::with_capacity(items.len());
2095 for item in items {
2096 let id = IndicatorConstraintId(
2097 u32::try_from(constraints.len()).expect("indicator constraint count overflow"),
2098 );
2099 constraints.push(IndicatorConstraint {
2100 name: item.name.clone(),
2101 trigger: item.trigger,
2102 active_value: item.active_value,
2103 lhs: item.lhs,
2104 lower: item.lower,
2105 upper: item.upper,
2106 active: true,
2107 });
2108 names.insert(item.name, id);
2109 ids.push(id);
2110 }
2111 if !ids.is_empty() {
2112 self.cached_kind.set(None);
2113 }
2114 ids
2115 }
2116
2117 pub fn add_indicator_constraint<'a>(
2119 &'a self,
2120 name: impl Into<SmolStr>,
2121 trigger: Expr<'a>,
2122 active_value: bool,
2123 consequent: ConstraintExpr<'a>,
2124 ) -> IndicatorConstraintHandle<'a> {
2125 self.assert_expr_belongs(consequent.lhs);
2126 let trigger = self.indicator_trigger(trigger);
2127 let (lower, upper) = match consequent.sense {
2128 Sense::Le => (f64::NEG_INFINITY, consequent.rhs),
2129 Sense::Ge => (consequent.rhs, f64::INFINITY),
2130 Sense::Eq => (consequent.rhs, consequent.rhs),
2131 };
2132 let id = self.register_indicator(
2133 name.into(),
2134 trigger,
2135 active_value,
2136 consequent.lhs.id,
2137 lower,
2138 upper,
2139 );
2140 IndicatorConstraintHandle { model: self, id }
2141 }
2142
2143 #[doc(hidden)]
2144 pub fn __add_indicator_constraint<'a>(
2145 &'a self,
2146 name: impl Into<SmolStr>,
2147 trigger: Expr<'a>,
2148 active_value: bool,
2149 consequent: ConstraintExpr<'a>,
2150 ) -> IndicatorConstraintHandle<'a> {
2151 self.add_indicator_constraint(name, trigger, active_value, consequent)
2152 }
2153
2154 fn next_auto_indicator_name(&self) -> SmolStr {
2155 loop {
2156 let n = self.auto_seq.get();
2157 self.auto_seq.set(n + 1);
2158 let candidate: SmolStr = format!("_ind{n}").into();
2159 if !self.indicator_names.borrow().contains_key(&candidate) {
2160 break candidate;
2161 }
2162 }
2163 }
2164
2165 #[doc(hidden)]
2166 pub fn __add_indicator_constraint_auto<'a>(
2167 &'a self,
2168 trigger: Expr<'a>,
2169 active_value: bool,
2170 consequent: ConstraintExpr<'a>,
2171 ) -> IndicatorConstraintHandle<'a> {
2172 self.add_indicator_constraint(
2173 self.next_auto_indicator_name(),
2174 trigger,
2175 active_value,
2176 consequent,
2177 )
2178 }
2179
2180 #[doc(hidden)]
2181 pub fn __add_indicator_interval<'a>(
2182 &'a self,
2183 name: impl Into<SmolStr>,
2184 trigger: Expr<'a>,
2185 active_value: bool,
2186 lhs: Expr<'a>,
2187 lower: f64,
2188 upper: f64,
2189 ) -> IndicatorConstraintHandle<'a> {
2190 self.assert_expr_belongs(lhs);
2191 let trigger = self.indicator_trigger(trigger);
2192 let id = self.register_indicator(name.into(), trigger, active_value, lhs.id, lower, upper);
2193 IndicatorConstraintHandle { model: self, id }
2194 }
2195
2196 #[doc(hidden)]
2197 pub fn __add_indicator_range<'a, B1: IntoRhs<'a>, B2: IntoRhs<'a>>(
2198 &'a self,
2199 name: &str,
2200 trigger: Expr<'a>,
2201 active_value: bool,
2202 mid: Expr<'a>,
2203 lo: B1,
2204 hi: B2,
2205 ) -> RangeIndicatorConstraintHandles<'a> {
2206 self.assert_expr_belongs(mid);
2207 let trigger = self.indicator_trigger(trigger);
2208 if let (Some(lower), Some(upper)) = (lo.const_bound(), hi.const_bound())
2209 && mid.__class() == ExprClass::Linear
2210 {
2211 let pending = self.prepare_indicator_interval(
2212 name.into(),
2213 trigger,
2214 active_value,
2215 mid.id,
2216 lower,
2217 upper,
2218 );
2219 let id = self.register_indicators_batch(vec![pending])[0];
2220 RangeIndicatorConstraintHandles::Interval(IndicatorConstraintHandle { model: self, id })
2221 } else {
2222 let lower = self.prepare_indicator(
2223 format!("{name}_lo").into(),
2224 trigger,
2225 active_value,
2226 mid.ge(lo),
2227 );
2228 let upper = self.prepare_indicator(
2229 format!("{name}_hi").into(),
2230 trigger,
2231 active_value,
2232 mid.le(hi),
2233 );
2234 let mut ids = self.register_indicators_batch(vec![lower, upper]).into_iter();
2235 let lower = IndicatorConstraintHandle {
2236 model: self,
2237 id: ids.next().expect("lower indicator range ID missing"),
2238 };
2239 let upper = IndicatorConstraintHandle {
2240 model: self,
2241 id: ids.next().expect("upper indicator range ID missing"),
2242 };
2243 RangeIndicatorConstraintHandles::Split { lower, upper }
2244 }
2245 }
2246
2247 #[doc(hidden)]
2248 pub fn __add_indicator_range_auto<'a, B1: IntoRhs<'a>, B2: IntoRhs<'a>>(
2249 &'a self,
2250 trigger: Expr<'a>,
2251 active_value: bool,
2252 mid: Expr<'a>,
2253 lo: B1,
2254 hi: B2,
2255 ) -> RangeIndicatorConstraintHandles<'a> {
2256 let name = self.next_auto_indicator_name();
2257 self.__add_indicator_range(&name, trigger, active_value, mid, lo, hi)
2258 }
2259
2260 #[doc(hidden)]
2261 pub fn __add_indicator_constraints_over<'a, K, F>(
2262 &'a self,
2263 prefix: &str,
2264 set: &Set<K>,
2265 rule: F,
2266 ) -> IndexedIndicatorConstraint<'a, K>
2267 where
2268 K: FromIndexKey,
2269 F: Fn(K) -> (Expr<'a>, bool, ConstraintExpr<'a>),
2270 {
2271 let keys: Vec<IndexKey> = set.iter().collect();
2272 let pending: Vec<_> = keys
2273 .iter()
2274 .map(|key| {
2275 let (trigger, value, consequent) = rule(K::from_index_key(key));
2276 let trigger = self.indicator_trigger(trigger);
2277 self.prepare_indicator(
2278 format_index_name(prefix, key).into(),
2279 trigger,
2280 value,
2281 consequent,
2282 )
2283 })
2284 .collect();
2285 let handles = self
2286 .register_indicators_batch(pending)
2287 .into_iter()
2288 .map(|id| IndicatorConstraintHandle { model: self, id })
2289 .collect();
2290 IndexedIndicatorConstraint::new(keys, set.axes(), handles)
2291 }
2292
2293 #[doc(hidden)]
2294 pub fn __add_indicator_ranges_over<'a, K, B1, B2, F>(
2295 &'a self,
2296 prefix: &str,
2297 set: &Set<K>,
2298 rule: F,
2299 ) -> IndexedRangeIndicatorConstraint<'a, K>
2300 where
2301 K: FromIndexKey,
2302 B1: IntoRhs<'a>,
2303 B2: IntoRhs<'a>,
2304 F: Fn(K) -> (Expr<'a>, bool, Expr<'a>, B1, B2),
2305 {
2306 let keys: Vec<IndexKey> = set.iter().collect();
2307 let mut pending = Vec::new();
2308 let mut row_counts = Vec::with_capacity(keys.len());
2309 for key in &keys {
2310 let (trigger, value, mid, lo, hi) = rule(K::from_index_key(key));
2311 self.assert_expr_belongs(mid);
2312 let trigger = self.indicator_trigger(trigger);
2313 let name = format_index_name(prefix, key);
2314 if let (Some(lower), Some(upper)) = (lo.const_bound(), hi.const_bound())
2315 && mid.__class() == ExprClass::Linear
2316 {
2317 pending.push(self.prepare_indicator_interval(
2318 name.into(),
2319 trigger,
2320 value,
2321 mid.id,
2322 lower,
2323 upper,
2324 ));
2325 row_counts.push(1_u8);
2326 } else {
2327 pending.push(self.prepare_indicator(
2328 format!("{name}_lo").into(),
2329 trigger,
2330 value,
2331 mid.ge(lo),
2332 ));
2333 pending.push(self.prepare_indicator(
2334 format!("{name}_hi").into(),
2335 trigger,
2336 value,
2337 mid.le(hi),
2338 ));
2339 row_counts.push(2_u8);
2340 }
2341 }
2342 let mut ids = self.register_indicators_batch(pending).into_iter();
2343 let handles = row_counts
2344 .into_iter()
2345 .map(|count| {
2346 let lower = IndicatorConstraintHandle {
2347 model: self,
2348 id: ids.next().expect("indicator range ID missing"),
2349 };
2350 match count {
2351 1 => RangeIndicatorConstraintHandles::Interval(lower),
2352 2 => RangeIndicatorConstraintHandles::Split {
2353 lower,
2354 upper: IndicatorConstraintHandle {
2355 model: self,
2356 id: ids.next().expect("upper indicator range ID missing"),
2357 },
2358 },
2359 _ => unreachable!("indicator range must lower to one or two rows"),
2360 }
2361 })
2362 .collect();
2363 IndexedRangeIndicatorConstraint::new(keys, set.axes(), handles)
2364 }
2365
2366 pub fn indicator_constraints(&self) -> Ref<'_, Vec<IndicatorConstraint>> {
2367 self.indicator_constraints.borrow()
2368 }
2369 pub fn num_indicator_constraints(&self) -> usize {
2370 self.indicator_constraints.borrow().len()
2371 }
2372 pub fn indicator_constraint_id(&self, name: &str) -> Option<IndicatorConstraintId> {
2373 self.indicator_names.borrow().get(name).copied()
2374 }
2375 pub fn has_indicator_constraints(&self) -> bool {
2376 !self.indicator_constraints.borrow().is_empty()
2377 }
2378 pub fn has_active_indicator_constraints(&self) -> bool {
2379 self.indicator_constraints.borrow().iter().any(|c| c.active)
2380 }
2381
2382 #[doc(hidden)]
2387 pub fn __minimize(&self, expr: Expr<'_>) {
2388 self.set_objective(expr, ObjectiveSense::Minimize);
2389 }
2390
2391 #[doc(hidden)]
2394 pub fn __maximize(&self, expr: Expr<'_>) {
2395 self.set_objective(expr, ObjectiveSense::Maximize);
2396 }
2397
2398 #[doc(hidden)]
2402 pub fn __feasibility(&self) {
2403 *self.objective.borrow_mut() = None;
2404 self.objective_declared.set(true);
2405 self.cached_kind.set(None);
2406 }
2407
2408 fn set_objective(&self, expr: Expr<'_>, sense: ObjectiveSense) {
2409 self.assert_expr_belongs(expr);
2410 *self.objective.borrow_mut() = Some(Objective { expr: expr.id, sense });
2411 self.objective_declared.set(true);
2412 self.cached_kind.set(None);
2413 }
2414
2415 pub fn is_feasibility(&self) -> bool {
2418 self.objective_declared.get() && self.objective.borrow().is_none()
2419 }
2420
2421 pub fn ensure_objective_declared(&self) -> Result<()> {
2429 if self.objective_declared.get() { Ok(()) } else { Err(Error::NoObjective) }
2430 }
2431
2432 pub fn objective(&self) -> Ref<'_, Option<Objective>> {
2433 self.objective.borrow()
2434 }
2435
2436 pub fn try_objective(&self) -> Result<Objective> {
2442 self.objective.borrow().clone().ok_or(Error::NoObjective)
2443 }
2444
2445 pub fn kind(&self) -> ModelKind {
2461 if let Some(k) = self.cached_kind.get() {
2462 return k;
2463 }
2464 let k = self.infer_kind_with(None);
2465 self.cached_kind.set(Some(k));
2466 k
2467 }
2468
2469 fn infer_kind_with(&self, parallel: Option<bool>) -> ModelKind {
2475 self.infer_kind_impl(parallel)
2476 }
2477
2478 #[cfg(any(test, feature = "benchmark-support"))]
2481 fn infer_kind(&self, parallel: bool) -> ModelKind {
2482 self.infer_kind_impl(Some(parallel))
2483 }
2484
2485 fn infer_kind_impl(&self, parallel: Option<bool>) -> ModelKind {
2486 let arena = self.arena.borrow();
2487 let vars = self.variables.borrow();
2488 let has_int = vars.iter().any(|v| v.domain.is_integer())
2489 || self.sos_constraints.borrow().iter().any(|constraint| constraint.active);
2490 let obj_class = self
2491 .objective
2492 .borrow()
2493 .as_ref()
2494 .map_or(ExprClass::Linear, |o| classify(&arena, o.expr));
2495
2496 let mut any_nonlinear = obj_class == ExprClass::Nonlinear;
2497 let mut plain_quad_con = false;
2498 let mut detected_soc = false;
2499 if !any_nonlinear {
2500 let constraints = self.constraints.borrow();
2501 let arena_ref = &*arena;
2502 let vars_ref = &*vars;
2503 let mut quadratic = Vec::new();
2504 for c in constraints.iter() {
2505 match classify(arena_ref, c.lhs) {
2506 ExprClass::Linear => {}
2507 ExprClass::Quadratic => quadratic.push(c),
2508 ExprClass::Nonlinear => {
2509 any_nonlinear = true;
2510 break;
2511 }
2512 }
2513 }
2514 if !any_nonlinear {
2515 let use_parallel = parallel.unwrap_or(
2516 quadratic.len() >= PAR_KIND_THRESHOLD && rayon::current_num_threads() > 1,
2517 );
2518 if use_parallel {
2519 let (has_cone, has_plain) = quadratic
2520 .par_iter()
2521 .map(|c| {
2522 let is_soc = is_detected_soc(arena_ref, vars_ref, c);
2523 (is_soc, !is_soc)
2524 })
2525 .reduce(
2526 || (false, false),
2527 |left, right| (left.0 || right.0, left.1 || right.1),
2528 );
2529 detected_soc = has_cone;
2530 plain_quad_con = has_plain;
2531 } else {
2532 for c in quadratic {
2533 if is_detected_soc(arena_ref, vars_ref, c) {
2534 detected_soc = true;
2535 } else {
2536 plain_quad_con = true;
2537 }
2538 }
2539 }
2540 }
2541 }
2542 let has_soc = detected_soc || !self.soc_constraints.borrow().is_empty();
2543
2544 let pick = |cont, int| if has_int { int } else { cont };
2545 if any_nonlinear {
2546 pick(ModelKind::NLP, ModelKind::MINLP)
2547 } else if plain_quad_con {
2548 pick(ModelKind::QCP, ModelKind::MIQCP)
2549 } else if has_soc {
2550 pick(ModelKind::SOCP, ModelKind::MISOCP)
2551 } else if obj_class == ExprClass::Quadratic {
2552 pick(ModelKind::QP, ModelKind::MIQP)
2553 } else {
2554 pick(ModelKind::LP, ModelKind::MILP)
2555 }
2556 }
2557}
2558
2559type BoundFn<'a> = Box<dyn Fn(&IndexKey) -> f64 + Send + Sync + 'a>;
2568
2569#[must_use = "IndexedVarBuilder does nothing until you call .build()"]
2570pub struct IndexedVarBuilder<'a, K = IndexKey> {
2571 model: &'a Model,
2572 base_name: String,
2573 keys: Vec<IndexKey>,
2574 axes: Option<Box<[Axis]>>,
2575 lb: f64,
2576 ub: f64,
2577 lb_by: Option<BoundFn<'a>>,
2578 ub_by: Option<BoundFn<'a>>,
2579 domain: Domain,
2580 parallel: Option<bool>,
2581 _k: PhantomData<fn() -> K>,
2582}
2583
2584impl<'a, K> std::fmt::Debug for IndexedVarBuilder<'a, K> {
2585 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2586 f.debug_struct("IndexedVarBuilder")
2587 .field("base_name", &self.base_name)
2588 .field("keys", &self.keys.len())
2589 .field("lb", &self.lb)
2590 .field("ub", &self.ub)
2591 .field("per_key_lb", &self.lb_by.is_some())
2592 .field("per_key_ub", &self.ub_by.is_some())
2593 .field("domain", &self.domain)
2594 .finish()
2595 }
2596}
2597
2598impl<'a, K> IndexedVarBuilder<'a, K> {
2599 pub fn lb(mut self, v: f64) -> Self {
2600 self.lb = v;
2601 self
2602 }
2603 pub fn ub(mut self, v: f64) -> Self {
2604 self.ub = v;
2605 self
2606 }
2607 pub fn bounds(mut self, lb: f64, ub: f64) -> Self {
2608 self.lb = lb;
2609 self.ub = ub;
2610 self
2611 }
2612 pub fn lb_by<F>(mut self, f: F) -> Self
2621 where
2622 K: FromIndexKey,
2623 F: Fn(K) -> f64 + 'a,
2624 F: Send + Sync,
2625 {
2626 self.lb_by = Some(Box::new(move |k: &IndexKey| f(K::from_index_key(k))));
2627 self
2628 }
2629 pub fn ub_by<F>(mut self, f: F) -> Self
2638 where
2639 K: FromIndexKey,
2640 F: Fn(K) -> f64 + 'a,
2641 F: Send + Sync,
2642 {
2643 self.ub_by = Some(Box::new(move |k: &IndexKey| f(K::from_index_key(k))));
2644 self
2645 }
2646 pub fn domain(mut self, d: Domain) -> Self {
2647 self.domain = d;
2648 self
2649 }
2650 pub fn integer(mut self) -> Self {
2651 self.domain = Domain::Integer;
2652 self
2653 }
2654 pub fn binary(mut self) -> Self {
2655 self.domain = Domain::Binary;
2656 self.lb = 0.0;
2657 self.ub = 1.0;
2658 self
2659 }
2660
2661 pub fn build(self) -> IndexedVar<'a, K> {
2666 let Self { model, base_name, keys, axes, lb, ub, lb_by, ub_by, domain, parallel, _k } =
2667 self;
2668
2669 if !indexed_parallel(keys.len(), parallel, PAR_INDEXED_METADATA_THRESHOLD) {
2670 let handles = keys
2671 .iter()
2672 .map(|key| {
2673 let scalar_name: SmolStr = format_index_name(&base_name, key).into();
2674 let lo = lb_by.as_ref().map_or(lb, |f| f(key));
2675 let hi = ub_by.as_ref().map_or(ub, |f| f(key));
2676 model.__var(scalar_name).lb(lo).ub(hi).domain(domain).build()
2677 })
2678 .collect();
2679 let storage = build_storage(keys, axes, handles);
2680 return IndexedFamily { storage, model_id: self.model.id(), _marker: PhantomData };
2681 }
2682
2683 let prepare = |key: &IndexKey| PendingVar {
2684 name: format_index_name(&base_name, key).into(),
2685 lb: lb_by.as_ref().map_or(lb, |f| f(key)),
2686 ub: ub_by.as_ref().map_or(ub, |f| f(key)),
2687 };
2688 let prepared: Vec<_> = keys.par_iter().map(prepare).collect();
2689 let handles = model.register_vars_batch(&prepared, domain);
2690 drop(prepared);
2691 let storage = build_storage(keys, axes, handles);
2692 IndexedFamily { storage, model_id: self.model.id(), _marker: PhantomData }
2693 }
2694
2695 #[cfg(any(test, feature = "benchmark-support"))]
2696 fn parallel_for_benchmark(mut self, parallel: bool) -> Self {
2697 self.parallel = Some(parallel);
2698 self
2699 }
2700}
2701
2702fn format_index_name(base: &str, key: &IndexKey) -> String {
2703 let mut out = String::with_capacity(base.len() + 4);
2704 out.push_str(base);
2705 out.push('[');
2706 write_key_parts(&mut out, key);
2707 out.push(']');
2708 out
2709}
2710
2711fn write_key_parts(out: &mut String, key: &IndexKey) {
2712 use std::fmt::Write;
2713 match key {
2714 IndexKey::Int(i) => write!(out, "{i}").unwrap(),
2715 IndexKey::Str(s) => out.push_str(s),
2716 IndexKey::Tuple(parts) => {
2717 for (i, p) in parts.iter().enumerate() {
2718 if i > 0 {
2719 out.push(',');
2720 }
2721 write_key_parts(out, p);
2722 }
2723 }
2724 }
2725}
2726
2727pub fn display_index_key(key: &IndexKey) -> String {
2730 let mut out = String::new();
2731 write_key_parts(&mut out, key);
2732 out
2733}
2734
2735#[cfg(feature = "benchmark-support")]
2736#[doc(hidden)]
2737#[expect(clippy::cast_precision_loss)]
2738#[allow(clippy::wildcard_imports)]
2739pub mod benchmark_support {
2740 use super::*;
2741
2742 pub const THRESHOLD: usize = PAR_KIND_THRESHOLD;
2743
2744 pub fn model(rows: usize, degree: usize) -> Model {
2745 let model = Model::new("kind_bench");
2746 let x = model.__var("x").build();
2747 let y = model.__var("y").build();
2748 let z = model.__var("z").build();
2749 model.__minimize(x + y + z);
2750 for i in 0..rows {
2751 let lhs = match degree {
2752 1 => x + 2.0 * y - z,
2753 2 => x.powi(2) + y,
2754 _ => x * y * z,
2755 };
2756 model.__add_constraint_auto(lhs.le(i as f64 + 10.0));
2757 }
2758 model
2759 }
2760
2761 pub fn soc_model(rows: usize) -> Model {
2762 let model = Model::new("kind_soc_bench");
2763 let x = model.__var("x").build();
2764 let y = model.__var("y").build();
2765 let t = model.__var("t").lb(0.0).build();
2766 model.__minimize(t);
2767 for _ in 0..rows {
2768 model.__add_constraint_auto((x.powi(2) + y.powi(2) - t.powi(2)).le(0.0));
2769 }
2770 model
2771 }
2772
2773 pub fn infer(model: &Model, parallel: bool) -> ModelKind {
2774 model.infer_kind(parallel)
2775 }
2776
2777 #[derive(Copy, Clone, Debug)]
2778 pub enum IndexedBuildCase {
2779 Variables,
2780 Parameters,
2781 Algebraic,
2782 Range,
2783 Soc,
2784 Sos,
2785 }
2786
2787 pub fn indexed_build(rows: usize, case: IndexedBuildCase, parallel: bool) -> usize {
2790 let model = Model::new("indexed_build_bench");
2791 let keys = Set::range(0..rows);
2792 match case {
2793 IndexedBuildCase::Variables => {
2794 let x = model
2795 .__indexed_var("x", &keys)
2796 .lb_by(|i: usize| -(i as f64))
2797 .ub_by(|i: usize| i as f64 + 1.0)
2798 .parallel_for_benchmark(parallel)
2799 .build();
2800 std::hint::black_box(x.len());
2801 }
2802 IndexedBuildCase::Parameters => {
2803 let value = |i: usize| i as f64 + 1.0;
2804 let p = model.indexed_param_with("p".to_owned(), &keys, &value, Some(parallel));
2805 std::hint::black_box(p.len());
2806 }
2807 IndexedBuildCase::Algebraic => {
2808 let x = model.__indexed_var("x", &keys).parallel_for_benchmark(parallel).build();
2809 let rule = |i: usize| (2.0 * x[i] + 1.0).le(i as f64 + 10.0);
2810 model.add_constraints_over_with("c", &keys, &rule, Some(parallel));
2811 }
2812 IndexedBuildCase::Range => {
2813 let x = model.__indexed_var("x", &keys).parallel_for_benchmark(parallel).build();
2814 let rule = |i: usize| (x[i] + 1.0, -(i as f64), i as f64 + 10.0);
2815 model.add_range_constraints_over_with("r", &keys, &rule, Some(parallel));
2816 }
2817 IndexedBuildCase::Soc => {
2818 let x = model.__indexed_var("x", &keys).parallel_for_benchmark(parallel).build();
2819 let y = model.__indexed_var("y", &keys).parallel_for_benchmark(parallel).build();
2820 let t = model.__var("t").lb(0.0).build();
2821 let rule = |i: usize| ([x[i] + 1.0, y[i] - 1.0], t + i as f64 + 1.0);
2822 model.add_soc_constraints_over_with("q", &keys, &rule, Some(parallel));
2823 }
2824 IndexedBuildCase::Sos => {
2825 let x = model.__indexed_var("x", &keys).parallel_for_benchmark(parallel).build();
2826 let y = model.__indexed_var("y", &keys).parallel_for_benchmark(parallel).build();
2827 let rule = |i: usize| [(x[i], 1.0), (y[i], 2.0)];
2828 model.add_sos_constraints_over_with(
2829 "s",
2830 &keys,
2831 SosType::Sos1,
2832 &rule,
2833 Some(parallel),
2834 );
2835 }
2836 }
2837 model.num_variables()
2838 + model.num_parameters()
2839 + model.num_constraints()
2840 + model.arena().len()
2841 }
2842
2843 pub fn scalar_build(rows: usize) -> usize {
2845 let model = Model::new("scalar_build_bench");
2846 for i in 0..rows {
2847 let x = model.__var(format!("x{i}")).build();
2848 model.__add_constraint(format!("c{i}"), (2.0 * x + 1.0).le(i as f64 + 10.0));
2849 }
2850 model.num_variables() + model.num_constraints() + model.arena().len()
2851 }
2852}
2853
2854#[cfg(test)]
2855#[expect(clippy::cast_precision_loss)]
2856mod tests {
2857 use std::panic::{AssertUnwindSafe, catch_unwind};
2858
2859 use oximo_expr::extract_linear;
2860
2861 use super::*;
2862 use crate::Set;
2863 use crate::constraint::Relate;
2864
2865 #[test]
2866 fn model_kind_display_uses_standard_ascii_acronyms() {
2867 let kinds = [
2868 ModelKind::LP,
2869 ModelKind::MILP,
2870 ModelKind::QP,
2871 ModelKind::MIQP,
2872 ModelKind::QCP,
2873 ModelKind::MIQCP,
2874 ModelKind::SOCP,
2875 ModelKind::MISOCP,
2876 ModelKind::NLP,
2877 ModelKind::MINLP,
2878 ];
2879 let labels: Vec<_> = kinds.into_iter().map(|kind| kind.to_string()).collect();
2880 assert_eq!(
2881 labels,
2882 ["LP", "MILP", "QP", "MIQP", "QCP", "MIQCP", "SOCP", "MISOCP", "NLP", "MINLP"]
2883 );
2884 assert!(labels.iter().all(|label| label.is_ascii()));
2885 }
2886
2887 #[test]
2888 fn param_times_var_keeps_model_linear() {
2889 let m = Model::new("p");
2890 let param = m.__param("param", 4.0);
2891 let x = m.__var("x").lb(0.0).build();
2892 m.__minimize(param * x);
2893 assert_eq!(m.kind(), ModelKind::LP);
2894 }
2895
2896 #[test]
2897 fn param_coeff_resolves_and_rebinds() {
2898 let m = Model::new("p");
2899 let param = m.__param("param", 4.0);
2900 let x = m.__var("x").lb(0.0).build();
2901 let obj = param * x;
2902
2903 let coeff = |m: &Model| {
2904 let arena = m.arena();
2905 extract_linear(&arena, obj.id).expect("linear").coeffs[0].1
2906 };
2907 assert!((coeff(&m) - 4.0).abs() < f64::EPSILON);
2908
2909 m.set_param(param, 9.0).unwrap();
2910 assert!((coeff(&m) - 9.0).abs() < f64::EPSILON);
2911 assert_eq!(m.parameter_id("param"), Some(param.param_id().unwrap()));
2912 }
2913
2914 #[test]
2915 fn param_value_reads_live_arena_value() {
2916 let m = Model::new("p");
2917 let param = m.__param("param", 4.0);
2918 let id = param.param_id().unwrap();
2919 assert!((m.param_value(id) - 4.0).abs() < f64::EPSILON);
2920 assert!((m.param_value_of(param).unwrap().unwrap() - 4.0).abs() < f64::EPSILON);
2921
2922 m.set_param(param, 7.5).unwrap();
2923 assert!((m.param_value(id) - 7.5).abs() < f64::EPSILON);
2924
2925 let x = m.__var("x").build();
2926 assert!(m.param_value_of(x).unwrap().is_none());
2927 }
2928
2929 #[test]
2930 fn handle_mutations_reject_foreign_models_without_changing_colliding_slots() {
2931 let source = Model::new("source");
2932 let target = Model::new("target");
2933 let source_x = source.__var("x").build();
2934 let target_x = target.__var("x").lb(-1.0).ub(1.0).build();
2935 let source_p = source.__param("p", 7.0);
2936 let target_p = target.__param("p", 3.0);
2937
2938 let expected = ModelMismatchError::new(target.id(), source.id());
2939 assert_eq!(target.fix(source_x, 0.5), Err(expected));
2940 assert_eq!(target.set_initial(source_x, 0.5), Err(expected));
2941 assert_eq!(target.set_param(source_p, 9.0), Err(expected));
2942
2943 let variable = &target.variables()[target_x.var_id().unwrap().index()];
2944 assert_eq!((variable.lb, variable.ub, variable.initial), (-1.0, 1.0, None));
2945 assert_eq!(target.param_value_of(target_p).unwrap(), Some(3.0));
2946 assert_ne!(source.id(), target.id());
2947 assert_eq!(source_x.model_id(), source.id());
2948 }
2949
2950 #[test]
2951 fn arena_snapshot_is_not_live_after_parameter_rebind() {
2952 let m = Model::new("snapshot");
2953 let param = m.__param("p", 1.0);
2954 let id = param.param_id().unwrap();
2955 let snapshot = m.arena();
2956 m.set_param(param, 2.0).unwrap();
2957 assert!((snapshot.param_value(id) - 1.0).abs() < f64::EPSILON);
2958 assert!((m.param_value(id) - 2.0).abs() < f64::EPSILON);
2959 }
2960
2961 #[test]
2962 #[should_panic(expected = "different model")]
2963 fn indexed_constraints_reject_foreign_expression_arenas() {
2964 let target = Model::new("target");
2965 let foreign = Model::new("foreign");
2966 let x = foreign.__var("x").build();
2967 let keys = Set::range(0..1024);
2968 target.add_constraints_over_with("c", &keys, &|_| x.le(1.0), Some(true));
2969 }
2970
2971 #[test]
2972 fn set_param_invalidates_kind_cache() {
2973 let m = Model::new("p");
2974 let p = m.__param("p", 1.0);
2975 let x = m.__var("x").lb(0.0).build();
2976 m.__add_constraint("c", (p * x).le(10.0));
2977 assert_eq!(m.kind(), ModelKind::LP);
2978 m.set_param(p, 2.0).unwrap();
2979 assert_eq!(m.kind(), ModelKind::LP);
2980 }
2981
2982 #[test]
2983 fn unified_constraints_include_inactive_entries() {
2984 let m = Model::new("inactive");
2985 let x = m.__var("x").build();
2986 let t = m.__var("t").lb(0.0).build();
2987 m.__add_constraint("row", x.le(1.0));
2988 m.add_soc_constraint("cone", [x], t);
2989 m.add_sos_constraint("sos", SosType::Sos1, [(x, 1.0)]);
2990 m.constraints.borrow_mut()[0].active = false;
2991 m.soc_constraints.borrow_mut()[0].active = false;
2992 m.sos_constraints.borrow_mut()[0].active = false;
2993
2994 let constraints = m.constraints();
2995 assert_eq!(constraints.len(), 3);
2996 assert!(constraints.iter().all(|entry| match entry {
2997 ConstraintRef::Algebraic { constraint, .. } => !constraint.active,
2998 ConstraintRef::SecondOrderCone { constraint, .. } => !constraint.active,
2999 ConstraintRef::SpecialOrderedSet { constraint, .. } => !constraint.active,
3000 ConstraintRef::Indicator { constraint, .. } => !constraint.active,
3001 }));
3002 }
3003
3004 #[test]
3005 fn uncached_kind_inference_leaves_kind_cache_empty() {
3006 let m = Model::new("uncached_kind");
3007 let x = m.__var("x").build();
3008 m.__minimize(x);
3009 for _ in 0..PAR_KIND_THRESHOLD {
3010 m.__add_constraint_auto(x.powi(2).le(1.0));
3011 }
3012
3013 assert_eq!(m.infer_kind(false), ModelKind::QCP);
3014 assert_eq!(m.cached_kind.get(), None);
3015 assert_eq!(m.infer_kind(true), ModelKind::QCP);
3016 assert_eq!(m.cached_kind.get(), None);
3017
3018 assert_eq!(m.kind(), ModelKind::QCP);
3019 assert_eq!(m.cached_kind.get(), Some(ModelKind::QCP));
3020 }
3021
3022 #[test]
3023 #[should_panic(expected = "parameter name \"dup\" is already registered")]
3024 fn duplicate_param_name_panics() {
3025 let m = Model::new("p");
3026 let _a = m.__param("dup", 1.0);
3027 let _b = m.__param("dup", 2.0);
3028 }
3029
3030 #[test]
3031 fn indexed_param_dense_value_and_per_key_rebind() {
3032 let m = Model::new("ip");
3033 let items = Set::range(0..3);
3034 let data = [10.0, 20.0, 30.0];
3035 let cost = m.__indexed_param("cost", &items, |i: usize| data[i]);
3036
3037 assert!(cost.is_dense());
3038 assert_eq!(cost.len(), 3);
3039 assert_eq!(m.num_parameters(), 3);
3040 assert!(m.parameter_id("cost[0]").is_some());
3041 assert!(m.parameter_id("cost[2]").is_some());
3042 assert!((m.param_value_idx(&cost, 1usize).unwrap().unwrap() - 20.0).abs() < f64::EPSILON);
3043
3044 let x = m.__var("x").lb(0.0).build();
3045 let obj = cost.at([1]) * x;
3046 let coeff = |m: &Model| {
3047 let arena = m.arena();
3048 extract_linear(&arena, obj.id).expect("linear").coeffs[0].1
3049 };
3050 assert!((coeff(&m) - 20.0).abs() < f64::EPSILON);
3051
3052 m.set_param_idx(&cost, 1usize, 99.0).unwrap();
3053 assert!((coeff(&m) - 99.0).abs() < f64::EPSILON);
3054 assert!((m.param_value_idx(&cost, 1usize).unwrap().unwrap() - 99.0).abs() < f64::EPSILON);
3055 assert!((m.param_value_idx(&cost, 0usize).unwrap().unwrap() - 10.0).abs() < f64::EPSILON);
3056 }
3057
3058 #[test]
3059 fn set_param_idx_rejects_foreign_family() {
3060 let a = Model::new("a");
3061 let b = Model::new("b");
3062 let items = Set::range(0..2);
3063 let pa = a.__indexed_param("p", &items, |_i: usize| 1.0);
3064 let pb = b.__indexed_param("p", &items, |_i: usize| 2.0);
3065 assert_eq!(b.set_param_idx(&pa, 0usize, 5.0), Err(ModelMismatchError::new(b.id(), a.id())));
3066 assert_eq!(b.param_value_idx(&pb, 0usize).unwrap(), Some(2.0));
3067 }
3068
3069 #[test]
3070 fn indexed_param_sparse_string_keyed() {
3071 let m = Model::new("ips");
3072 let plants = Set::strings(["a", "b"]);
3073 let price =
3074 m.__indexed_param("price", &plants, |p: String| if p == "a" { 1.5 } else { 2.5 });
3075 assert!(!price.is_dense());
3076 assert_eq!(price.len(), 2);
3077 assert!((m.param_value_idx(&price, "a").unwrap().unwrap() - 1.5).abs() < f64::EPSILON);
3078 assert!((m.param_value_idx(&price, "b").unwrap().unwrap() - 2.5).abs() < f64::EPSILON);
3079 assert!(m.param_value_idx(&price, "z").unwrap().is_none());
3080 }
3081
3082 #[test]
3083 fn kind_forced_serial_and_parallel_classification_agree() {
3084 let qcp = Model::new("qcp");
3085 let x = qcp.__var("x").build();
3086 let y = qcp.__var("y").build();
3087 qcp.__minimize(x + y);
3088 for i in 0..PAR_KIND_THRESHOLD + 3 {
3089 qcp.__add_constraint_auto((x.powi(2) + y).le(i as f64 + 1.0));
3090 }
3091 assert_eq!(qcp.infer_kind(false), ModelKind::QCP);
3092 assert_eq!(qcp.infer_kind(false), qcp.infer_kind(true));
3093 assert_eq!(qcp.infer_kind(false), qcp.infer_kind_with(None));
3094
3095 let socp = Model::new("socp");
3096 let x = socp.__var("x").build();
3097 let t = socp.__var("t").lb(0.0).build();
3098 socp.__minimize(t);
3099 for _ in 0..PAR_KIND_THRESHOLD + 3 {
3100 socp.__add_constraint_auto((x.powi(2) - t.powi(2)).le(0.0));
3101 }
3102 assert_eq!(socp.infer_kind(false), ModelKind::SOCP);
3103 assert_eq!(socp.infer_kind(false), socp.infer_kind(true));
3104 assert_eq!(socp.infer_kind(false), socp.infer_kind_with(None));
3105
3106 let nlp = Model::new("nlp");
3107 let x = nlp.__var("x").build();
3108 let y = nlp.__var("y").build();
3109 let z = nlp.__var("z").build();
3110 nlp.__minimize(x + y + z);
3111 for _ in 0..PAR_KIND_THRESHOLD + 3 {
3112 nlp.__add_constraint_auto((x * y * z).le(1.0));
3113 }
3114 assert_eq!(nlp.infer_kind(false), ModelKind::NLP);
3115 assert_eq!(nlp.infer_kind(false), nlp.infer_kind(true));
3116 }
3117
3118 #[test]
3119 fn arena_snapshot_allows_nested_model_reads() {
3120 let model = Model::new("nested_reads");
3121 let x = model.__var("x").build();
3122 model.__minimize(x);
3123
3124 let arena = model.arena();
3125 assert_eq!(model.kind(), ModelKind::LP);
3126 assert!(matches!(arena.get(x.id), oximo_expr::ExprNode::Var(_)));
3127 }
3128
3129 #[test]
3130 fn indexed_constraint_handles_match_in_serial_and_parallel() {
3131 #[derive(Debug, PartialEq, Eq)]
3132 struct Handles {
3133 ordinary: Vec<(usize, ConstraintId)>,
3134 ranges: Vec<(usize, RangeConstraintIds)>,
3135 }
3136
3137 fn build(parallel: bool, keys: &Set<usize>) -> Handles {
3138 let model = Model::new("family_id_parity");
3139 let x = model.__var("x").build();
3140 model.__add_constraint("prior", x.ge(0.0));
3141 let ordinary =
3142 model.add_constraints_over_with("c", keys, &|_| x.le(5.0), Some(parallel));
3143 let ranges = model.add_range_constraints_over_with(
3144 "r",
3145 keys,
3146 &|i| (if i % 2 == 0 { x + 1.0 } else { x.powi(2) }, 0.0, 10.0),
3147 Some(parallel),
3148 );
3149 for (key, id) in ordinary.iter() {
3150 assert_eq!(ordinary.get(key), Some(id));
3151 assert_eq!(model.constraint_handle(&format!("c[{key}]")), Some(id));
3152 }
3153 for (key, ids) in ranges.iter() {
3154 assert_eq!(ranges.get(key), Some(ids));
3155 match ids {
3156 RangeConstraintHandles::Interval(id) => {
3157 assert_eq!(model.constraint_handle(&format!("r[{key}]")), Some(id));
3158 let row = &model.constraints.borrow()[id.index()];
3159 assert_eq!((row.lower, row.upper), (0.0, 10.0));
3160 assert_eq!(classify(&model.arena(), row.lhs), ExprClass::Linear);
3161 }
3162 RangeConstraintHandles::Split { lower, upper } => {
3163 assert_eq!(model.constraint_handle(&format!("r[{key}]_lo")), Some(lower));
3164 assert_eq!(model.constraint_handle(&format!("r[{key}]_hi")), Some(upper));
3165 let rows = model.constraints.borrow();
3166 assert_eq!(rows[lower.index()].lower.to_bits(), 0.0_f64.to_bits());
3167 assert_eq!(rows[upper.index()].upper.to_bits(), 10.0_f64.to_bits());
3168 assert_eq!(
3169 classify(&model.arena(), rows[lower.index()].lhs),
3170 ExprClass::Quadratic
3171 );
3172 }
3173 }
3174 }
3175 Handles {
3176 ordinary: ordinary.iter().map(|(key, handle)| (key, handle.id())).collect(),
3177 ranges: ranges.iter().map(|(key, handles)| (key, handles.ids())).collect(),
3178 }
3179 }
3180
3181 rayon::ThreadPoolBuilder::new().num_threads(4).build().unwrap().install(|| {
3182 for keys in [Set::range(3..1030), Set::from_ints([8, 1, 6, 3, 4])] {
3183 assert_eq!(build(false, &keys), build(true, &keys));
3184 }
3185 });
3186 }
3187
3188 #[test]
3189 fn indexed_build_forced_parallel_matches_serial_for_every_family() {
3190 #[derive(Debug, PartialEq)]
3191 struct Digest {
3192 variables: Vec<String>,
3193 parameters: Vec<(String, f64)>,
3194 rows: Vec<String>,
3195 typed: Vec<String>,
3196 arena_len: usize,
3197 }
3198
3199 fn digest(parallel: bool) -> Digest {
3200 let model = Model::new("indexed_parity");
3201 let keys = Set::range(0..128usize);
3202 let x = model
3203 .__indexed_var("x", &keys)
3204 .lb_by(|i: usize| -(i as f64))
3205 .ub_by(|i: usize| i as f64 + 10.0)
3206 .parallel_for_benchmark(parallel)
3207 .build();
3208 let y = model.__indexed_var("y", &keys).parallel_for_benchmark(parallel).build();
3209 let values = |i: usize| i as f64 + 0.5;
3210 let p = model.indexed_param_with("p".to_owned(), &keys, &values, Some(parallel));
3211
3212 let algebraic = |i: usize| (2.0 * x[i] + p[i]).le(i as f64 + 20.0);
3213 model.add_constraints_over_with("c", &keys, &algebraic, Some(parallel));
3214 let ranges = |i: usize| (y[i] + 1.0, -(i as f64), i as f64 + 5.0);
3215 model.add_range_constraints_over_with("r", &keys, &ranges, Some(parallel));
3216 let symbolic_ranges = |i: usize| (x[i] + 2.0, -p[i], p[i] + 3.0);
3217 model.add_range_constraints_over_with("sr", &keys, &symbolic_ranges, Some(parallel));
3218 let t = model.__var("t").lb(0.0).build();
3219 let cones = |i: usize| ([x[i] + 1.0, y[i] - 1.0], t + p[i]);
3220 model.add_soc_constraints_over_with("q", &keys, &cones, Some(parallel));
3221 let explicit_sos = |i: usize| [(x[i], 1.0), (y[i], 2.0)];
3222 model.add_sos_constraints_over_with(
3223 "s",
3224 &keys,
3225 SosType::Sos1,
3226 &explicit_sos,
3227 Some(parallel),
3228 );
3229 let auto_sos = |i: usize| [x[i], y[i]];
3230 model.add_sos_constraints_over_auto_weights_with(
3231 "a",
3232 &keys,
3233 SosType::Sos2,
3234 &auto_sos,
3235 Some(parallel),
3236 );
3237
3238 let variables = model
3239 .variables()
3240 .iter()
3241 .map(|variable| {
3242 format!(
3243 "{}:{:?}:{}:{}:{}",
3244 variable.name, variable.id, variable.lb, variable.ub, variable.domain
3245 )
3246 })
3247 .collect();
3248 let parameters = model
3249 .parameters()
3250 .iter()
3251 .map(|parameter| (parameter.name.to_string(), model.param_value(parameter.id)))
3252 .collect();
3253 let arena = model.arena();
3254 let constraints = model.constraints();
3255 let rows = constraints
3256 .algebraic()
3257 .iter()
3258 .map(|constraint| {
3259 let terms = extract_linear(&arena, constraint.lhs).unwrap();
3260 format!(
3261 "{}:{:?}:{}:{}:{}",
3262 constraint.name,
3263 terms.coeffs,
3264 terms.constant,
3265 constraint.lower,
3266 constraint.upper
3267 )
3268 })
3269 .collect();
3270 let mut typed = constraints
3271 .second_order_cones()
3272 .iter()
3273 .map(|constraint| {
3274 let terms: Vec<_> = constraint
3275 .terms
3276 .iter()
3277 .map(|&term| extract_linear(&arena, term).unwrap().into_owned())
3278 .collect();
3279 let bound = extract_linear(&arena, constraint.bound).unwrap().into_owned();
3280 format!("{}:{terms:?}:{bound:?}", constraint.name)
3281 })
3282 .collect::<Vec<_>>();
3283 typed.extend(
3284 constraints
3285 .special_ordered_sets()
3286 .iter()
3287 .map(|constraint| format!("{constraint:?}")),
3288 );
3289 let arena_len = arena.len();
3290 Digest { variables, parameters, rows, typed, arena_len }
3291 }
3292
3293 assert_eq!(digest(false), digest(true));
3294 }
3295
3296 #[test]
3297 fn parallel_duplicate_name_failure_does_not_mutate_model_or_arena() {
3298 let model = Model::new("indexed_atomicity");
3299 let x = model.__var("x").build();
3300 let duplicate_keys = Set::from_ints([0usize, 0]);
3301 let arena_len = model.arena().len();
3302 let rule = |i: usize| (x + i as f64).le(1.0);
3303 let result = catch_unwind(AssertUnwindSafe(|| {
3304 model.add_constraints_over_with("dup", &duplicate_keys, &rule, Some(true));
3305 }));
3306 assert!(result.is_err());
3307 assert_eq!(model.num_constraints(), 0);
3308 assert_eq!(model.arena().len(), arena_len);
3309
3310 model.__add_constraint("after", x.le(2.0));
3311 assert_eq!(model.constraint_id("after"), Some(ConstraintId(0)));
3312 }
3313
3314 #[test]
3315 fn parallel_range_failure_does_not_mutate_model_or_arena() {
3316 for duplicate_name in [false, true] {
3317 let model = Model::new("range_batch_atomicity");
3318 let x = model.__var("x").build();
3319 let prior = model.__add_constraint("r[17]_hi", x.le(2.0));
3320 let arena_len = model.arena().len();
3321 let keys = Set::range(0..128usize);
3322 let rule = |i: usize| {
3323 let body = if i.is_multiple_of(2) { x + 1.0 } else { x.powi(2) };
3324 assert!(duplicate_name || i != 17, "deliberate range callback panic");
3325 (body, 0.0, 10.0)
3326 };
3327 let result = catch_unwind(AssertUnwindSafe(|| {
3328 model.add_range_constraints_over_with("r", &keys, &rule, Some(true));
3329 }));
3330 assert!(result.is_err());
3331 assert_eq!(model.num_constraints(), 1);
3332 assert_eq!(model.constraint_handle("r[17]_hi"), Some(prior));
3333 assert_eq!(model.arena().len(), arena_len);
3334
3335 let family = model.add_range_constraints_over_with(
3337 "after",
3338 &keys,
3339 &|_| (x, 0.0, 1.0),
3340 Some(true),
3341 );
3342 assert_eq!(family.get(0).unwrap().ids(), RangeConstraintIds::Interval(ConstraintId(1)));
3343 assert_eq!(model.num_constraints(), 129);
3344 }
3345 }
3346
3347 #[test]
3348 fn parallel_callback_panic_does_not_mutate_model_or_arena() {
3349 let model = Model::new("indexed_callback_atomicity");
3350 let x = model.__var("x").build();
3351 let keys = Set::range(0..128usize);
3352 let arena_len = model.arena().len();
3353 let rule = |i: usize| {
3354 let constraint = (x + i as f64).le(1.0);
3355 assert_ne!(i, 17, "deliberate callback panic");
3356 constraint
3357 };
3358 let result = catch_unwind(AssertUnwindSafe(|| {
3359 model.add_constraints_over_with("panic", &keys, &rule, Some(true));
3360 }));
3361 assert!(result.is_err());
3362 assert_eq!(model.num_constraints(), 0);
3363 assert_eq!(model.arena().len(), arena_len);
3364
3365 model.__add_constraint("after", x.le(2.0));
3366 assert_eq!(model.constraint_id("after"), Some(ConstraintId(0)));
3367 }
3368}