Skip to main content

oximo_core/
model.rs

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