Skip to main content

oximo_core/
model.rs

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/// The kind of mathematical program a `Model` represents.
22///
23/// This is inferred from the variables and expressions in the model, not set
24/// explicitly by the user. See [`Model::kind`] for the exact decision ladder.
25///
26/// The `MI*` variant of each class is picked when any variable has an integer
27/// domain. The continuous classes are, from most to least general:
28///
29/// - `NLP`: some expression is nonlinear (degree > 2, transcendental, division)
30/// - `QCP`: some constraint is quadratic and not recognized as a second-order
31///   cone
32/// - `SOCP`: second-order cone constraints are present (explicit
33///   [`crate::SocConstraint`]s or SOC-shaped quadratic constraints recognized
34///   by [`crate::detect_soc`]); the objective may be linear or quadratic
35/// - `QP`: quadratic objective, linear constraints
36/// - `LP`: everything linear
37#[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/// A borrowed constraint from a [`Model`].
52///
53/// Algebraic constraints and explicitly declared second-order-cone constraints
54/// retain their typed IDs and storage. This enum provides a unified inspection
55/// boundary without changing either representation.
56#[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/// Unified borrowed view of every constraint declared on a [`Model`].
63///
64/// The underlying algebraic and explicit-SOC registries remain separate, so
65/// backends can iterate a homogeneous slice without a per-constraint branch.
66/// [`Self::iter`] visits algebraic constraints in [`ConstraintId`] order,
67/// followed by explicit cones in [`SocConstraintId`] order.
68#[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    /// Algebraic constraints in [`ConstraintId`] order.
76    pub fn algebraic(&self) -> &[Constraint] {
77        &self.algebraic
78    }
79
80    /// Explicit second-order-cone constraints in [`SocConstraintId`] order.
81    pub fn second_order_cones(&self) -> &[SocConstraint] {
82        &self.second_order_cones
83    }
84
85    /// Iterate over all declared constraints without allocating.
86    #[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    /// Total number of algebraic and explicit second-order-cone constraints.
102    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
111/// The optimization model. Owns the expression arena, variable/parameter
112/// registries, constraints, and (optional) objective.
113///
114/// `Model` uses interior mutability so the builder API can take `&self`
115/// references.
116///
117/// Variables, constraints, and the objective are added through
118/// `RefCell`s under the hood.
119pub 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    /// Monotonic counter for auto-naming anonymous constraints registered via
134    /// the `constraint!` macro.
135    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    // Variables
173
174    /// Macro-facing entry point backing the `variable!` macro. Not part of the
175    /// stable public API.
176    #[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    /// Construct a constant expression for format readers and other adapters.
189    #[doc(hidden)]
190    pub fn __constant(&self, value: f64) -> Expr<'_> {
191        Expr::constant(&self.arena, value)
192    }
193
194    /// Called by [`VarBuilder::build`]. Pushes the var into the registry and
195    /// returns its `Expr` handle.
196    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    /// Macro-facing entry point backing the indexed form of the `variable!`
221    /// macro. Not part of the stable public API.
222    #[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    /// Render an [`EvalError`] using this model's registered variable/parameter
259    /// name instead of the bare numeric id it carries.
260    /// Use it when surfacing an evaluation failure to a user.
261    #[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    /// Fix a single-variable expression to `value`.
279    /// Convenience over [`Self::fix_var`] for handles from the `variable!` macro
280    /// or [`crate::IndexedVar`] indexing.
281    ///
282    /// # Panics
283    ///
284    /// Panics if `e` is not a bare variable handle.
285    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    /// Fix variable `id` to `value` by setting `lb = ub = value`.
291    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    /// Set the initial (warm-start) value of a single-variable expression.
301    /// The macro API has no bound-style syntax for warm starts, so this is the
302    /// supported way to seed `variable!`-declared variables.
303    ///
304    /// # Panics
305    ///
306    /// Panics if `e` is not a bare variable handle.
307    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    /// Restore bounds on variable `id`. Pass `f64::NEG_INFINITY` / `f64::INFINITY`
313    /// to restore an unbounded direction.
314    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    // Parameters
324
325    /// Macro-facing entry point backing the `param!` macro. Not part of the
326    /// stable public API.
327    ///
328    /// Registers a named scalar parameter initialized to `value`, returning an
329    /// [`Expr`] handle that references it symbolically. A parameter behaves like a
330    /// constant coefficient (`param * var` is linear) but stays symbolic so it can
331    /// be re-bound with [`Self::set_param`] / [`Self::set_param_id`] between solves
332    /// without rebuilding the model.
333    ///
334    /// # Panics
335    ///
336    /// Panics if a parameter with the same name is already registered.
337    #[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    /// Register one scalar parameter named `name` initialized to `value` and
343    /// return its `Expr` handle. Shared by [`Self::__param`] and the indexed
344    /// builder.
345    ///
346    /// # Panics
347    ///
348    /// Panics if a parameter with the same name is already registered.
349    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    /// Macro-facing entry point backing the indexed form of the `param!` macro
366    /// (`param!(m, cost[i in items] = data[i])`). Registers one scalar parameter
367    /// per key, evaluating `value` on the typed key, and returns an
368    /// [`IndexedParam`]. Not part of the stable public API.
369    ///
370    /// # Panics
371    ///
372    /// Panics if a per-key parameter name collides with one already registered.
373    #[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    /// Re-bind the parameter at `key` of an indexed family to `value`. Takes
397    /// effect on the next solve.
398    ///
399    /// # Panics
400    ///
401    /// Panics if `key` is not present in the family, or if `params` was built on
402    /// a different `Model`.
403    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    /// Current value bound to the parameter at `key` of an indexed family, or
419    /// `None` if the key is absent.
420    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    /// Re-bind the parameter referenced by handle `p` to `value`.
429    ///
430    /// # Panics
431    ///
432    /// Panics if `p` is not a bare parameter handle (one returned by the `param!`
433    /// macro).
434    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    /// Re-bind parameter `id` to `value`. Takes effect on the next solve.
440    ///
441    /// The value is stored only in the expression arena (its single source of
442    /// truth); extraction and evaluation read it from there.
443    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    /// Current value bound to parameter `id`.
449    ///
450    /// # Panics
451    ///
452    /// Panics if `id` does not belong to a parameter registered on this model.
453    pub fn param_value(&self, id: ParamId) -> f64 {
454        self.arena.borrow().param_value(id)
455    }
456
457    /// Current value of the parameter referenced by handle `p`, or `None` if
458    /// `p` is not a bare parameter handle.
459    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    // Constraints
476
477    /// Macro-facing entry point backing the `constraint!` macro. Not part of the
478    /// stable public API.
479    ///
480    /// # Panics
481    ///
482    /// Panics if a constraint with the same name is already registered, or if
483    /// the constraint count exceeds `u32::MAX`.
484    #[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    /// Push a constraint row `lower <= lhs <= upper` into the registry. Shared by
499    /// [`Self::__add_constraint`] and the range entry points.
500    ///
501    /// # Panics
502    ///
503    /// Panics if a constraint with the same name is already registered, if a
504    /// bound is NaN, or if the constraint count exceeds `u32::MAX`.
505    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    /// A fresh unique auto-name `_c{n}`, skipping any a user already took.
527    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    /// Register an anonymous constraint, deriving a unique name `_c{n}` from an
539    /// internal counter. Backs the name-less form of the `constraint!` macro.
540    #[doc(hidden)]
541    pub fn __add_constraint_auto(&self, c: ConstraintExpr<'_>) -> ConstraintId {
542        self.__add_constraint(self.next_auto_name(), c)
543    }
544
545    /// Register a canonical interval row. This is intentionally hidden from
546    /// the public builder API; file readers need to preserve native range rows.
547    #[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    /// Bulk-register constraints. Each entry is `(name, ConstraintExpr)`.
559    /// Useful with `.par_iter().map(...).collect()` style construction.
560    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    /// Macro-facing entry point backing the indexed-family form of the
570    /// `constraint!` macro. The closure receives the index as a typed value `K`
571    /// (any [`FromIndexKey`]: `i64`, `i32`, `usize`, `String`, raw `IndexKey`, or
572    /// tuples up to arity 4). Not part of the stable public API.
573    #[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    /// Macro-facing entry point for a two-sided range `lo <= mid <= hi`.
588    ///
589    /// Collapses to a single interval [`Constraint`] named `name` only when both
590    /// bounds are pure constants and the body is linear (the condition under which
591    /// one two-sided row is representable).
592    #[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    /// Anonymous form of [`Self::__add_range`] (auto-named rows).
607    #[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    /// The interval `(lower, upper)` a range collapses to, or `None` (keep two
622    /// rows). Requires both bounds to be literal constants and the body `mid` to
623    /// be linear.
624    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    /// Macro-facing entry point for a two-sided range family. One row per key,
636    /// each collapsing to a single interval constraint when both bounds are
637    /// constant (see [`Self::__add_range`]).
638    #[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    /// Unified view of every algebraic and explicit second-order-cone
658    /// constraint declared on this model.
659    pub fn constraints(&self) -> ModelConstraints<'_> {
660        ModelConstraints {
661            algebraic: self.constraints.borrow(),
662            second_order_cones: self.soc_constraints.borrow(),
663        }
664    }
665
666    /// Total number of algebraic and explicit second-order-cone constraints.
667    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    // Second-order cone constraints
676
677    /// Register the explicit second-order cone constraint
678    /// `||terms||_2 <= bound`.
679    ///
680    /// Every member of `terms` and the `bound` must be affine; the bound is
681    /// additionally constrained to be nonnegative by the cone itself, so
682    /// backends emit a `bound >= 0` side condition where needed.
683    ///
684    /// # Panics
685    ///
686    /// Panics if a SOC constraint with the same name is already registered, if
687    /// `terms` is empty, if any term or the bound is not affine, or if the
688    /// count exceeds `u32::MAX`.
689    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    /// A fresh unique auto-name `_soc{n}` in the SOC namespace, skipping any a
725    /// user already took. Shares `auto_seq` with [`Self::next_auto_name`]; the
726    /// prefixes differ, so the two namespaces never collide.
727    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    /// Register an anonymous SOC constraint, deriving a unique name `_soc{n}`
739    /// from an internal counter. Backs the name-less form of the
740    /// `soc_constraint!` macro. Not part of the stable public API.
741    #[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    /// Macro-facing entry point backing the indexed-family form of the
751    /// `soc_constraint!` macro: one cone per key, named `{prefix}[{key}]`. The
752    /// closure returns the cone's `(terms, bound)` pair for each typed key.
753    /// Not part of the stable public API.
754    #[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    /// Typed explicit-SOC registry for specialized backend passes.
774    ///
775    /// Use [`Self::constraints`] when inspecting constraints generically. This
776    /// accessor exists so performance-sensitive translators can keep a
777    /// homogeneous borrow scoped to one conic pass.
778    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    /// Whether the model carries any explicit second-order cone constraints.
791    pub fn has_cones(&self) -> bool {
792        !self.soc_constraints.borrow().is_empty()
793    }
794
795    // Objective
796
797    /// Macro-facing entry point backing `objective!(m, Min, ..)`. Not part of the
798    /// stable public API.
799    #[doc(hidden)]
800    pub fn __minimize(&self, expr: Expr<'_>) {
801        self.set_objective(expr, ObjectiveSense::Minimize);
802    }
803
804    /// Macro-facing entry point backing `objective!(m, Max, ..)`. Not part of the
805    /// stable public API.
806    #[doc(hidden)]
807    pub fn __maximize(&self, expr: Expr<'_>) {
808        self.set_objective(expr, ObjectiveSense::Maximize);
809    }
810
811    /// Macro-facing entry point backing `objective!(m, Feasibility)`. Declares
812    /// the model a feasibility problem (no objective to optimize), clearing any
813    /// previously set objective. Not part of the stable public API.
814    #[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    /// Whether feasibility was declared explicitly via `objective!(m, Feasibility)`,
828    /// as opposed to a model that simply has no objective set.
829    pub fn is_feasibility(&self) -> bool {
830        self.objective_declared.get() && self.objective.borrow().is_none()
831    }
832
833    /// Ensure the model has a solve direction declared: either an objective
834    /// (`Min`/`Max`) or an explicit feasibility problem.
835    ///
836    /// # Errors
837    ///
838    /// Returns [`Error::NoObjective`] if neither an objective nor
839    /// `objective!(m, Feasibility)` was declared.
840    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    /// Try to get a cloned copy of the objective.
849    ///
850    /// # Errors
851    ///
852    /// Returns [`Error::NoObjective`] if no objective is set on this model.
853    pub fn try_objective(&self) -> Result<Objective> {
854        self.objective.borrow().clone().ok_or(Error::NoObjective)
855    }
856
857    // Classification
858
859    /// Infer the [`ModelKind`] from current variables and expressions.
860    /// Result is cached and invalidated whenever variables, constraints, or the
861    /// objective change.
862    ///
863    /// The decision ladder, top-down (any integer variable picks the `MI*`
864    /// column):
865    ///
866    /// 1. any nonlinear expression (objective or constraint) -> `NLP`
867    /// 2. any quadratic constraint not recognized as SOC (see
868    ///    [`crate::detect_soc`]) -> `QCP`
869    /// 3. cones present (explicit or detected) -> `SOCP`
870    /// 4. quadratic objective -> `QP`
871    /// 5. otherwise -> `LP`
872    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    /// Infer the model kind without reading or updating the kind cache.
882    ///
883    /// Automatic inference keeps the initial classification scan serial, then
884    /// parallelizes only a large set of already-known quadratic candidates for
885    /// SOC recognition. This keeps LP/NLP models off the Rayon path.
886    fn infer_kind_with(&self, parallel: Option<bool>) -> ModelKind {
887        self.infer_kind_impl(parallel)
888    }
889
890    /// Infer the kind with a forced serial or parallel SOC-recognition pass.
891    /// This is used by benchmarks and parity tests.
892    #[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
970// IndexedVarBuilder
971
972/// Builder for a collection of scalar variables indexed by a [`Set`].
973///
974/// For example, `flow[i]` for `i in 0..3` registers `flow[0]`, `flow[1]`, and
975/// `flow[2]` as separate scalar variables in the model. Call `.build()` to get
976/// an [`IndexedVar`] that maps each key to its [`Expr`] handle. Bounds and
977/// domain set here apply uniformly to every scalar in the collection.
978type 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    /// Per-key lower bound. Overrides [`Self::lb`] when both are set.
1023    ///
1024    /// The closure receives a typed index value via [`FromIndexKey`].
1025    /// Annotate the argument to select the projection:
1026    /// ```ignore
1027    /// .lb_by(|(p, q): (String, String)| floor_for(&p, &q))
1028    /// .lb_by(|i: usize| lower_bounds[i])
1029    /// ```
1030    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    /// Per-key upper bound. Overrides [`Self::ub`] when both are set.
1039    ///
1040    /// The closure receives a typed index value via [`FromIndexKey`]; annotate
1041    /// the argument to select the projection:
1042    /// ```ignore
1043    /// .ub_by(|(p, q): (String, String)| capacity_for(&p, &q))
1044    /// .ub_by(|i: usize| upper_bounds[i])
1045    /// ```
1046    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    /// Register one scalar variable per key and return the [`IndexedVar`] handle.
1070    ///
1071    /// # Panics
1072    /// Panics if a scalar variable name collides with one already registered.
1073    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
1113/// Public render of an `IndexKey`'s textual form, used when deriving
1114/// auto-generated names for indexed-family constraints.
1115pub 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}