Skip to main content

oximo_core/
model.rs

1use std::cell::{Cell, Ref, RefCell};
2use std::marker::PhantomData;
3
4use oximo_expr::{Expr, ExprArena, ExprClass, ParamId, VarId, classify};
5use rustc_hash::FxHashMap;
6use smol_str::SmolStr;
7
8use crate::constraint::{Constraint, ConstraintExpr, ConstraintId};
9use crate::domain::Domain;
10use crate::error::{Error, Result};
11use crate::indexed::{IndexedVar, Storage, grid_offset};
12use crate::objective::{Objective, ObjectiveSense};
13use crate::param::Parameter;
14use crate::set::{Axis, FromIndexKey, IndexKey, Set};
15use crate::var::{VarBuilder, Variable};
16
17/// The kind of mathematical program a `Model` represents.
18///
19/// This is inferred from the variables and expressions in the model, not set
20/// explicitly by the user. See [`Model::kind`] for details.
21#[derive(Copy, Clone, Debug, PartialEq, Eq)]
22pub enum ModelKind {
23    LP,
24    MILP,
25    QP,
26    MIQP,
27    NLP,
28    MINLP,
29}
30
31/// The optimization model. Owns the expression arena, variable/parameter
32/// registries, constraints, and (optional) objective.
33///
34/// `Model` uses interior mutability so the builder API can take `&self`
35/// references.
36///
37/// Variables, constraints, and the objective are added through
38/// `RefCell`s under the hood.
39pub struct Model {
40    pub name: String,
41    pub(crate) arena: RefCell<ExprArena>,
42    pub(crate) variables: RefCell<Vec<Variable>>,
43    pub(crate) var_names: RefCell<FxHashMap<SmolStr, VarId>>,
44    pub(crate) parameters: RefCell<Vec<Parameter>>,
45    pub(crate) param_names: RefCell<FxHashMap<SmolStr, ParamId>>,
46    pub(crate) constraints: RefCell<Vec<Constraint>>,
47    pub(crate) constraint_names: RefCell<FxHashMap<SmolStr, ConstraintId>>,
48    pub(crate) objective: RefCell<Option<Objective>>,
49    cached_kind: RefCell<Option<ModelKind>>,
50    /// Monotonic counter for auto-naming anonymous constraints registered via
51    /// the `constraint!` macro.
52    auto_seq: Cell<u32>,
53}
54
55impl std::fmt::Debug for Model {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("Model")
58            .field("name", &self.name)
59            .field("vars", &self.variables.borrow().len())
60            .field("params", &self.parameters.borrow().len())
61            .field("constraints", &self.constraints.borrow().len())
62            .field("has_objective", &self.objective.borrow().is_some())
63            .finish()
64    }
65}
66
67impl Model {
68    pub fn new(name: impl Into<String>) -> Self {
69        Self {
70            name: name.into(),
71            arena: RefCell::new(ExprArena::new()),
72            variables: RefCell::new(Vec::new()),
73            var_names: RefCell::new(FxHashMap::default()),
74            parameters: RefCell::new(Vec::new()),
75            param_names: RefCell::new(FxHashMap::default()),
76            constraints: RefCell::new(Vec::new()),
77            constraint_names: RefCell::new(FxHashMap::default()),
78            objective: RefCell::new(None),
79            cached_kind: RefCell::new(None),
80            auto_seq: Cell::new(0),
81        }
82    }
83
84    // Variables
85
86    #[deprecated(
87        since = "0.3.0",
88        note = "use the `variable!` macro, the builder API is scheduled for removal in 0.4.0"
89    )]
90    pub fn var(&self, name: impl Into<SmolStr>) -> VarBuilder<'_> {
91        self.__var(name)
92    }
93
94    /// Macro-facing entry point behind [`Self::var`]. Not part of the stable
95    /// public API; use the `variable!` macro (or `var`) instead.
96    #[doc(hidden)]
97    pub fn __var(&self, name: impl Into<SmolStr>) -> VarBuilder<'_> {
98        VarBuilder {
99            model: self,
100            name: name.into(),
101            lb: f64::NEG_INFINITY,
102            ub: f64::INFINITY,
103            domain: Domain::Real,
104            initial: None,
105        }
106    }
107
108    /// Called by [`VarBuilder::build`]. Pushes the var into the registry and
109    /// returns its `Expr` handle.
110    pub(crate) fn register_var<'a>(&'a self, b: VarBuilder<'a>) -> Expr<'a> {
111        let mut names = self.var_names.borrow_mut();
112        assert!(!names.contains_key(&b.name), "variable name {:?} already registered", b.name);
113        let mut vars = self.variables.borrow_mut();
114        let id = VarId(u32::try_from(vars.len()).expect("variable count overflow"));
115        vars.push(Variable {
116            id,
117            name: b.name.clone(),
118            domain: b.domain,
119            lb: b.lb,
120            ub: b.ub,
121            initial: b.initial,
122        });
123        names.insert(b.name, id);
124        drop(vars);
125        drop(names);
126        *self.cached_kind.borrow_mut() = None;
127        Expr::from_var(&self.arena, id)
128    }
129
130    #[deprecated(
131        since = "0.3.0",
132        note = "use the `variable!` macro, the builder API is scheduled for removal in 0.4.0"
133    )]
134    pub fn indexed_var<'a, K>(
135        &'a self,
136        name: impl Into<String>,
137        set: &Set<K>,
138    ) -> IndexedVarBuilder<'a, K> {
139        self.__indexed_var(name, set)
140    }
141
142    /// Macro-facing entry point behind [`Self::indexed_var`]. Not part of the
143    /// stable public API; use the `variable!` macro instead.
144    #[doc(hidden)]
145    pub fn __indexed_var<'a, K>(
146        &'a self,
147        name: impl Into<String>,
148        set: &Set<K>,
149    ) -> IndexedVarBuilder<'a, K> {
150        IndexedVarBuilder {
151            model: self,
152            base_name: name.into(),
153            keys: set.iter().collect(),
154            axes: set.axes().map(Box::from),
155            lb: f64::NEG_INFINITY,
156            ub: f64::INFINITY,
157            lb_by: None,
158            ub_by: None,
159            domain: Domain::Real,
160            _k: PhantomData,
161        }
162    }
163
164    pub fn variable_id(&self, name: &str) -> Option<VarId> {
165        self.var_names.borrow().get(name).copied()
166    }
167
168    pub fn variables(&self) -> Ref<'_, Vec<Variable>> {
169        self.variables.borrow()
170    }
171
172    pub fn arena(&self) -> Ref<'_, ExprArena> {
173        self.arena.borrow()
174    }
175
176    pub fn num_variables(&self) -> usize {
177        self.variables.borrow().len()
178    }
179
180    /// Fix a single-variable expression to `value`.
181    /// Convenience over [`Self::fix_var`] for handles from [`Model::var`] or
182    /// [`crate::IndexedVar`] indexing.
183    ///
184    /// # Panics
185    ///
186    /// Panics if `e` is not a bare variable handle.
187    pub fn fix(&self, e: Expr<'_>, value: f64) {
188        let id = e.var_id().expect("Model::fix expects a single-variable expression");
189        self.fix_var(id, value);
190    }
191
192    /// Fix variable `id` to `value` by setting `lb = ub = value`.
193    pub fn fix_var(&self, id: VarId, value: f64) {
194        let mut vars = self.variables.borrow_mut();
195        let v = &mut vars[id.index()];
196        v.lb = value;
197        v.ub = value;
198    }
199
200    /// Set the initial (warm-start) value of a single-variable expression.
201    /// The macro API has no bound-style syntax for warm starts, so this is the
202    /// supported way to seed `variable!`-declared variables.
203    ///
204    /// # Panics
205    ///
206    /// Panics if `e` is not a bare variable handle.
207    pub fn set_initial(&self, e: Expr<'_>, value: f64) {
208        let id = e.var_id().expect("Model::set_initial expects a single-variable expression");
209        self.variables.borrow_mut()[id.index()].initial = Some(value);
210    }
211
212    /// Restore bounds on variable `id`. Pass `f64::NEG_INFINITY` / `f64::INFINITY`
213    /// to restore an unbounded direction.
214    pub fn unfix_var(&self, id: VarId, lb: f64, ub: f64) {
215        let mut vars = self.variables.borrow_mut();
216        let v = &mut vars[id.index()];
217        v.lb = lb;
218        v.ub = ub;
219    }
220
221    // Parameters
222
223    /// Register a named scalar parameter initialized to `value`, returning an
224    /// [`Expr`] handle that references it symbolically.
225    ///
226    /// A parameter behaves like a constant coefficient (`param * var` is linear),
227    /// but stays symbolic in the expression tree so it can be re-bound with
228    /// [`Self::set_param`] / [`Self::set_param_id`] between solves without
229    /// rebuilding the model.
230    ///
231    /// # Panics
232    ///
233    /// Panics if a parameter with the same name is already registered.
234    #[deprecated(
235        since = "0.3.0",
236        note = "use the `param!` macro, the builder API is scheduled for removal in 0.4.0"
237    )]
238    pub fn param<'a>(&'a self, name: impl Into<SmolStr>, value: f64) -> Expr<'a> {
239        self.__param(name, value)
240    }
241
242    /// Macro-facing entry point behind [`Self::param`]. Not part of the stable
243    /// public API; use the `param!` macro instead.
244    #[doc(hidden)]
245    pub fn __param<'a>(&'a self, name: impl Into<SmolStr>, value: f64) -> Expr<'a> {
246        let name = name.into();
247        assert!(
248            !self.param_names.borrow().contains_key(&name),
249            "parameter name {name:?} already registered"
250        );
251        let (id, node) = {
252            let mut a = self.arena.borrow_mut();
253            let id = a.new_param(value);
254            (id, a.param(id))
255        };
256        self.parameters.borrow_mut().push(Parameter { id, name: name.clone() });
257        self.param_names.borrow_mut().insert(name, id);
258        *self.cached_kind.borrow_mut() = None;
259        Expr::new(node, &self.arena)
260    }
261
262    /// Re-bind the parameter referenced by handle `p` to `value`.
263    ///
264    /// # Panics
265    ///
266    /// Panics if `p` is not a bare parameter handle (see [`Self::param`]).
267    pub fn set_param(&self, p: Expr<'_>, value: f64) {
268        let id = p.param_id().expect("Model::set_param expects a single-parameter expression");
269        self.set_param_id(id, value);
270    }
271
272    /// Re-bind parameter `id` to `value`. Takes effect on the next solve.
273    ///
274    /// The value is stored only in the expression arena (its single source of
275    /// truth); extraction and evaluation read it from there.
276    pub fn set_param_id(&self, id: ParamId, value: f64) {
277        self.arena.borrow_mut().set_param_value(id, value);
278        *self.cached_kind.borrow_mut() = None;
279    }
280
281    /// Current value bound to parameter `id`.
282    ///
283    /// # Panics
284    ///
285    /// Panics if `id` was not produced by [`Self::param`] on this model.
286    pub fn param_value(&self, id: ParamId) -> f64 {
287        self.arena.borrow().param_value(id)
288    }
289
290    /// Current value of the parameter referenced by handle `p`, or `None` if
291    /// `p` is not a bare parameter handle.
292    pub fn param_value_of(&self, p: Expr<'_>) -> Option<f64> {
293        p.param_id().map(|id| self.param_value(id))
294    }
295
296    pub fn parameter_id(&self, name: &str) -> Option<ParamId> {
297        self.param_names.borrow().get(name).copied()
298    }
299
300    pub fn parameters(&self) -> Ref<'_, Vec<Parameter>> {
301        self.parameters.borrow()
302    }
303
304    pub fn num_parameters(&self) -> usize {
305        self.parameters.borrow().len()
306    }
307
308    // Constraints
309
310    /// Register a new constraint.
311    ///
312    /// # Panics
313    ///
314    /// Panics if a constraint with the same name is already registered, or if
315    /// the constraint count exceeds `u32::MAX`.
316    #[deprecated(
317        since = "0.3.0",
318        note = "use the `constraint!` macro, the builder API is scheduled for removal in 0.4.0"
319    )]
320    pub fn constraint(&self, name: impl Into<SmolStr>, c: ConstraintExpr<'_>) -> ConstraintId {
321        self.__add_constraint(name, c)
322    }
323
324    /// Macro-facing entry point behind [`Self::constraint`]. Not part of the
325    /// stable public API; use the `constraint!` macro instead.
326    #[doc(hidden)]
327    pub fn __add_constraint(
328        &self,
329        name: impl Into<SmolStr>,
330        c: ConstraintExpr<'_>,
331    ) -> ConstraintId {
332        let name = name.into();
333        let mut by_name = self.constraint_names.borrow_mut();
334        assert!(!by_name.contains_key(&name), "constraint name {name:?} already registered");
335        let mut all = self.constraints.borrow_mut();
336        let id = ConstraintId(u32::try_from(all.len()).expect("constraint count overflow"));
337        all.push(Constraint {
338            name: name.clone(),
339            lhs: c.lhs.id,
340            sense: c.sense,
341            rhs: c.rhs,
342            active: true,
343        });
344        by_name.insert(name, id);
345        *self.cached_kind.borrow_mut() = None;
346        id
347    }
348
349    /// Register an anonymous constraint, deriving a unique name `_c{n}` from an
350    /// internal counter. Backs the name-less form of the `constraint!` macro.
351    #[doc(hidden)]
352    pub fn __add_constraint_auto(&self, c: ConstraintExpr<'_>) -> ConstraintId {
353        // Skip over any names a user may already have taken.
354        let name = loop {
355            let n = self.auto_seq.get();
356            self.auto_seq.set(n + 1);
357            let candidate: SmolStr = format!("_c{n}").into();
358            if !self.constraint_names.borrow().contains_key(&candidate) {
359                break candidate;
360            }
361        };
362        self.__add_constraint(name, c)
363    }
364
365    /// Bulk-register constraints. Each entry is `(name, ConstraintExpr)`.
366    /// Useful with `.par_iter().map(...).collect()` style construction.
367    pub fn add_constraints<'a, I>(&'a self, items: I)
368    where
369        I: IntoIterator<Item = (SmolStr, ConstraintExpr<'a>)>,
370    {
371        for (name, c) in items {
372            self.__add_constraint(name, c);
373        }
374    }
375
376    /// Rule-style bulk constraint registration.
377    ///
378    /// The closure receives the index as a typed value `K`. Any type
379    /// implementing [`FromIndexKey`] is accepted. Built-in impls cover `i64`,
380    /// `i32`, `usize`, `String`, raw `IndexKey`, and tuples up to arity 4.
381    /// The user states the expected shape via the closure-arg annotation.
382    ///
383    /// # Example
384    /// ```ignore
385    /// // Scalar set: closure receives a usize directly.
386    /// m.add_constraints_over("upper", &i, |i: usize| x[i].le(b[i]));
387    ///
388    /// // Tuple set: destructure inline.
389    /// m.add_constraints_over("blo", &(&tasks * &events), |(t, n): (usize, usize)| {
390    ///     (b[(t, n)] - b_min[t] * w[(t, n)]).ge(0.0)
391    /// });
392    /// ```
393    #[deprecated(
394        since = "0.3.0",
395        note = "use the indexed-family form of the `constraint!` macro, the builder API is scheduled for removal in 0.4.0"
396    )]
397    pub fn add_constraints_over<'a, K, F>(&'a self, name_prefix: &str, set: &Set<K>, rule: F)
398    where
399        K: FromIndexKey,
400        F: FnMut(K) -> ConstraintExpr<'a>,
401    {
402        self.__add_constraints_over(name_prefix, set, rule);
403    }
404
405    /// Macro-facing entry point behind [`Self::add_constraints_over`]. Backs the
406    /// indexed-family form of the `constraint!` macro. Not part of the stable
407    /// public API.
408    #[doc(hidden)]
409    pub fn __add_constraints_over<'a, K, F>(&'a self, name_prefix: &str, set: &Set<K>, mut rule: F)
410    where
411        K: FromIndexKey,
412        F: FnMut(K) -> ConstraintExpr<'a>,
413    {
414        for key in set {
415            let typed = K::from_index_key(&key);
416            let c = rule(typed);
417            let name: SmolStr = format_index_name(name_prefix, &key).into();
418            self.__add_constraint(name, c);
419        }
420    }
421
422    /// Macro-facing entry point for a two-sided range family.
423    #[doc(hidden)]
424    pub fn __add_range_constraints_over<'a, K, F>(&'a self, name: &str, set: &Set<K>, mut rule: F)
425    where
426        K: FromIndexKey,
427        F: FnMut(K) -> (ConstraintExpr<'a>, ConstraintExpr<'a>),
428    {
429        let lo_prefix = format!("{name}_lo");
430        let hi_prefix = format!("{name}_hi");
431        for key in set {
432            let (lo, hi) = rule(K::from_index_key(&key));
433            self.__add_constraint(format_index_name(&lo_prefix, &key), lo);
434            self.__add_constraint(format_index_name(&hi_prefix, &key), hi);
435        }
436    }
437
438    pub fn constraints(&self) -> Ref<'_, Vec<Constraint>> {
439        self.constraints.borrow()
440    }
441
442    pub fn num_constraints(&self) -> usize {
443        self.constraints.borrow().len()
444    }
445
446    pub fn constraint_id(&self, name: &str) -> Option<ConstraintId> {
447        self.constraint_names.borrow().get(name).copied()
448    }
449
450    // Objective
451
452    #[deprecated(
453        since = "0.3.0",
454        note = "use `objective!(m, Min, ..)`, the builder API is scheduled for removal in 0.4.0"
455    )]
456    pub fn minimize(&self, expr: Expr<'_>) {
457        self.__minimize(expr);
458    }
459
460    #[deprecated(
461        since = "0.3.0",
462        note = "use `objective!(m, Max, ..)`, the builder API is scheduled for removal in 0.4.0"
463    )]
464    pub fn maximize(&self, expr: Expr<'_>) {
465        self.__maximize(expr);
466    }
467
468    /// Macro-facing entry point behind [`Self::minimize`]. Backs `objective!(m, Min, ..)`.
469    #[doc(hidden)]
470    pub fn __minimize(&self, expr: Expr<'_>) {
471        self.set_objective(expr, ObjectiveSense::Minimize);
472    }
473
474    /// Macro-facing entry point behind [`Self::maximize`]. Backs `objective!(m, Max, ..)`.
475    #[doc(hidden)]
476    pub fn __maximize(&self, expr: Expr<'_>) {
477        self.set_objective(expr, ObjectiveSense::Maximize);
478    }
479
480    fn set_objective(&self, expr: Expr<'_>, sense: ObjectiveSense) {
481        *self.objective.borrow_mut() = Some(Objective { expr: expr.id, sense });
482        *self.cached_kind.borrow_mut() = None;
483    }
484
485    pub fn objective(&self) -> Ref<'_, Option<Objective>> {
486        self.objective.borrow()
487    }
488
489    /// Try to get a cloned copy of the objective.
490    ///
491    /// # Errors
492    ///
493    /// Returns [`Error::NoObjective`] if no objective is set on this model.
494    pub fn try_objective(&self) -> Result<Objective> {
495        self.objective.borrow().clone().ok_or(Error::NoObjective)
496    }
497
498    // Classification
499
500    /// Infer the [`ModelKind`] from current variables and expressions.
501    /// Result is cached and invalidated whenever variables, constraints, or the
502    /// objective change.
503    pub fn kind(&self) -> ModelKind {
504        if let Some(k) = *self.cached_kind.borrow() {
505            return k;
506        }
507        let arena = self.arena.borrow();
508        let has_int = self.variables.borrow().iter().any(|v| v.domain.is_integer());
509
510        // Highest expression class across the objective and every constraint
511        // determines the model class
512        let mut class = ExprClass::Linear;
513        if let Some(o) = self.objective.borrow().as_ref() {
514            class = class.max(classify(&arena, o.expr));
515        }
516        for c in self.constraints.borrow().iter() {
517            if class == ExprClass::Nonlinear {
518                break;
519            }
520            class = class.max(classify(&arena, c.lhs));
521        }
522
523        let k = match (has_int, class) {
524            (false, ExprClass::Linear) => ModelKind::LP,
525            (true, ExprClass::Linear) => ModelKind::MILP,
526            (false, ExprClass::Quadratic) => ModelKind::QP,
527            (true, ExprClass::Quadratic) => ModelKind::MIQP,
528            (false, ExprClass::Nonlinear) => ModelKind::NLP,
529            (true, ExprClass::Nonlinear) => ModelKind::MINLP,
530        };
531        *self.cached_kind.borrow_mut() = Some(k);
532        k
533    }
534}
535
536// IndexedVarBuilder
537
538/// Builder for a collection of scalar variables indexed by a [`Set`].
539///
540/// For example, `flow[i]` for `i in 0..3` registers `flow[0]`, `flow[1]`, and
541/// `flow[2]` as separate scalar variables in the model. Call `.build()` to get
542/// an [`IndexedVar`] that maps each key to its [`Expr`] handle. Bounds and
543/// domain set here apply uniformly to every scalar in the collection.
544type BoundFn<'a> = Box<dyn Fn(&IndexKey) -> f64 + 'a>;
545
546#[must_use = "IndexedVarBuilder does nothing until you call .build()"]
547pub struct IndexedVarBuilder<'a, K = IndexKey> {
548    model: &'a Model,
549    base_name: String,
550    keys: Vec<IndexKey>,
551    axes: Option<Box<[Axis]>>,
552    lb: f64,
553    ub: f64,
554    lb_by: Option<BoundFn<'a>>,
555    ub_by: Option<BoundFn<'a>>,
556    domain: Domain,
557    _k: PhantomData<fn() -> K>,
558}
559
560impl<'a, K> std::fmt::Debug for IndexedVarBuilder<'a, K> {
561    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
562        f.debug_struct("IndexedVarBuilder")
563            .field("base_name", &self.base_name)
564            .field("keys", &self.keys.len())
565            .field("lb", &self.lb)
566            .field("ub", &self.ub)
567            .field("per_key_lb", &self.lb_by.is_some())
568            .field("per_key_ub", &self.ub_by.is_some())
569            .field("domain", &self.domain)
570            .finish()
571    }
572}
573
574impl<'a, K> IndexedVarBuilder<'a, K> {
575    pub fn lb(mut self, v: f64) -> Self {
576        self.lb = v;
577        self
578    }
579    pub fn ub(mut self, v: f64) -> Self {
580        self.ub = v;
581        self
582    }
583    pub fn bounds(mut self, lb: f64, ub: f64) -> Self {
584        self.lb = lb;
585        self.ub = ub;
586        self
587    }
588    /// Per-key lower bound. Overrides [`Self::lb`] when both are set.
589    ///
590    /// The closure receives a typed index value via [`FromIndexKey`].
591    /// Annotate the argument to select the projection:
592    /// ```ignore
593    /// .lb_by(|(p, q): (String, String)| floor_for(&p, &q))
594    /// .lb_by(|i: usize| lower_bounds[i])
595    /// ```
596    pub fn lb_by<F>(mut self, f: F) -> Self
597    where
598        K: FromIndexKey,
599        F: Fn(K) -> f64 + 'a,
600    {
601        self.lb_by = Some(Box::new(move |k: &IndexKey| f(K::from_index_key(k))));
602        self
603    }
604    /// Per-key upper bound. Overrides [`Self::ub`] when both are set.
605    ///
606    /// The closure receives a typed index value via [`FromIndexKey`]; annotate
607    /// the argument to select the projection:
608    /// ```ignore
609    /// .ub_by(|(p, q): (String, String)| capacity_for(&p, &q))
610    /// .ub_by(|i: usize| upper_bounds[i])
611    /// ```
612    pub fn ub_by<F>(mut self, f: F) -> Self
613    where
614        K: FromIndexKey,
615        F: Fn(K) -> f64 + 'a,
616    {
617        self.ub_by = Some(Box::new(move |k: &IndexKey| f(K::from_index_key(k))));
618        self
619    }
620    pub fn domain(mut self, d: Domain) -> Self {
621        self.domain = d;
622        self
623    }
624    pub fn integer(mut self) -> Self {
625        self.domain = Domain::Integer;
626        self
627    }
628    pub fn binary(mut self) -> Self {
629        self.domain = Domain::Binary;
630        self.lb = 0.0;
631        self.ub = 1.0;
632        self
633    }
634
635    /// Register one scalar variable per key and return the [`IndexedVar`] handle.
636    ///
637    /// # Panics
638    /// Panics if a scalar variable name collides with one already registered.
639    pub fn build(self) -> IndexedVar<'a, K> {
640        let Self { model, base_name, keys, axes, lb, ub, lb_by, ub_by, domain, _k } = self;
641
642        let make = |key: &IndexKey| -> Expr<'a> {
643            let scalar_name: SmolStr = format_index_name(&base_name, key).into();
644            let lo = lb_by.as_ref().map_or(lb, |f| f(key));
645            let hi = ub_by.as_ref().map_or(ub, |f| f(key));
646            model.__var(scalar_name).lb(lo).ub(hi).domain(domain).build()
647        };
648
649        let storage = if let Some(axes) = axes {
650            // Dense grid: place each scalar at its computed row-major offset,
651            // so the storage cannot be silently mis-built if `Set` iteration
652            // vever changes.
653            let total = keys.len();
654            let mut data: Vec<Option<Expr<'a>>> = vec![None; total];
655            let mut kept: Vec<Option<IndexKey>> = vec![None; total];
656            for key in keys {
657                let expr = make(&key);
658                let off = grid_offset(&axes, &key).expect("dense grid key out of range");
659                data[off] = Some(expr);
660                kept[off] = Some(key);
661            }
662            let data = data.into_iter().map(|o| o.expect("dense grid had a gap")).collect();
663            let kept = kept.into_iter().map(|o| o.expect("dense grid had a gap")).collect();
664            Storage::Dense { data, keys: kept, axes }
665        } else {
666            let mut entries = FxHashMap::default();
667            for key in keys {
668                let expr = make(&key);
669                entries.insert(key, expr);
670            }
671            Storage::Sparse(entries)
672        };
673        IndexedVar { storage, _k: PhantomData }
674    }
675}
676
677fn format_index_name(base: &str, key: &IndexKey) -> String {
678    let mut out = String::with_capacity(base.len() + 4);
679    out.push_str(base);
680    out.push('[');
681    write_key_parts(&mut out, key);
682    out.push(']');
683    out
684}
685
686fn write_key_parts(out: &mut String, key: &IndexKey) {
687    use std::fmt::Write;
688    match key {
689        IndexKey::Int(i) => write!(out, "{i}").unwrap(),
690        IndexKey::Str(s) => out.push_str(s),
691        IndexKey::Tuple(parts) => {
692            for (i, p) in parts.iter().enumerate() {
693                if i > 0 {
694                    out.push(',');
695                }
696                write_key_parts(out, p);
697            }
698        }
699    }
700}
701
702/// Public render of an `IndexKey`'s textual form, used by helpers like
703/// [`Model::add_constraints_over`] to derive constraint names.
704pub fn display_index_key(key: &IndexKey) -> String {
705    let mut out = String::new();
706    write_key_parts(&mut out, key);
707    out
708}
709
710#[cfg(test)]
711// exercises the builder API directly until its 0.4.0 removal
712#[allow(deprecated)]
713mod tests {
714    use oximo_expr::extract_linear;
715
716    use super::*;
717    use crate::constraint::Relate;
718
719    #[test]
720    fn param_times_var_keeps_model_linear() {
721        let m = Model::new("p");
722        let param = m.param("param", 4.0);
723        let x = m.var("x").lb(0.0).build();
724        m.minimize(param * x);
725        assert_eq!(m.kind(), ModelKind::LP);
726    }
727
728    #[test]
729    fn param_coeff_resolves_and_rebinds() {
730        let m = Model::new("p");
731        let param = m.param("param", 4.0);
732        let x = m.var("x").lb(0.0).build();
733        let obj = param * x;
734
735        let coeff = |m: &Model| {
736            let arena = m.arena();
737            extract_linear(&arena, obj.id).expect("linear").coeffs[0].1
738        };
739        assert!((coeff(&m) - 4.0).abs() < f64::EPSILON);
740
741        m.set_param(param, 9.0);
742        assert!((coeff(&m) - 9.0).abs() < f64::EPSILON);
743        assert_eq!(m.parameter_id("param"), Some(param.param_id().unwrap()));
744    }
745
746    #[test]
747    fn param_value_reads_live_arena_value() {
748        let m = Model::new("p");
749        let param = m.param("param", 4.0);
750        let id = param.param_id().unwrap();
751        assert!((m.param_value(id) - 4.0).abs() < f64::EPSILON);
752        assert!((m.param_value_of(param).unwrap() - 4.0).abs() < f64::EPSILON);
753
754        m.set_param(param, 7.5);
755        assert!((m.param_value(id) - 7.5).abs() < f64::EPSILON);
756
757        let x = m.var("x").build();
758        assert!(m.param_value_of(x).is_none());
759    }
760
761    #[test]
762    fn set_param_invalidates_kind_cache() {
763        let m = Model::new("p");
764        let p = m.param("p", 1.0);
765        let x = m.var("x").lb(0.0).build();
766        m.constraint("c", (p * x).le(10.0));
767        assert_eq!(m.kind(), ModelKind::LP);
768        m.set_param(p, 2.0);
769        assert_eq!(m.kind(), ModelKind::LP);
770    }
771
772    #[test]
773    #[should_panic(expected = "parameter name \"dup\" already registered")]
774    fn duplicate_param_name_panics() {
775        let m = Model::new("p");
776        let _a = m.param("dup", 1.0);
777        let _b = m.param("dup", 2.0);
778    }
779}