1use std::cell::{Cell, Ref, RefCell};
2use std::marker::PhantomData;
3
4use oximo_expr::{EvalError, Expr, ExprArena, ExprClass, ExprId, ParamId, VarId, classify};
5use rayon::prelude::*;
6use rustc_hash::FxHashMap;
7use smol_str::SmolStr;
8
9use crate::constraint::{Constraint, ConstraintExpr, ConstraintId, IntoRhs, Relate, Sense};
10use crate::domain::Domain;
11use crate::error::{Error, Result};
12use crate::indexed::{IndexedFamily, IndexedParam, IndexedVar, build_storage};
13use crate::objective::{Objective, ObjectiveSense};
14use crate::param::Parameter;
15use crate::set::{Axis, FromIndexKey, IndexKey, Set};
16use crate::soc::{SocConstraint, SocConstraintId, is_detected_soc};
17use crate::var::{VarBuilder, Variable};
18
19const PAR_KIND_THRESHOLD: usize = 256;
20
21#[derive(Copy, Clone, Debug, PartialEq, Eq)]
38pub enum ModelKind {
39 LP,
40 MILP,
41 QP,
42 MIQP,
43 QCP,
44 MIQCP,
45 SOCP,
46 MISOCP,
47 NLP,
48 MINLP,
49}
50
51#[derive(Copy, Clone, Debug)]
57pub enum ConstraintRef<'a> {
58 Algebraic { id: ConstraintId, constraint: &'a Constraint },
59 SecondOrderCone { id: SocConstraintId, constraint: &'a SocConstraint },
60}
61
62#[derive(Debug)]
69pub struct ModelConstraints<'a> {
70 algebraic: Ref<'a, Vec<Constraint>>,
71 second_order_cones: Ref<'a, Vec<SocConstraint>>,
72}
73
74impl ModelConstraints<'_> {
75 pub fn algebraic(&self) -> &[Constraint] {
77 &self.algebraic
78 }
79
80 pub fn second_order_cones(&self) -> &[SocConstraint] {
82 &self.second_order_cones
83 }
84
85 #[expect(
87 clippy::cast_possible_truncation,
88 reason = "registration rejects constraint counts above u32::MAX"
89 )]
90 pub fn iter(&self) -> impl DoubleEndedIterator<Item = ConstraintRef<'_>> + Clone {
91 let algebraic = self.algebraic.iter().enumerate().map(|(index, constraint)| {
92 ConstraintRef::Algebraic { id: ConstraintId(index as u32), constraint }
93 });
94 let second_order_cones =
95 self.second_order_cones.iter().enumerate().map(|(index, constraint)| {
96 ConstraintRef::SecondOrderCone { id: SocConstraintId(index as u32), constraint }
97 });
98 algebraic.chain(second_order_cones)
99 }
100
101 pub fn len(&self) -> usize {
103 self.algebraic.len() + self.second_order_cones.len()
104 }
105
106 pub fn is_empty(&self) -> bool {
107 self.algebraic.is_empty() && self.second_order_cones.is_empty()
108 }
109}
110
111pub struct Model {
120 pub name: SmolStr,
121 pub(crate) arena: RefCell<ExprArena>,
122 pub(crate) variables: RefCell<Vec<Variable>>,
123 pub(crate) var_names: RefCell<FxHashMap<SmolStr, VarId>>,
124 pub(crate) parameters: RefCell<Vec<Parameter>>,
125 pub(crate) param_names: RefCell<FxHashMap<SmolStr, ParamId>>,
126 pub(crate) constraints: RefCell<Vec<Constraint>>,
127 pub(crate) constraint_names: RefCell<FxHashMap<SmolStr, ConstraintId>>,
128 pub(crate) soc_constraints: RefCell<Vec<SocConstraint>>,
129 pub(crate) soc_names: RefCell<FxHashMap<SmolStr, SocConstraintId>>,
130 pub(crate) objective: RefCell<Option<Objective>>,
131 objective_declared: Cell<bool>,
132 cached_kind: Cell<Option<ModelKind>>,
133 auto_seq: Cell<u32>,
136}
137
138impl std::fmt::Debug for Model {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("Model")
141 .field("name", &self.name)
142 .field("vars", &self.variables.borrow().len())
143 .field("params", &self.parameters.borrow().len())
144 .field("constraints", &self.constraints.borrow().len())
145 .field("soc_constraints", &self.soc_constraints.borrow().len())
146 .field("has_objective", &self.objective.borrow().is_some())
147 .field("feasibility", &self.is_feasibility())
148 .finish()
149 }
150}
151
152impl Model {
153 pub fn new(name: impl Into<SmolStr>) -> Self {
154 Self {
155 name: name.into(),
156 arena: RefCell::new(ExprArena::new()),
157 variables: RefCell::new(Vec::new()),
158 var_names: RefCell::new(FxHashMap::default()),
159 parameters: RefCell::new(Vec::new()),
160 param_names: RefCell::new(FxHashMap::default()),
161 constraints: RefCell::new(Vec::new()),
162 constraint_names: RefCell::new(FxHashMap::default()),
163 soc_constraints: RefCell::new(Vec::new()),
164 soc_names: RefCell::new(FxHashMap::default()),
165 objective: RefCell::new(None),
166 objective_declared: Cell::new(false),
167 cached_kind: Cell::new(None),
168 auto_seq: Cell::new(0),
169 }
170 }
171
172 #[doc(hidden)]
177 pub fn __var(&self, name: impl Into<SmolStr>) -> VarBuilder<'_> {
178 VarBuilder {
179 model: self,
180 name: name.into(),
181 lb: f64::NEG_INFINITY,
182 ub: f64::INFINITY,
183 domain: Domain::Real,
184 initial: None,
185 }
186 }
187
188 #[doc(hidden)]
190 pub fn __constant(&self, value: f64) -> Expr<'_> {
191 Expr::constant(&self.arena, value)
192 }
193
194 pub(crate) fn register_var<'a>(&'a self, b: VarBuilder<'a>) -> Expr<'a> {
197 let mut names = self.var_names.borrow_mut();
198 assert!(
199 !names.contains_key(&b.name),
200 "variable name {:?} is already registered on this model",
201 b.name
202 );
203 let mut vars = self.variables.borrow_mut();
204 let id = VarId(u32::try_from(vars.len()).expect("variable count overflow"));
205 vars.push(Variable {
206 id,
207 name: b.name.clone(),
208 domain: b.domain,
209 lb: b.lb,
210 ub: b.ub,
211 initial: b.initial,
212 });
213 names.insert(b.name, id);
214 drop(vars);
215 drop(names);
216 self.cached_kind.set(None);
217 Expr::from_var(&self.arena, id)
218 }
219
220 #[doc(hidden)]
223 pub fn __indexed_var<'a, K>(
224 &'a self,
225 name: impl Into<String>,
226 set: &Set<K>,
227 ) -> IndexedVarBuilder<'a, K> {
228 IndexedVarBuilder {
229 model: self,
230 base_name: name.into(),
231 keys: set.iter().collect(),
232 axes: set.axes().map(Box::from),
233 lb: f64::NEG_INFINITY,
234 ub: f64::INFINITY,
235 lb_by: None,
236 ub_by: None,
237 domain: Domain::Real,
238 _k: PhantomData,
239 }
240 }
241
242 pub fn variable_id(&self, name: &str) -> Option<VarId> {
243 self.var_names.borrow().get(name).copied()
244 }
245
246 pub fn variables(&self) -> Ref<'_, Vec<Variable>> {
247 self.variables.borrow()
248 }
249
250 pub fn arena(&self) -> Ref<'_, ExprArena> {
251 self.arena.borrow()
252 }
253
254 pub fn num_variables(&self) -> usize {
255 self.variables.borrow().len()
256 }
257
258 #[must_use]
262 pub fn describe_eval_error(&self, err: &EvalError) -> String {
263 match err {
264 EvalError::UnboundVar(v) => {
265 let name = crate::var::var_name(&self.variables.borrow(), *v);
266 format!("variable {name} has no value bound in the evaluation context")
267 }
268 EvalError::UnboundParam(p) => {
269 let name = self.parameters.borrow().iter().find(|par| par.id == *p).map_or_else(
270 || format!("parameter #{}", p.index()),
271 |par| par.name.to_string(),
272 );
273 format!("parameter {name} has no value bound in the evaluation context")
274 }
275 }
276 }
277
278 pub fn fix(&self, e: Expr<'_>, value: f64) {
286 let id = e.var_id().expect("Model::fix expects a single-variable expression");
287 self.fix_var(id, value);
288 }
289
290 pub fn fix_var(&self, id: VarId, value: f64) {
292 let mut vars = self.variables.borrow_mut();
293 let v = &mut vars[id.index()];
294 v.lb = value;
295 v.ub = value;
296 drop(vars);
297 self.cached_kind.set(None);
298 }
299
300 pub fn set_initial(&self, e: Expr<'_>, value: f64) {
308 let id = e.var_id().expect("Model::set_initial expects a single-variable expression");
309 self.variables.borrow_mut()[id.index()].initial = Some(value);
310 }
311
312 pub fn unfix_var(&self, id: VarId, lb: f64, ub: f64) {
315 let mut vars = self.variables.borrow_mut();
316 let v = &mut vars[id.index()];
317 v.lb = lb;
318 v.ub = ub;
319 drop(vars);
320 self.cached_kind.set(None);
321 }
322
323 #[doc(hidden)]
338 pub fn __param<'a>(&'a self, name: impl Into<SmolStr>, value: f64) -> Expr<'a> {
339 self.register_param(name.into(), value)
340 }
341
342 fn register_param(&self, name: SmolStr, value: f64) -> Expr<'_> {
350 assert!(
351 !self.param_names.borrow().contains_key(&name),
352 "parameter name {name:?} is already registered on this model"
353 );
354 let (id, node) = {
355 let mut a = self.arena.borrow_mut();
356 let id = a.new_param(value);
357 (id, a.param(id))
358 };
359 self.parameters.borrow_mut().push(Parameter { id, name: name.clone() });
360 self.param_names.borrow_mut().insert(name, id);
361 self.cached_kind.set(None);
362 Expr::new(node, &self.arena)
363 }
364
365 #[doc(hidden)]
374 pub fn __indexed_param<'a, K, F>(
375 &'a self,
376 name: impl Into<String>,
377 set: &Set<K>,
378 mut value: F,
379 ) -> IndexedParam<'a, K>
380 where
381 K: FromIndexKey,
382 F: FnMut(K) -> f64,
383 {
384 let base = name.into();
385 let axes = set.axes().map(Box::from);
386 let keys: Vec<IndexKey> = set.iter().collect();
387 let make = |key: &IndexKey| -> Expr<'a> {
388 let pname: SmolStr = format_index_name(&base, key).into();
389 let v = value(K::from_index_key(key));
390 self.register_param(pname, v)
391 };
392 let storage = build_storage(keys, axes, make);
393 IndexedFamily { storage, _marker: PhantomData }
394 }
395
396 pub fn set_param_idx<K, Q: Into<IndexKey>>(
404 &self,
405 params: &IndexedParam<'_, K>,
406 key: Q,
407 value: f64,
408 ) {
409 let e = params.get(key).expect("set_param_idx: key not present in indexed parameter");
410 assert!(
411 std::ptr::eq(e.arena, std::ptr::from_ref(&self.arena)),
412 "set_param_idx: indexed parameter belongs to a different model"
413 );
414 let id = e.param_id().expect("indexed parameter entry is not a parameter handle");
415 self.set_param_id(id, value);
416 }
417
418 pub fn param_value_idx<K, Q: Into<IndexKey>>(
421 &self,
422 params: &IndexedParam<'_, K>,
423 key: Q,
424 ) -> Option<f64> {
425 params.get(key).and_then(|e| self.param_value_of(e))
426 }
427
428 pub fn set_param(&self, p: Expr<'_>, value: f64) {
435 let id = p.param_id().expect("Model::set_param expects a single-parameter expression");
436 self.set_param_id(id, value);
437 }
438
439 pub fn set_param_id(&self, id: ParamId, value: f64) {
444 self.arena.borrow_mut().set_param_value(id, value);
445 self.cached_kind.set(None);
446 }
447
448 pub fn param_value(&self, id: ParamId) -> f64 {
454 self.arena.borrow().param_value(id)
455 }
456
457 pub fn param_value_of(&self, p: Expr<'_>) -> Option<f64> {
460 p.param_id().map(|id| self.param_value(id))
461 }
462
463 pub fn parameter_id(&self, name: &str) -> Option<ParamId> {
464 self.param_names.borrow().get(name).copied()
465 }
466
467 pub fn parameters(&self) -> Ref<'_, Vec<Parameter>> {
468 self.parameters.borrow()
469 }
470
471 pub fn num_parameters(&self) -> usize {
472 self.parameters.borrow().len()
473 }
474
475 #[doc(hidden)]
485 pub fn __add_constraint(
486 &self,
487 name: impl Into<SmolStr>,
488 c: ConstraintExpr<'_>,
489 ) -> ConstraintId {
490 let (lower, upper) = match c.sense {
491 Sense::Le => (f64::NEG_INFINITY, c.rhs),
492 Sense::Ge => (c.rhs, f64::INFINITY),
493 Sense::Eq => (c.rhs, c.rhs),
494 };
495 self.register_constraint(name.into(), c.lhs.id, lower, upper)
496 }
497
498 fn register_constraint(
506 &self,
507 name: SmolStr,
508 lhs: ExprId,
509 lower: f64,
510 upper: f64,
511 ) -> ConstraintId {
512 assert!(
513 !lower.is_nan() && !upper.is_nan(),
514 "constraint {name:?} has NaN bound (lower={lower}, upper={upper})"
515 );
516 let mut by_name = self.constraint_names.borrow_mut();
517 assert!(!by_name.contains_key(&name), "constraint name {name:?} already registered");
518 let mut all = self.constraints.borrow_mut();
519 let id = ConstraintId(u32::try_from(all.len()).expect("constraint count overflow"));
520 all.push(Constraint { name: name.clone(), lhs, lower, upper, active: true });
521 by_name.insert(name, id);
522 self.cached_kind.set(None);
523 id
524 }
525
526 fn next_auto_name(&self) -> SmolStr {
528 loop {
529 let n = self.auto_seq.get();
530 self.auto_seq.set(n + 1);
531 let candidate: SmolStr = format!("_c{n}").into();
532 if !self.constraint_names.borrow().contains_key(&candidate) {
533 break candidate;
534 }
535 }
536 }
537
538 #[doc(hidden)]
541 pub fn __add_constraint_auto(&self, c: ConstraintExpr<'_>) -> ConstraintId {
542 self.__add_constraint(self.next_auto_name(), c)
543 }
544
545 #[doc(hidden)]
548 pub fn __add_constraint_interval(
549 &self,
550 name: impl Into<SmolStr>,
551 lhs: Expr<'_>,
552 lower: f64,
553 upper: f64,
554 ) -> ConstraintId {
555 self.register_constraint(name.into(), lhs.id, lower, upper)
556 }
557
558 pub fn add_constraints<'a, I>(&'a self, items: I)
561 where
562 I: IntoIterator<Item = (SmolStr, ConstraintExpr<'a>)>,
563 {
564 for (name, c) in items {
565 self.__add_constraint(name, c);
566 }
567 }
568
569 #[doc(hidden)]
574 pub fn __add_constraints_over<'a, K, F>(&'a self, name_prefix: &str, set: &Set<K>, mut rule: F)
575 where
576 K: FromIndexKey,
577 F: FnMut(K) -> ConstraintExpr<'a>,
578 {
579 for key in set {
580 let typed = K::from_index_key(&key);
581 let c = rule(typed);
582 let name: SmolStr = format_index_name(name_prefix, &key).into();
583 self.__add_constraint(name, c);
584 }
585 }
586
587 #[doc(hidden)]
593 pub fn __add_range<'a, B1, B2>(&'a self, name: &str, mid: Expr<'a>, lo: B1, hi: B2)
594 where
595 B1: IntoRhs<'a>,
596 B2: IntoRhs<'a>,
597 {
598 if let Some((lower, upper)) = self.collapse_bounds(mid.id, &lo, &hi) {
599 self.register_constraint(name.into(), mid.id, lower, upper);
600 } else {
601 self.__add_constraint(format!("{name}_lo"), mid.ge(lo));
602 self.__add_constraint(format!("{name}_hi"), mid.le(hi));
603 }
604 }
605
606 #[doc(hidden)]
608 pub fn __add_range_auto<'a, B1, B2>(&'a self, mid: Expr<'a>, lo: B1, hi: B2)
609 where
610 B1: IntoRhs<'a>,
611 B2: IntoRhs<'a>,
612 {
613 if let Some((lower, upper)) = self.collapse_bounds(mid.id, &lo, &hi) {
614 self.register_constraint(self.next_auto_name(), mid.id, lower, upper);
615 } else {
616 self.__add_constraint_auto(mid.ge(lo));
617 self.__add_constraint_auto(mid.le(hi));
618 }
619 }
620
621 fn collapse_bounds<'a>(
625 &self,
626 mid: ExprId,
627 lo: &impl IntoRhs<'a>,
628 hi: &impl IntoRhs<'a>,
629 ) -> Option<(f64, f64)> {
630 let lower = lo.const_bound()?;
631 let upper = hi.const_bound()?;
632 (classify(&self.arena.borrow(), mid) == ExprClass::Linear).then_some((lower, upper))
633 }
634
635 #[doc(hidden)]
639 pub fn __add_range_constraints_over<'a, K, B1, B2, F>(
640 &'a self,
641 name: &str,
642 set: &Set<K>,
643 mut rule: F,
644 ) where
645 K: FromIndexKey,
646 B1: IntoRhs<'a>,
647 B2: IntoRhs<'a>,
648 F: FnMut(K) -> (Expr<'a>, B1, B2),
649 {
650 for key in set {
651 let (mid, lo, hi) = rule(K::from_index_key(&key));
652 let row_name = format_index_name(name, &key);
653 self.__add_range(&row_name, mid, lo, hi);
654 }
655 }
656
657 pub fn constraints(&self) -> ModelConstraints<'_> {
660 ModelConstraints {
661 algebraic: self.constraints.borrow(),
662 second_order_cones: self.soc_constraints.borrow(),
663 }
664 }
665
666 pub fn num_constraints(&self) -> usize {
668 self.constraints.borrow().len() + self.soc_constraints.borrow().len()
669 }
670
671 pub fn constraint_id(&self, name: &str) -> Option<ConstraintId> {
672 self.constraint_names.borrow().get(name).copied()
673 }
674
675 pub fn add_soc_constraint<'a>(
690 &'a self,
691 name: impl Into<SmolStr>,
692 terms: impl IntoIterator<Item = Expr<'a>>,
693 bound: Expr<'a>,
694 ) -> SocConstraintId {
695 let name = name.into();
696 let arena = self.arena.borrow();
697 let terms: Vec<ExprId> = terms
698 .into_iter()
699 .map(|e| {
700 assert!(
701 classify(&arena, e.id) == ExprClass::Linear,
702 "SOC constraint {name:?} has a non-affine term"
703 );
704 e.id
705 })
706 .collect();
707 assert!(!terms.is_empty(), "SOC constraint {name:?} has no terms");
708 assert!(
709 classify(&arena, bound.id) == ExprClass::Linear,
710 "SOC constraint {name:?} has a non-affine bound"
711 );
712 drop(arena);
713
714 let mut by_name = self.soc_names.borrow_mut();
715 assert!(!by_name.contains_key(&name), "SOC constraint name {name:?} already registered");
716 let mut all = self.soc_constraints.borrow_mut();
717 let id = SocConstraintId(u32::try_from(all.len()).expect("SOC constraint count overflow"));
718 all.push(SocConstraint { name: name.clone(), terms, bound: bound.id, active: true });
719 by_name.insert(name, id);
720 self.cached_kind.set(None);
721 id
722 }
723
724 fn next_auto_soc_name(&self) -> SmolStr {
728 loop {
729 let n = self.auto_seq.get();
730 self.auto_seq.set(n + 1);
731 let candidate: SmolStr = format!("_soc{n}").into();
732 if !self.soc_names.borrow().contains_key(&candidate) {
733 break candidate;
734 }
735 }
736 }
737
738 #[doc(hidden)]
742 pub fn __add_soc_constraint_auto<'a>(
743 &'a self,
744 terms: impl IntoIterator<Item = Expr<'a>>,
745 bound: Expr<'a>,
746 ) -> SocConstraintId {
747 self.add_soc_constraint(self.next_auto_soc_name(), terms, bound)
748 }
749
750 #[doc(hidden)]
755 pub fn __add_soc_constraints_over<'a, K, T, F>(
756 &'a self,
757 name_prefix: &str,
758 set: &Set<K>,
759 mut rule: F,
760 ) where
761 K: FromIndexKey,
762 T: IntoIterator<Item = Expr<'a>>,
763 F: FnMut(K) -> (T, Expr<'a>),
764 {
765 for key in set {
766 let typed = K::from_index_key(&key);
767 let (terms, bound) = rule(typed);
768 let name: SmolStr = format_index_name(name_prefix, &key).into();
769 self.add_soc_constraint(name, terms, bound);
770 }
771 }
772
773 pub fn soc_constraints(&self) -> Ref<'_, Vec<SocConstraint>> {
779 self.soc_constraints.borrow()
780 }
781
782 pub fn num_soc_constraints(&self) -> usize {
783 self.soc_constraints.borrow().len()
784 }
785
786 pub fn soc_constraint_id(&self, name: &str) -> Option<SocConstraintId> {
787 self.soc_names.borrow().get(name).copied()
788 }
789
790 pub fn has_cones(&self) -> bool {
792 !self.soc_constraints.borrow().is_empty()
793 }
794
795 #[doc(hidden)]
800 pub fn __minimize(&self, expr: Expr<'_>) {
801 self.set_objective(expr, ObjectiveSense::Minimize);
802 }
803
804 #[doc(hidden)]
807 pub fn __maximize(&self, expr: Expr<'_>) {
808 self.set_objective(expr, ObjectiveSense::Maximize);
809 }
810
811 #[doc(hidden)]
815 pub fn __feasibility(&self) {
816 *self.objective.borrow_mut() = None;
817 self.objective_declared.set(true);
818 self.cached_kind.set(None);
819 }
820
821 fn set_objective(&self, expr: Expr<'_>, sense: ObjectiveSense) {
822 *self.objective.borrow_mut() = Some(Objective { expr: expr.id, sense });
823 self.objective_declared.set(true);
824 self.cached_kind.set(None);
825 }
826
827 pub fn is_feasibility(&self) -> bool {
830 self.objective_declared.get() && self.objective.borrow().is_none()
831 }
832
833 pub fn ensure_objective_declared(&self) -> Result<()> {
841 if self.objective_declared.get() { Ok(()) } else { Err(Error::NoObjective) }
842 }
843
844 pub fn objective(&self) -> Ref<'_, Option<Objective>> {
845 self.objective.borrow()
846 }
847
848 pub fn try_objective(&self) -> Result<Objective> {
854 self.objective.borrow().clone().ok_or(Error::NoObjective)
855 }
856
857 pub fn kind(&self) -> ModelKind {
873 if let Some(k) = self.cached_kind.get() {
874 return k;
875 }
876 let k = self.infer_kind_with(None);
877 self.cached_kind.set(Some(k));
878 k
879 }
880
881 fn infer_kind_with(&self, parallel: Option<bool>) -> ModelKind {
887 self.infer_kind_impl(parallel)
888 }
889
890 #[cfg(any(test, feature = "benchmark-support"))]
893 fn infer_kind(&self, parallel: bool) -> ModelKind {
894 self.infer_kind_impl(Some(parallel))
895 }
896
897 fn infer_kind_impl(&self, parallel: Option<bool>) -> ModelKind {
898 let arena = self.arena.borrow();
899 let vars = self.variables.borrow();
900 let has_int = vars.iter().any(|v| v.domain.is_integer());
901 let obj_class = self
902 .objective
903 .borrow()
904 .as_ref()
905 .map_or(ExprClass::Linear, |o| classify(&arena, o.expr));
906
907 let mut any_nonlinear = obj_class == ExprClass::Nonlinear;
908 let mut plain_quad_con = false;
909 let mut detected_soc = false;
910 if !any_nonlinear {
911 let constraints = self.constraints.borrow();
912 let arena_ref = &*arena;
913 let vars_ref = &*vars;
914 let mut quadratic = Vec::new();
915 for c in constraints.iter() {
916 match classify(arena_ref, c.lhs) {
917 ExprClass::Linear => {}
918 ExprClass::Quadratic => quadratic.push(c),
919 ExprClass::Nonlinear => {
920 any_nonlinear = true;
921 break;
922 }
923 }
924 }
925 if !any_nonlinear {
926 let use_parallel = parallel.unwrap_or(
927 quadratic.len() >= PAR_KIND_THRESHOLD && rayon::current_num_threads() > 1,
928 );
929 if use_parallel {
930 let (has_cone, has_plain) = quadratic
931 .par_iter()
932 .map(|c| {
933 let is_soc = is_detected_soc(arena_ref, vars_ref, c);
934 (is_soc, !is_soc)
935 })
936 .reduce(
937 || (false, false),
938 |left, right| (left.0 || right.0, left.1 || right.1),
939 );
940 detected_soc = has_cone;
941 plain_quad_con = has_plain;
942 } else {
943 for c in quadratic {
944 if is_detected_soc(arena_ref, vars_ref, c) {
945 detected_soc = true;
946 } else {
947 plain_quad_con = true;
948 }
949 }
950 }
951 }
952 }
953 let has_soc = detected_soc || !self.soc_constraints.borrow().is_empty();
954
955 let pick = |cont, int| if has_int { int } else { cont };
956 if any_nonlinear {
957 pick(ModelKind::NLP, ModelKind::MINLP)
958 } else if plain_quad_con {
959 pick(ModelKind::QCP, ModelKind::MIQCP)
960 } else if has_soc {
961 pick(ModelKind::SOCP, ModelKind::MISOCP)
962 } else if obj_class == ExprClass::Quadratic {
963 pick(ModelKind::QP, ModelKind::MIQP)
964 } else {
965 pick(ModelKind::LP, ModelKind::MILP)
966 }
967 }
968}
969
970type BoundFn<'a> = Box<dyn Fn(&IndexKey) -> f64 + 'a>;
979
980#[must_use = "IndexedVarBuilder does nothing until you call .build()"]
981pub struct IndexedVarBuilder<'a, K = IndexKey> {
982 model: &'a Model,
983 base_name: String,
984 keys: Vec<IndexKey>,
985 axes: Option<Box<[Axis]>>,
986 lb: f64,
987 ub: f64,
988 lb_by: Option<BoundFn<'a>>,
989 ub_by: Option<BoundFn<'a>>,
990 domain: Domain,
991 _k: PhantomData<fn() -> K>,
992}
993
994impl<'a, K> std::fmt::Debug for IndexedVarBuilder<'a, K> {
995 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
996 f.debug_struct("IndexedVarBuilder")
997 .field("base_name", &self.base_name)
998 .field("keys", &self.keys.len())
999 .field("lb", &self.lb)
1000 .field("ub", &self.ub)
1001 .field("per_key_lb", &self.lb_by.is_some())
1002 .field("per_key_ub", &self.ub_by.is_some())
1003 .field("domain", &self.domain)
1004 .finish()
1005 }
1006}
1007
1008impl<'a, K> IndexedVarBuilder<'a, K> {
1009 pub fn lb(mut self, v: f64) -> Self {
1010 self.lb = v;
1011 self
1012 }
1013 pub fn ub(mut self, v: f64) -> Self {
1014 self.ub = v;
1015 self
1016 }
1017 pub fn bounds(mut self, lb: f64, ub: f64) -> Self {
1018 self.lb = lb;
1019 self.ub = ub;
1020 self
1021 }
1022 pub fn lb_by<F>(mut self, f: F) -> Self
1031 where
1032 K: FromIndexKey,
1033 F: Fn(K) -> f64 + 'a,
1034 {
1035 self.lb_by = Some(Box::new(move |k: &IndexKey| f(K::from_index_key(k))));
1036 self
1037 }
1038 pub fn ub_by<F>(mut self, f: F) -> Self
1047 where
1048 K: FromIndexKey,
1049 F: Fn(K) -> f64 + 'a,
1050 {
1051 self.ub_by = Some(Box::new(move |k: &IndexKey| f(K::from_index_key(k))));
1052 self
1053 }
1054 pub fn domain(mut self, d: Domain) -> Self {
1055 self.domain = d;
1056 self
1057 }
1058 pub fn integer(mut self) -> Self {
1059 self.domain = Domain::Integer;
1060 self
1061 }
1062 pub fn binary(mut self) -> Self {
1063 self.domain = Domain::Binary;
1064 self.lb = 0.0;
1065 self.ub = 1.0;
1066 self
1067 }
1068
1069 pub fn build(self) -> IndexedVar<'a, K> {
1074 let Self { model, base_name, keys, axes, lb, ub, lb_by, ub_by, domain, _k } = self;
1075
1076 let make = |key: &IndexKey| -> Expr<'a> {
1077 let scalar_name: SmolStr = format_index_name(&base_name, key).into();
1078 let lo = lb_by.as_ref().map_or(lb, |f| f(key));
1079 let hi = ub_by.as_ref().map_or(ub, |f| f(key));
1080 model.__var(scalar_name).lb(lo).ub(hi).domain(domain).build()
1081 };
1082
1083 let storage = build_storage(keys, axes, make);
1084 IndexedFamily { storage, _marker: PhantomData }
1085 }
1086}
1087
1088fn format_index_name(base: &str, key: &IndexKey) -> String {
1089 let mut out = String::with_capacity(base.len() + 4);
1090 out.push_str(base);
1091 out.push('[');
1092 write_key_parts(&mut out, key);
1093 out.push(']');
1094 out
1095}
1096
1097fn write_key_parts(out: &mut String, key: &IndexKey) {
1098 use std::fmt::Write;
1099 match key {
1100 IndexKey::Int(i) => write!(out, "{i}").unwrap(),
1101 IndexKey::Str(s) => out.push_str(s),
1102 IndexKey::Tuple(parts) => {
1103 for (i, p) in parts.iter().enumerate() {
1104 if i > 0 {
1105 out.push(',');
1106 }
1107 write_key_parts(out, p);
1108 }
1109 }
1110 }
1111}
1112
1113pub fn display_index_key(key: &IndexKey) -> String {
1116 let mut out = String::new();
1117 write_key_parts(&mut out, key);
1118 out
1119}
1120
1121#[cfg(feature = "benchmark-support")]
1122#[doc(hidden)]
1123#[expect(clippy::cast_precision_loss)]
1124#[allow(clippy::wildcard_imports)]
1125pub mod benchmark_support {
1126 use super::*;
1127
1128 pub const THRESHOLD: usize = PAR_KIND_THRESHOLD;
1129
1130 pub fn model(rows: usize, degree: usize) -> Model {
1131 let model = Model::new("kind_bench");
1132 let x = model.__var("x").build();
1133 let y = model.__var("y").build();
1134 let z = model.__var("z").build();
1135 model.__minimize(x + y + z);
1136 for i in 0..rows {
1137 let lhs = match degree {
1138 1 => x + 2.0 * y - z,
1139 2 => x.powi(2) + y,
1140 _ => x * y * z,
1141 };
1142 model.__add_constraint_auto(lhs.le(i as f64 + 10.0));
1143 }
1144 model
1145 }
1146
1147 pub fn soc_model(rows: usize) -> Model {
1148 let model = Model::new("kind_soc_bench");
1149 let x = model.__var("x").build();
1150 let y = model.__var("y").build();
1151 let t = model.__var("t").lb(0.0).build();
1152 model.__minimize(t);
1153 for _ in 0..rows {
1154 model.__add_constraint_auto((x.powi(2) + y.powi(2) - t.powi(2)).le(0.0));
1155 }
1156 model
1157 }
1158
1159 pub fn infer(model: &Model, parallel: bool) -> ModelKind {
1160 model.infer_kind(parallel)
1161 }
1162}
1163
1164#[cfg(test)]
1165#[expect(clippy::cast_precision_loss)]
1166mod tests {
1167 use oximo_expr::extract_linear;
1168
1169 use super::*;
1170 use crate::Set;
1171 use crate::constraint::Relate;
1172
1173 #[test]
1174 fn param_times_var_keeps_model_linear() {
1175 let m = Model::new("p");
1176 let param = m.__param("param", 4.0);
1177 let x = m.__var("x").lb(0.0).build();
1178 m.__minimize(param * x);
1179 assert_eq!(m.kind(), ModelKind::LP);
1180 }
1181
1182 #[test]
1183 fn param_coeff_resolves_and_rebinds() {
1184 let m = Model::new("p");
1185 let param = m.__param("param", 4.0);
1186 let x = m.__var("x").lb(0.0).build();
1187 let obj = param * x;
1188
1189 let coeff = |m: &Model| {
1190 let arena = m.arena();
1191 extract_linear(&arena, obj.id).expect("linear").coeffs[0].1
1192 };
1193 assert!((coeff(&m) - 4.0).abs() < f64::EPSILON);
1194
1195 m.set_param(param, 9.0);
1196 assert!((coeff(&m) - 9.0).abs() < f64::EPSILON);
1197 assert_eq!(m.parameter_id("param"), Some(param.param_id().unwrap()));
1198 }
1199
1200 #[test]
1201 fn param_value_reads_live_arena_value() {
1202 let m = Model::new("p");
1203 let param = m.__param("param", 4.0);
1204 let id = param.param_id().unwrap();
1205 assert!((m.param_value(id) - 4.0).abs() < f64::EPSILON);
1206 assert!((m.param_value_of(param).unwrap() - 4.0).abs() < f64::EPSILON);
1207
1208 m.set_param(param, 7.5);
1209 assert!((m.param_value(id) - 7.5).abs() < f64::EPSILON);
1210
1211 let x = m.__var("x").build();
1212 assert!(m.param_value_of(x).is_none());
1213 }
1214
1215 #[test]
1216 fn set_param_invalidates_kind_cache() {
1217 let m = Model::new("p");
1218 let p = m.__param("p", 1.0);
1219 let x = m.__var("x").lb(0.0).build();
1220 m.__add_constraint("c", (p * x).le(10.0));
1221 assert_eq!(m.kind(), ModelKind::LP);
1222 m.set_param(p, 2.0);
1223 assert_eq!(m.kind(), ModelKind::LP);
1224 }
1225
1226 #[test]
1227 fn unified_constraints_include_inactive_entries() {
1228 let m = Model::new("inactive");
1229 let x = m.__var("x").build();
1230 let t = m.__var("t").lb(0.0).build();
1231 m.__add_constraint("row", x.le(1.0));
1232 m.add_soc_constraint("cone", [x], t);
1233 m.constraints.borrow_mut()[0].active = false;
1234 m.soc_constraints.borrow_mut()[0].active = false;
1235
1236 let constraints = m.constraints();
1237 assert_eq!(constraints.len(), 2);
1238 assert!(constraints.iter().all(|entry| match entry {
1239 ConstraintRef::Algebraic { constraint, .. } => !constraint.active,
1240 ConstraintRef::SecondOrderCone { constraint, .. } => !constraint.active,
1241 }));
1242 }
1243
1244 #[test]
1245 fn uncached_kind_inference_leaves_kind_cache_empty() {
1246 let m = Model::new("uncached_kind");
1247 let x = m.__var("x").build();
1248 m.__minimize(x);
1249 for _ in 0..PAR_KIND_THRESHOLD {
1250 m.__add_constraint_auto(x.powi(2).le(1.0));
1251 }
1252
1253 assert_eq!(m.infer_kind(false), ModelKind::QCP);
1254 assert_eq!(m.cached_kind.get(), None);
1255 assert_eq!(m.infer_kind(true), ModelKind::QCP);
1256 assert_eq!(m.cached_kind.get(), None);
1257
1258 assert_eq!(m.kind(), ModelKind::QCP);
1259 assert_eq!(m.cached_kind.get(), Some(ModelKind::QCP));
1260 }
1261
1262 #[test]
1263 #[should_panic(expected = "parameter name \"dup\" is already registered")]
1264 fn duplicate_param_name_panics() {
1265 let m = Model::new("p");
1266 let _a = m.__param("dup", 1.0);
1267 let _b = m.__param("dup", 2.0);
1268 }
1269
1270 #[test]
1271 fn indexed_param_dense_value_and_per_key_rebind() {
1272 let m = Model::new("ip");
1273 let items = Set::range(0..3);
1274 let data = [10.0, 20.0, 30.0];
1275 let cost = m.__indexed_param("cost", &items, |i: usize| data[i]);
1276
1277 assert!(cost.is_dense());
1278 assert_eq!(cost.len(), 3);
1279 assert_eq!(m.num_parameters(), 3);
1280 assert!(m.parameter_id("cost[0]").is_some());
1281 assert!(m.parameter_id("cost[2]").is_some());
1282 assert!((m.param_value_idx(&cost, 1usize).unwrap() - 20.0).abs() < f64::EPSILON);
1283
1284 let x = m.__var("x").lb(0.0).build();
1285 let obj = cost.at([1]) * x;
1286 let coeff = |m: &Model| {
1287 let arena = m.arena();
1288 extract_linear(&arena, obj.id).expect("linear").coeffs[0].1
1289 };
1290 assert!((coeff(&m) - 20.0).abs() < f64::EPSILON);
1291
1292 m.set_param_idx(&cost, 1usize, 99.0);
1293 assert!((coeff(&m) - 99.0).abs() < f64::EPSILON);
1294 assert!((m.param_value_idx(&cost, 1usize).unwrap() - 99.0).abs() < f64::EPSILON);
1295 assert!((m.param_value_idx(&cost, 0usize).unwrap() - 10.0).abs() < f64::EPSILON);
1296 }
1297
1298 #[test]
1299 #[should_panic(expected = "different model")]
1300 fn set_param_idx_rejects_foreign_family() {
1301 let a = Model::new("a");
1302 let b = Model::new("b");
1303 let items = Set::range(0..2);
1304 let pa = a.__indexed_param("p", &items, |_i: usize| 1.0);
1305 b.set_param_idx(&pa, 0usize, 5.0);
1306 }
1307
1308 #[test]
1309 fn indexed_param_sparse_string_keyed() {
1310 let m = Model::new("ips");
1311 let plants = Set::strings(["a", "b"]);
1312 let price =
1313 m.__indexed_param("price", &plants, |p: String| if p == "a" { 1.5 } else { 2.5 });
1314 assert!(!price.is_dense());
1315 assert_eq!(price.len(), 2);
1316 assert!((m.param_value_idx(&price, "a").unwrap() - 1.5).abs() < f64::EPSILON);
1317 assert!((m.param_value_idx(&price, "b").unwrap() - 2.5).abs() < f64::EPSILON);
1318 assert!(m.param_value_idx(&price, "z").is_none());
1319 }
1320
1321 #[test]
1322 fn kind_forced_serial_and_parallel_classification_agree() {
1323 let qcp = Model::new("qcp");
1324 let x = qcp.__var("x").build();
1325 let y = qcp.__var("y").build();
1326 qcp.__minimize(x + y);
1327 for i in 0..PAR_KIND_THRESHOLD + 3 {
1328 qcp.__add_constraint_auto((x.powi(2) + y).le(i as f64 + 1.0));
1329 }
1330 assert_eq!(qcp.infer_kind(false), ModelKind::QCP);
1331 assert_eq!(qcp.infer_kind(false), qcp.infer_kind(true));
1332 assert_eq!(qcp.infer_kind(false), qcp.infer_kind_with(None));
1333
1334 let socp = Model::new("socp");
1335 let x = socp.__var("x").build();
1336 let t = socp.__var("t").lb(0.0).build();
1337 socp.__minimize(t);
1338 for _ in 0..PAR_KIND_THRESHOLD + 3 {
1339 socp.__add_constraint_auto((x.powi(2) - t.powi(2)).le(0.0));
1340 }
1341 assert_eq!(socp.infer_kind(false), ModelKind::SOCP);
1342 assert_eq!(socp.infer_kind(false), socp.infer_kind(true));
1343 assert_eq!(socp.infer_kind(false), socp.infer_kind_with(None));
1344
1345 let nlp = Model::new("nlp");
1346 let x = nlp.__var("x").build();
1347 let y = nlp.__var("y").build();
1348 let z = nlp.__var("z").build();
1349 nlp.__minimize(x + y + z);
1350 for _ in 0..PAR_KIND_THRESHOLD + 3 {
1351 nlp.__add_constraint_auto((x * y * z).le(1.0));
1352 }
1353 assert_eq!(nlp.infer_kind(false), ModelKind::NLP);
1354 assert_eq!(nlp.infer_kind(false), nlp.infer_kind(true));
1355 }
1356}