Skip to main content

ommx/instance/
setter.rs

1use super::*;
2
3fn validate_one_hot_non_empty(constraint: &crate::OneHotConstraint) -> crate::Result<()> {
4    crate::ensure!(
5        !constraint.variables.is_empty(),
6        crate::OneHotConstraintError::EmptyVariables
7    );
8    Ok(())
9}
10
11fn validate_sos1_non_empty(constraint: &crate::Sos1Constraint) -> crate::Result<()> {
12    crate::ensure!(
13        !constraint.variables.is_empty(),
14        crate::Sos1ConstraintError::EmptyVariables
15    );
16    Ok(())
17}
18
19impl Instance {
20    /// Internal helper to validate required IDs against precomputed sets.
21    ///
22    /// Mirrors the `used / fixed / dependent` disjointness invariant the
23    /// builder enforces (`builder.rs`): a constraint or objective cannot
24    /// reference a variable whose value has been pinned via
25    /// a table-owned fixed value (`fixed`), nor a variable
26    /// used as a substitution-dependency key (`dependent`).
27    fn validate_required_ids_with_sets(
28        required_ids: &VariableIDSet,
29        variable_ids: &VariableIDSet,
30        dependency_keys: &VariableIDSet,
31        fixed_ids: &VariableIDSet,
32    ) -> crate::Result<()> {
33        // Check if all required IDs are defined
34        if !required_ids.is_subset(variable_ids) {
35            let id = *required_ids.difference(variable_ids).next().unwrap();
36            crate::bail!({ ?id }, "Undefined variable ID is used: {id:?}");
37        }
38
39        // Check if any required ID is a dependent variable (used as a key in decision_variable_dependency)
40        if let Some(&id) = required_ids.intersection(dependency_keys).next() {
41            crate::bail!(
42                { ?id },
43                "Dependent variable cannot be used in objectives or constraints: {id:?}",
44            );
45        }
46
47        // Check if any required ID is a fixed variable.
48        if let Some(&id) = required_ids.intersection(fixed_ids).next() {
49            crate::bail!(
50                { ?id },
51                "Fixed variable {id:?} cannot be used in objectives or constraints",
52            );
53        }
54
55        Ok(())
56    }
57
58    /// Validate that all required variable IDs are defined in the instance
59    /// and are not dependent variables (i.e., not used as keys in
60    /// decision_variable_dependency) and are not fixed variables
61    /// (table-owned fixed value set).
62    fn validate_required_ids(&self, required_ids: VariableIDSet) -> crate::Result<()> {
63        let variable_ids: VariableIDSet = self.decision_variables.keys().cloned().collect();
64        let dependency_keys: VariableIDSet = self.decision_variable_dependency.keys().collect();
65        let fixed_ids: VariableIDSet = self
66            .fixed_decision_variable_values()
67            .keys()
68            .copied()
69            .collect();
70        Self::validate_required_ids_with_sets(
71            &required_ids,
72            &variable_ids,
73            &dependency_keys,
74            &fixed_ids,
75        )
76    }
77
78    /// Set the objective function and rebase output semantics.
79    ///
80    /// # Postconditions
81    ///
82    /// Setting the objective makes it the new active and output semantics.
83    ///
84    /// ```
85    /// use ommx::{
86    ///     coeff, linear, v1::State, ATol, DecisionVariable, Evaluate, Function,
87    ///     Instance, Sense, VariableID,
88    /// };
89    /// use std::collections::{BTreeMap, HashMap};
90    ///
91    /// let variable = VariableID::from(1);
92    /// let mut instance = Instance::builder()
93    ///     .sense(Sense::Maximize)
94    ///     .objective(Function::from(linear!(1)))
95    ///     .decision_variables(BTreeMap::from([(variable, DecisionVariable::binary())]))
96    ///     .constraints(BTreeMap::new())
97    ///     .build()
98    ///     .unwrap();
99    /// assert!(instance.convert_active_objective(Sense::Minimize));
100    ///
101    /// instance
102    ///     .set_objective(Function::from((coeff!(2.0) * linear!(1)).unwrap()))
103    ///     .unwrap();
104    /// assert_eq!(instance.sense(), Sense::Minimize);
105    /// assert!(instance.output_objective().is_none());
106    /// let state = State::from(HashMap::from([(1, 1.0)]));
107    /// let solution = instance.evaluate(&state, ATol::default()).unwrap();
108    /// assert_eq!(*solution.sense(), Some(Sense::Minimize));
109    /// assert_eq!(*solution.objective(), 2.0);
110    /// ```
111    pub fn set_objective(&mut self, objective: Function) -> crate::Result<()> {
112        // Validate that all variables in the objective are defined
113        self.validate_required_ids(objective.required_ids())?;
114        self.objective = objective;
115        self.output_objective = None;
116        Ok(())
117    }
118
119    /// Insert a new constraint with its context, picking an unused id.
120    ///
121    /// Returns the newly assigned [`ConstraintID`]. The context is
122    /// drained into the per-constraint [`ConstraintContextStore`]; pass
123    /// `ConstraintContext::default()` for an unannotated constraint.
124    ///
125    /// All variable IDs referenced by the constraint must already be
126    /// present in `decision_variables` and must not be substitution-
127    /// dependency keys, matching the validation enforced by
128    /// [`Self::insert_constraint`].
129    pub fn add_constraint(
130        &mut self,
131        constraint: Constraint,
132        context: crate::ConstraintContext,
133    ) -> crate::Result<ConstraintID> {
134        self.validate_required_ids(constraint.required_ids())?;
135        let id = self.constraint_collection.unused_id();
136        self.constraint_collection
137            .insert_active_with_context(id, constraint, context)?;
138        Ok(id)
139    }
140
141    /// Verify that the given id is a binary decision variable.
142    ///
143    /// Used at the structural positions of indicator and one-hot constraints,
144    /// where the [`Instance`] builder enforces `Kind::Binary` and the same
145    /// invariant must hold for the post-construction `add_*` setters.
146    fn require_binary_variable(&self, id: VariableID) -> crate::Result<()> {
147        let dv = self
148            .decision_variables
149            .get(&id)
150            .ok_or_else(|| crate::error!("Variable {id:?} is not defined in decision_variables"))?;
151        if dv.kind() != crate::decision_variable::Kind::Binary {
152            crate::bail!({ ?id }, "Variable {id:?} must be binary");
153        }
154        Ok(())
155    }
156
157    /// Insert a new indicator constraint with its context, picking an unused id.
158    ///
159    /// Returns the newly assigned [`crate::IndicatorConstraintID`].
160    /// Enforces the same invariants as the [`Instance`] builder:
161    /// - All variable IDs referenced by the constraint (function plus the
162    ///   indicator variable) must be present in `decision_variables` and must
163    ///   not be substitution-dependency keys.
164    /// - The indicator variable must have [`Kind::Binary`](crate::decision_variable::Kind).
165    pub fn add_indicator_constraint(
166        &mut self,
167        constraint: crate::IndicatorConstraint,
168        context: crate::ConstraintContext,
169    ) -> crate::Result<crate::IndicatorConstraintID> {
170        self.validate_required_ids(constraint.required_ids())?;
171        self.require_binary_variable(constraint.indicator_variable)?;
172        let id = self.indicator_constraint_collection.unused_id();
173        self.indicator_constraint_collection
174            .insert_active_with_context(id, constraint, context)?;
175        Ok(id)
176    }
177
178    /// Insert a new one-hot constraint with its context, picking an unused id.
179    ///
180    /// Returns the newly assigned [`crate::OneHotConstraintID`]. Enforces
181    /// the [`Instance`] builder's invariants: the one-hot set must be
182    /// non-empty and every variable in it must be defined and have
183    /// [`Kind::Binary`](crate::decision_variable::Kind).
184    pub fn add_one_hot_constraint(
185        &mut self,
186        constraint: crate::OneHotConstraint,
187        context: crate::ConstraintContext,
188    ) -> crate::Result<crate::OneHotConstraintID> {
189        validate_one_hot_non_empty(&constraint)?;
190        self.validate_required_ids(constraint.required_ids())?;
191        for var_id in &constraint.variables {
192            self.require_binary_variable(*var_id)?;
193        }
194        let id = self.one_hot_constraint_collection.unused_id();
195        self.one_hot_constraint_collection
196            .insert_active_with_context(id, constraint, context)?;
197        Ok(id)
198    }
199
200    /// Insert a new SOS1 constraint with its context, picking an unused id.
201    ///
202    /// Returns the newly assigned [`crate::Sos1ConstraintID`]. Enforces the
203    /// [`Instance`] builder's invariants: the variable set must be non-empty
204    /// and every variable must be defined in `decision_variables`.
205    pub fn add_sos1_constraint(
206        &mut self,
207        constraint: crate::Sos1Constraint,
208        context: crate::ConstraintContext,
209    ) -> crate::Result<crate::Sos1ConstraintID> {
210        validate_sos1_non_empty(&constraint)?;
211        self.validate_required_ids(constraint.required_ids())?;
212        let id = self.sos1_constraint_collection.unused_id();
213        self.sos1_constraint_collection
214            .insert_active_with_context(id, constraint, context)?;
215        Ok(id)
216    }
217
218    /// Insert a decision variable with its modeling label.
219    ///
220    /// The table key must not collide with any existing variable and must not
221    /// be a substitution-dependency key. Returns the inserted variable's id for
222    /// symmetry with `add_constraint`.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error whose chain contains
227    /// [`crate::DecisionVariableError::DuplicateID`] when `id` is already
228    /// owned by a decision variable.
229    pub fn add_decision_variable(
230        &mut self,
231        id: crate::VariableID,
232        variable: crate::DecisionVariable,
233        label: crate::DecisionVariableLabel,
234    ) -> crate::Result<crate::VariableID> {
235        if !self.decision_variables.contains_key(&id)
236            && self.decision_variable_dependency.keys().any(|k| k == id)
237        {
238            crate::bail!(
239                { ?id },
240                "Variable id {id:?} is currently used as a substitution-dependency key",
241            );
242        }
243        self.decision_variables
244            .insert(id, variable, label, None, crate::ATol::default())?;
245        Ok(id)
246    }
247
248    fn apply_validated_constraint_insert(
249        &mut self,
250        id: ConstraintID,
251        constraint: Constraint,
252    ) -> crate::Result<Option<Constraint>> {
253        if self.constraint_collection.active().contains_key(&id)
254            || self.constraint_collection.removed().contains_key(&id)
255        {
256            let old = self
257                .constraint_collection
258                .replace_row_preserving_lifecycle(id, constraint)
259                .expect("read-only membership check found this constraint");
260            Ok(Some(old))
261        } else {
262            self.constraint_collection.insert_active_with_context(
263                id,
264                constraint,
265                crate::ConstraintContext::default(),
266            )?;
267            Ok(None)
268        }
269    }
270
271    /// Insert a constraint into the instance under the given [`ConstraintID`].
272    ///
273    /// - If the constraint already exists, it will be replaced.
274    /// - If the ID is in the removed constraints, it replaces it.
275    /// - Otherwise, it adds the constraint to the instance.
276    ///
277    pub fn insert_constraint(
278        &mut self,
279        id: ConstraintID,
280        constraint: Constraint,
281    ) -> crate::Result<Option<Constraint>> {
282        // Validate that all variables in the constraints are defined
283        self.validate_required_ids(constraint.required_ids())?;
284        self.apply_validated_constraint_insert(id, constraint)
285    }
286
287    /// Insert multiple `(id, constraint)` pairs into the instance with a single validation pass.
288    ///
289    /// This is more efficient than calling [`Self::insert_constraint`] multiple times
290    /// because it validates all required variable IDs once, rather than
291    /// rebuilding the validation sets for each constraint.
292    ///
293    /// The behavior for each constraint follows the same rules as [`Self::insert_constraint`]:
294    /// - If the constraint already exists, it will be replaced.
295    /// - If the ID is in the removed constraints, it replaces it.
296    /// - Otherwise, it adds the constraint to the instance.
297    ///
298    /// # Atomicity
299    ///
300    /// This method is atomic: all constraints are validated before any insertion occurs.
301    /// If any constraint fails validation, no constraints are inserted and an error is returned.
302    ///
303    pub fn insert_constraints(
304        &mut self,
305        constraints: Vec<(ConstraintID, Constraint)>,
306    ) -> crate::Result<BTreeMap<ConstraintID, Constraint>> {
307        // Build validation sets once
308        let variable_ids: VariableIDSet = self.decision_variables.keys().cloned().collect();
309        let dependency_keys: VariableIDSet = self.decision_variable_dependency.keys().collect();
310        let fixed_ids: VariableIDSet = self
311            .fixed_decision_variable_values()
312            .keys()
313            .copied()
314            .collect();
315
316        // Validate all constraints first (atomic: fail before any insertion)
317        for (_, constraint) in &constraints {
318            let required_ids = constraint.required_ids();
319            Self::validate_required_ids_with_sets(
320                &required_ids,
321                &variable_ids,
322                &dependency_keys,
323                &fixed_ids,
324            )?;
325        }
326
327        // Insert all constraints (validation already done)
328        let mut replaced = BTreeMap::new();
329        for (id, constraint) in constraints {
330            if let Some(old_constraint) = self.apply_validated_constraint_insert(id, constraint)? {
331                replaced.insert(id, old_constraint);
332            }
333        }
334
335        Ok(replaced)
336    }
337
338    /// Returns the next available ConstraintID.
339    ///
340    /// Finds the maximum ID from both active constraints and removed constraints,
341    /// then adds 1. If there are no constraints, returns ConstraintID(0).
342    ///
343    /// Note: This method does not track which IDs have been allocated.
344    /// Consecutive calls will return the same ID until a constraint is actually added.
345    pub fn next_constraint_id(&self) -> ConstraintID {
346        let max_in_constraints = self
347            .constraints()
348            .last_key_value()
349            .map(|(id, _)| id.into_inner());
350        let max_in_removed = self
351            .removed_constraints()
352            .last_key_value()
353            .map(|(id, _)| id.into_inner());
354
355        max_in_constraints
356            .max(max_in_removed)
357            .map(|max| ConstraintID::from(max + 1))
358            .unwrap_or(ConstraintID::from(0))
359    }
360}
361
362impl ParametricInstance {
363    /// Validate that all required IDs are defined either as decision variables
364    /// or as parameters, and are not currently used as substitution-dependency
365    /// keys.
366    ///
367    /// `ParametricInstance` validation differs from
368    /// [`Instance::validate_required_ids`](Instance) by also accepting
369    /// parameter IDs — constraints in a parametric instance may reference
370    /// parameters that will be substituted later via
371    /// [`ParametricInstance::with_parameters`].
372    fn validate_required_ids(&self, required_ids: VariableIDSet) -> crate::Result<()> {
373        let variable_ids: VariableIDSet = self.decision_variables().keys().cloned().collect();
374        let parameter_ids: VariableIDSet = self.parameters().keys().cloned().collect();
375        let known_ids: VariableIDSet = variable_ids.union(&parameter_ids).cloned().collect();
376        let dependency_keys: VariableIDSet = self.decision_variable_dependency().keys().collect();
377        let fixed_ids: VariableIDSet = self
378            .fixed_decision_variable_values()
379            .keys()
380            .copied()
381            .collect();
382
383        if !required_ids.is_subset(&known_ids) {
384            let id = *required_ids.difference(&known_ids).next().unwrap();
385            crate::bail!({ ?id }, "Undefined variable ID is used: {id:?}");
386        }
387        if let Some(&id) = required_ids.intersection(&dependency_keys).next() {
388            crate::bail!(
389                { ?id },
390                "Dependent variable cannot be used in objectives or constraints: {id:?}",
391            );
392        }
393        if let Some(&id) = required_ids.intersection(&fixed_ids).next() {
394            crate::bail!(
395                { ?id },
396                "Fixed variable {id:?} cannot be used in objectives or constraints",
397            );
398        }
399        Ok(())
400    }
401
402    /// Insert a new constraint with its context, picking an unused id.
403    ///
404    /// Mirrors [`Instance::add_constraint`] for parametric instances.
405    /// Returns the newly assigned [`ConstraintID`]. The context is drained
406    /// into the per-constraint [`ConstraintContextStore`]; pass
407    /// [`ConstraintContext::default`](crate::ConstraintContext) for an
408    /// unannotated constraint.
409    ///
410    /// All IDs referenced by the constraint must already be present in either
411    /// `decision_variables` or `parameters`, and must not be substitution-
412    /// dependency keys.
413    pub fn add_constraint(
414        &mut self,
415        constraint: Constraint,
416        context: crate::ConstraintContext,
417    ) -> crate::Result<ConstraintID> {
418        self.validate_required_ids(constraint.required_ids())?;
419        let id = self.constraint_collection.unused_id();
420        self.constraint_collection
421            .insert_active_with_context(id, constraint, context)?;
422        Ok(id)
423    }
424
425    /// Validate that the given ids are real decision variables (not parameters).
426    ///
427    /// Used for *structural* positions in special constraints — the indicator
428    /// variable and the variable sets of one-hot / SOS1 — where parameter ids
429    /// would not be substitutable in a way that preserves the constraint's
430    /// semantics. Function-body ids continue to be validated through
431    /// [`Self::validate_required_ids`], which permits parameters.
432    fn require_decision_variables(&self, ids: VariableIDSet) -> crate::Result<()> {
433        let variable_ids: VariableIDSet = self.decision_variables().keys().cloned().collect();
434        if !ids.is_subset(&variable_ids) {
435            let id = *ids.difference(&variable_ids).next().unwrap();
436            if self.parameters().contains_key(&id) {
437                crate::bail!(
438                    { ?id },
439                    "Parameter id {id:?} cannot occupy a structural variable position; \
440                     structural variables in indicator / one-hot / SOS1 constraints \
441                     must be decision variables",
442                );
443            }
444            crate::bail!({ ?id }, "Undefined variable ID is used: {id:?}");
445        }
446        Ok(())
447    }
448
449    /// Verify that the given id is a binary decision variable. Mirrors
450    /// [`Instance::require_binary_variable`](Instance) for the parametric
451    /// host. Parameter ids are rejected because they are not decision
452    /// variables in the first place — the matching error message points
453    /// at the structural-position rule from
454    /// [`Self::require_decision_variables`].
455    fn require_binary_variable(&self, id: VariableID) -> crate::Result<()> {
456        let dv = self.decision_variables().get(&id).ok_or_else(|| {
457            if self.parameters().contains_key(&id) {
458                crate::error!(
459                    "Parameter id {id:?} cannot occupy a structural variable position; \
460                     it must be a binary decision variable",
461                )
462            } else {
463                crate::error!("Variable {id:?} is not defined in decision_variables")
464            }
465        })?;
466        if dv.kind() != crate::decision_variable::Kind::Binary {
467            crate::bail!({ ?id }, "Variable {id:?} must be binary");
468        }
469        Ok(())
470    }
471
472    /// Insert a new indicator constraint with its context, picking an unused id.
473    ///
474    /// Mirrors [`Instance::add_indicator_constraint`] for parametric
475    /// instances. The function body may reference either decision variables
476    /// or parameters, but the indicator variable itself must be a binary
477    /// decision variable — substitution cannot replace a structural variable
478    /// position, and the indicator semantics require `Kind::Binary`.
479    pub fn add_indicator_constraint(
480        &mut self,
481        constraint: crate::IndicatorConstraint,
482        context: crate::ConstraintContext,
483    ) -> crate::Result<crate::IndicatorConstraintID> {
484        // Structural position: the indicator variable must be a binary
485        // decision variable, not a parameter or a non-binary variable.
486        self.require_binary_variable(constraint.indicator_variable)?;
487        // `validate_required_ids` (variables ∪ parameters minus dependency
488        // keys) is allowed to see the indicator variable here too: the
489        // variable-vs-parameter axis is already enforced above, so this
490        // call's only added contribution for the indicator variable is the
491        // dependency-key check.
492        self.validate_required_ids(constraint.required_ids())?;
493        let id = self.indicator_constraint_collection.unused_id();
494        self.indicator_constraint_collection
495            .insert_active_with_context(id, constraint, context)?;
496        Ok(id)
497    }
498
499    /// Insert a new one-hot constraint with its context, picking an unused id.
500    ///
501    /// The one-hot set must be non-empty. All variables in the set are
502    /// structural and must be binary decision variables (parameter ids and
503    /// non-binary kinds are rejected). Dependency keys are also rejected.
504    pub fn add_one_hot_constraint(
505        &mut self,
506        constraint: crate::OneHotConstraint,
507        context: crate::ConstraintContext,
508    ) -> crate::Result<crate::OneHotConstraintID> {
509        validate_one_hot_non_empty(&constraint)?;
510        for var_id in &constraint.variables {
511            self.require_binary_variable(*var_id)?;
512        }
513        self.validate_required_ids(constraint.required_ids())?;
514        let id = self.one_hot_constraint_collection.unused_id();
515        self.one_hot_constraint_collection
516            .insert_active_with_context(id, constraint, context)?;
517        Ok(id)
518    }
519
520    /// Insert a new SOS1 constraint with its context, picking an unused id.
521    ///
522    /// All variables in the SOS1 set are structural and must be decision
523    /// variables (parameter ids are rejected). The set must be non-empty.
524    /// Dependency keys are also rejected. Unlike one-hot, SOS1 does not
525    /// require `Kind::Binary`.
526    pub fn add_sos1_constraint(
527        &mut self,
528        constraint: crate::Sos1Constraint,
529        context: crate::ConstraintContext,
530    ) -> crate::Result<crate::Sos1ConstraintID> {
531        validate_sos1_non_empty(&constraint)?;
532        let required_ids = constraint.required_ids();
533        self.require_decision_variables(required_ids.clone())?;
534        self.validate_required_ids(required_ids)?;
535        let id = self.sos1_constraint_collection.unused_id();
536        self.sos1_constraint_collection
537            .insert_active_with_context(id, constraint, context)?;
538        Ok(id)
539    }
540
541    /// Insert a decision variable with its modeling label.
542    ///
543    /// The table key must not collide with any existing decision
544    /// variable, parameter, or substitution-dependency key.
545    ///
546    /// # Errors
547    ///
548    /// Returns an error whose chain contains
549    /// [`crate::DecisionVariableError::DuplicateID`] when `id` is already
550    /// owned by a decision variable, or
551    /// [`crate::ParameterIDCollision`] when it is already owned by a
552    /// parameter.
553    pub fn add_decision_variable(
554        &mut self,
555        id: crate::VariableID,
556        variable: crate::DecisionVariable,
557        label: crate::DecisionVariableLabel,
558    ) -> crate::Result<crate::VariableID> {
559        let has_decision_variable = self.decision_variables().contains_key(&id);
560        if !has_decision_variable && self.parameters().contains_key(&id) {
561            return Err(crate::ParameterIDCollision { id }.into());
562        }
563        if !has_decision_variable && self.decision_variable_dependency().keys().any(|k| k == id) {
564            crate::bail!(
565                { ?id },
566                "Variable id {id:?} is currently used as a substitution-dependency key",
567            );
568        }
569        self.decision_variables
570            .insert(id, variable, label, None, crate::ATol::default())?;
571        Ok(id)
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578    use crate::{
579        assign, coeff,
580        constraint::{Constraint, ConstraintID},
581        linear,
582        polynomial_base::{Linear, LinearMonomial},
583        DecisionVariable, Function, ParameterIDCollision, ParameterTable, Substitute, VariableID,
584    };
585
586    use maplit::btreemap;
587    use std::collections::BTreeSet;
588
589    fn empty_one_hot_constraint() -> crate::OneHotConstraint {
590        crate::OneHotConstraint {
591            variables: BTreeSet::new(),
592            stage: crate::OneHotCreatedData,
593        }
594    }
595
596    fn empty_sos1_constraint() -> crate::Sos1Constraint {
597        crate::Sos1Constraint {
598            variables: BTreeSet::new(),
599            stage: crate::Sos1CreatedData,
600        }
601    }
602
603    #[test]
604    fn add_decision_variable_preserves_duplicate_id_signal() {
605        let id = VariableID::from(7);
606        let mut instance = Instance::new(
607            Sense::Minimize,
608            Function::Zero,
609            btreemap! { id => DecisionVariable::binary() },
610            BTreeMap::new(),
611        )
612        .unwrap();
613
614        let error = instance
615            .add_decision_variable(
616                id,
617                DecisionVariable::binary(),
618                crate::DecisionVariableLabel::default(),
619            )
620            .unwrap_err();
621
622        assert!(matches!(
623            error.downcast_ref::<crate::DecisionVariableError>(),
624            Some(crate::DecisionVariableError::DuplicateID { id: duplicate }) if *duplicate == id
625        ));
626        assert_eq!(instance.decision_variables().len(), 1);
627    }
628
629    #[test]
630    fn parametric_add_decision_variable_preserves_substituted_duplicate_signal() {
631        let independent = VariableID::from(0);
632        let dependent = VariableID::from(7);
633        let instance = ParametricInstance::new(
634            Sense::Minimize,
635            Function::from(linear!(dependent.into_inner())),
636            btreemap! {
637                independent => DecisionVariable::binary(),
638                dependent => DecisionVariable::binary(),
639            },
640            ParameterTable::default(),
641            BTreeMap::new(),
642        )
643        .unwrap();
644        let mut instance = instance
645            .substitute_one(
646                dependent,
647                &Function::from(linear!(independent.into_inner())),
648            )
649            .unwrap();
650
651        let error = instance
652            .add_decision_variable(
653                dependent,
654                DecisionVariable::binary(),
655                crate::DecisionVariableLabel::default(),
656            )
657            .unwrap_err();
658
659        assert!(matches!(
660            error.downcast_ref::<crate::DecisionVariableError>(),
661            Some(crate::DecisionVariableError::DuplicateID { id }) if id == &dependent
662        ));
663        assert_eq!(instance.decision_variables().len(), 2);
664    }
665
666    #[test]
667    fn add_decision_variable_preserves_parameter_id_collision_signal() {
668        let parameter = VariableID::from(100);
669        let mut instance = ParametricInstance::new(
670            Sense::Minimize,
671            Function::Zero,
672            BTreeMap::new(),
673            ParameterTable::from_ids(BTreeSet::from([parameter])),
674            BTreeMap::new(),
675        )
676        .unwrap();
677
678        let error = instance
679            .add_decision_variable(
680                parameter,
681                DecisionVariable::binary(),
682                crate::DecisionVariableLabel::default(),
683            )
684            .unwrap_err();
685
686        assert!(matches!(
687            error.downcast_ref::<ParameterIDCollision>(),
688            Some(ParameterIDCollision { id }) if *id == parameter
689        ));
690        assert!(instance.decision_variables().is_empty());
691    }
692
693    #[test]
694    fn test_insert_constraint_success() {
695        // Create a simple instance with two decision variables
696        let decision_variables = btreemap! {
697            VariableID::from(1) => DecisionVariable::binary(),
698            VariableID::from(2) => DecisionVariable::binary(),
699        };
700
701        let objective = linear!(1) + coeff!(1.0);
702
703        let mut instance = Instance::new(
704            Sense::Minimize,
705            objective.into(),
706            decision_variables,
707            BTreeMap::new(),
708        )
709        .unwrap();
710
711        // Insert a new constraint using variable 1
712        let constraint = Constraint::equal_to_zero((linear!(1) + coeff!(2.0)).into());
713        let result = instance
714            .insert_constraint(ConstraintID::from(10), constraint.clone())
715            .unwrap();
716
717        // Should return None since no constraint with ID 10 existed before
718        assert!(result.is_none());
719        assert_eq!(instance.constraints().len(), 1);
720        assert_eq!(
721            instance.constraints().get(&ConstraintID::from(10)),
722            Some(&constraint)
723        );
724    }
725
726    #[test]
727    fn test_insert_constraint_replace_existing() {
728        // Create instance with one constraint
729        let mut decision_variables = BTreeMap::new();
730        decision_variables.insert(VariableID::from(1), DecisionVariable::binary());
731        decision_variables.insert(VariableID::from(2), DecisionVariable::binary());
732
733        let objective = Function::Linear(Linear::single_term(
734            LinearMonomial::Variable(VariableID::from(1)),
735            coeff!(1.0),
736        ));
737
738        let mut constraints = BTreeMap::new();
739        let original_constraint = Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into());
740        constraints.insert(ConstraintID::from(5), original_constraint.clone());
741
742        let mut instance =
743            Instance::new(Sense::Minimize, objective, decision_variables, constraints).unwrap();
744
745        // Insert a new constraint with the same ID but using variable 2
746        let new_constraint = Constraint::equal_to_zero((linear!(2) + coeff!(1.0)).into());
747        let result = instance
748            .insert_constraint(ConstraintID::from(5), new_constraint.clone())
749            .unwrap();
750
751        // Should return the old constraint that was replaced
752        assert_eq!(result, Some(original_constraint));
753        assert_eq!(instance.constraints().len(), 1);
754        assert_eq!(
755            instance.constraints().get(&ConstraintID::from(5)),
756            Some(&new_constraint)
757        );
758    }
759
760    #[test]
761    fn test_insert_constraint_undefined_variable() {
762        // Create instance with only variable 1 and 2
763        let mut decision_variables = BTreeMap::new();
764        decision_variables.insert(VariableID::from(1), DecisionVariable::binary());
765        decision_variables.insert(VariableID::from(2), DecisionVariable::binary());
766
767        let objective = Function::Linear(Linear::single_term(
768            LinearMonomial::Variable(VariableID::from(1)),
769            coeff!(1.0),
770        ));
771
772        let mut instance = Instance::new(
773            Sense::Minimize,
774            objective,
775            decision_variables,
776            BTreeMap::new(),
777        )
778        .unwrap();
779
780        // Try to insert constraint using undefined variable 999
781        let constraint = Constraint::equal_to_zero((linear!(999) + coeff!(1.0)).into());
782        let result = instance.insert_constraint(ConstraintID::from(1), constraint);
783
784        // Should fail with undefined variable error
785        assert!(result.is_err());
786        let err = result.unwrap_err();
787        assert_eq!(
788            err.to_string(),
789            "Undefined variable ID is used: VariableID(999)"
790        );
791        // Ensure no constraint was added
792        assert_eq!(instance.constraints().len(), 0);
793    }
794
795    #[test]
796    fn test_insert_constraint_multiple_operations() {
797        // Test multiple insertions and replacements
798        let mut decision_variables = BTreeMap::new();
799        decision_variables.insert(VariableID::from(1), DecisionVariable::binary());
800        decision_variables.insert(VariableID::from(2), DecisionVariable::binary());
801        decision_variables.insert(VariableID::from(3), DecisionVariable::binary());
802
803        let objective = Function::Linear(Linear::single_term(
804            LinearMonomial::Variable(VariableID::from(1)),
805            coeff!(1.0),
806        ));
807
808        let mut instance = Instance::new(
809            Sense::Minimize,
810            objective,
811            decision_variables,
812            BTreeMap::new(),
813        )
814        .unwrap();
815
816        // Insert multiple constraints
817        let constraint1 = Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into());
818        let constraint2 = Constraint::equal_to_zero((linear!(2) + coeff!(1.0)).into());
819        let constraint3 = Constraint::equal_to_zero((linear!(3) + coeff!(1.0)).into());
820
821        assert!(instance
822            .insert_constraint(ConstraintID::from(1), constraint1.clone())
823            .unwrap()
824            .is_none());
825        assert!(instance
826            .insert_constraint(ConstraintID::from(2), constraint2.clone())
827            .unwrap()
828            .is_none());
829        assert!(instance
830            .insert_constraint(ConstraintID::from(3), constraint3.clone())
831            .unwrap()
832            .is_none());
833        assert_eq!(instance.constraints().len(), 3);
834
835        // Replace constraint 2 with new one
836        let new_constraint2 = Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into());
837        let replaced = instance
838            .insert_constraint(ConstraintID::from(2), new_constraint2.clone())
839            .unwrap();
840        assert_eq!(replaced, Some(constraint2));
841        assert_eq!(instance.constraints().len(), 3);
842        assert_eq!(
843            instance.constraints().get(&ConstraintID::from(2)),
844            Some(&new_constraint2)
845        );
846    }
847
848    #[test]
849    fn test_insert_constraint_with_dependency_key() {
850        // Create instance with decision variables and dependency
851        let decision_variables = btreemap! {
852            VariableID::from(1) => DecisionVariable::binary(),
853            VariableID::from(2) => DecisionVariable::binary(),
854            VariableID::from(3) => DecisionVariable::binary(),
855        };
856        let objective = linear!(1) + coeff!(1.0);
857        let mut instance = Instance::new(
858            Sense::Minimize,
859            objective.into(),
860            decision_variables,
861            BTreeMap::new(),
862        )
863        .unwrap();
864
865        // Add a dependency: x2 = x1 + 1
866        instance.decision_variable_dependency = assign! {
867            2 <- linear!(1) + coeff!(1.0)
868        };
869
870        // Try to insert constraint using variable 2 (which is in dependency keys)
871        let constraint = Constraint::equal_to_zero((linear!(2) + coeff!(1.0)).into());
872        let result = instance.insert_constraint(ConstraintID::from(1), constraint);
873        assert_eq!(
874            result.unwrap_err().to_string(),
875            "Dependent variable cannot be used in objectives or constraints: VariableID(2)"
876        );
877        // Ensure no constraint was added
878        assert_eq!(instance.constraints().len(), 0);
879    }
880
881    #[test]
882    fn test_add_constraint_rejects_fixed_variable() {
883        // Pin variable 2's value, then try to add a constraint that references it.
884        // The setter must reject — same rule the builder enforces (used ∩ fixed = ∅).
885        let decision_variables = btreemap! {
886            VariableID::from(1) => DecisionVariable::binary(),
887            VariableID::from(2) => DecisionVariable::binary(),
888        };
889
890        let objective = linear!(1) + coeff!(1.0);
891        let mut instance = Instance::builder()
892            .sense(Sense::Minimize)
893            .objective(objective.into())
894            .decision_variables(decision_variables)
895            .fixed_decision_variable_values(btreemap! {
896                VariableID::from(2) => 0.0,
897            })
898            .constraints(BTreeMap::new())
899            .build()
900            .unwrap();
901
902        let bad = crate::Constraint::equal_to_zero((linear!(2) + coeff!(1.0)).into());
903        let err = instance
904            .add_constraint(bad, crate::ConstraintContext::default())
905            .unwrap_err();
906        assert!(
907            err.to_string().contains("Fixed variable") && err.to_string().contains("VariableID(2)"),
908            "unexpected error: {err}"
909        );
910        assert!(instance.constraints().is_empty());
911    }
912
913    #[test]
914    fn test_add_one_hot_constraint_rejects_empty_variable_set() {
915        let mut instance = Instance::new(
916            Sense::Minimize,
917            Function::Zero,
918            BTreeMap::new(),
919            BTreeMap::new(),
920        )
921        .unwrap();
922
923        let err = instance
924            .add_one_hot_constraint(
925                empty_one_hot_constraint(),
926                crate::ConstraintContext::default(),
927            )
928            .unwrap_err();
929
930        assert!(matches!(
931            err.downcast_ref::<crate::OneHotConstraintError>(),
932            Some(crate::OneHotConstraintError::EmptyVariables)
933        ));
934        assert!(
935            err.to_string().contains("at least one variable"),
936            "unexpected error: {err}"
937        );
938        assert!(instance.one_hot_constraints().is_empty());
939    }
940
941    #[test]
942    fn test_parametric_add_one_hot_constraint_rejects_empty_variable_set() {
943        let mut instance = ParametricInstance::new(
944            Sense::Minimize,
945            Function::Zero,
946            BTreeMap::new(),
947            crate::ParameterTable::default(),
948            BTreeMap::new(),
949        )
950        .unwrap();
951
952        let err = instance
953            .add_one_hot_constraint(
954                empty_one_hot_constraint(),
955                crate::ConstraintContext::default(),
956            )
957            .unwrap_err();
958
959        assert!(matches!(
960            err.downcast_ref::<crate::OneHotConstraintError>(),
961            Some(crate::OneHotConstraintError::EmptyVariables)
962        ));
963        assert!(
964            err.to_string().contains("at least one variable"),
965            "unexpected error: {err}"
966        );
967        assert!(instance.one_hot_constraints().is_empty());
968    }
969
970    #[test]
971    fn test_add_sos1_constraint_rejects_empty_variable_set() {
972        let mut instance = Instance::new(
973            Sense::Minimize,
974            Function::Zero,
975            BTreeMap::new(),
976            BTreeMap::new(),
977        )
978        .unwrap();
979
980        let err = instance
981            .add_sos1_constraint(empty_sos1_constraint(), crate::ConstraintContext::default())
982            .unwrap_err();
983
984        assert!(matches!(
985            err.downcast_ref::<crate::Sos1ConstraintError>(),
986            Some(crate::Sos1ConstraintError::EmptyVariables)
987        ));
988        assert!(instance.sos1_constraints().is_empty());
989    }
990
991    #[test]
992    fn test_parametric_add_sos1_constraint_rejects_empty_variable_set() {
993        let mut instance = ParametricInstance::new(
994            Sense::Minimize,
995            Function::Zero,
996            BTreeMap::new(),
997            crate::ParameterTable::default(),
998            BTreeMap::new(),
999        )
1000        .unwrap();
1001
1002        let err = instance
1003            .add_sos1_constraint(empty_sos1_constraint(), crate::ConstraintContext::default())
1004            .unwrap_err();
1005
1006        assert!(matches!(
1007            err.downcast_ref::<crate::Sos1ConstraintError>(),
1008            Some(crate::Sos1ConstraintError::EmptyVariables)
1009        ));
1010        assert!(instance.sos1_constraints().is_empty());
1011    }
1012
1013    #[test]
1014    fn test_set_objective_with_dependency_key() {
1015        // Create instance with decision variables and dependency
1016        let decision_variables = btreemap! {
1017            VariableID::from(1) => DecisionVariable::binary(),
1018            VariableID::from(2) => DecisionVariable::binary(),
1019        };
1020        let objective = linear!(1) + coeff!(1.0);
1021        let mut instance = Instance::new(
1022            Sense::Minimize,
1023            objective.into(),
1024            decision_variables,
1025            BTreeMap::new(),
1026        )
1027        .unwrap();
1028
1029        // Add a dependency: x2 = x1 + 1
1030        instance.decision_variable_dependency = assign! {
1031            2 <- linear!(1) + coeff!(1.0)
1032        };
1033
1034        // Try to set objective using variable 2 (which is in dependency keys)
1035        let new_objective = linear!(2) + coeff!(1.0);
1036        let result = instance.set_objective(new_objective.into());
1037
1038        // Should fail with DependentVariableUsed error
1039        assert_eq!(
1040            result.unwrap_err().to_string(),
1041            "Dependent variable cannot be used in objectives or constraints: VariableID(2)"
1042        );
1043        // Ensure objective was not changed
1044        assert_eq!(instance.objective, Function::from(linear!(1) + coeff!(1.0)));
1045    }
1046
1047    #[test]
1048    fn test_insert_constraint_replace_removed_constraint() {
1049        // Create instance with one active constraint and one removed constraint
1050        let decision_variables = btreemap! {
1051            VariableID::from(1) => DecisionVariable::binary(),
1052            VariableID::from(2) => DecisionVariable::binary(),
1053        };
1054
1055        let objective = (linear!(1) + coeff!(1.0)).into();
1056        let constraints = btreemap! {
1057            ConstraintID::from(1) => Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into(),
1058            ),
1059            ConstraintID::from(2) => Constraint::equal_to_zero((linear!(2) + coeff!(2.0)).into(),
1060            ),
1061        };
1062
1063        let mut instance =
1064            Instance::new(Sense::Minimize, objective, decision_variables, constraints).unwrap();
1065        instance
1066            .relax_constraint(ConstraintID::from(2), "test".to_string(), [])
1067            .unwrap();
1068
1069        // Verify initial state
1070        assert_eq!(instance.constraints().len(), 1);
1071        assert_eq!(instance.removed_constraints().len(), 1);
1072
1073        // Insert a new constraint with the same ID as the removed constraint
1074        let new_constraint = Constraint::equal_to_zero(
1075            ((linear!(1) + linear!(2)).unwrap() + coeff!(3.0))
1076                .unwrap()
1077                .into(),
1078        );
1079        let result = instance
1080            .insert_constraint(ConstraintID::from(2), new_constraint.clone())
1081            .unwrap();
1082
1083        // Should return the old removed constraint
1084        assert_eq!(
1085            result,
1086            Some(Constraint::equal_to_zero((linear!(2) + coeff!(2.0)).into(),))
1087        );
1088
1089        assert_eq!(instance.constraints().len(), 1);
1090        assert_eq!(instance.removed_constraints().len(), 1);
1091        let (removed, _reason) = instance
1092            .removed_constraints()
1093            .get(&ConstraintID::from(2))
1094            .unwrap();
1095        assert_eq!(removed.equality, new_constraint.equality);
1096        assert_eq!(removed.stage.function, new_constraint.stage.function);
1097    }
1098
1099    #[test]
1100    fn test_insert_constraints_bulk() {
1101        // Create instance with decision variables
1102        let decision_variables = btreemap! {
1103            VariableID::from(1) => DecisionVariable::binary(),
1104            VariableID::from(2) => DecisionVariable::binary(),
1105            VariableID::from(3) => DecisionVariable::binary(),
1106        };
1107        let objective = linear!(1) + coeff!(1.0);
1108        let mut instance = Instance::new(
1109            Sense::Minimize,
1110            objective.into(),
1111            decision_variables,
1112            BTreeMap::new(),
1113        )
1114        .unwrap();
1115
1116        // Insert multiple constraints at once
1117        let constraints = vec![
1118            (
1119                ConstraintID::from(1),
1120                Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into()),
1121            ),
1122            (
1123                ConstraintID::from(2),
1124                Constraint::equal_to_zero((linear!(2) + coeff!(2.0)).into()),
1125            ),
1126            (
1127                ConstraintID::from(3),
1128                Constraint::equal_to_zero((linear!(3) + coeff!(3.0)).into()),
1129            ),
1130        ];
1131
1132        let replaced = instance.insert_constraints(constraints.clone()).unwrap();
1133
1134        // No constraints were replaced since none existed before
1135        assert!(replaced.is_empty());
1136        assert_eq!(instance.constraints().len(), 3);
1137
1138        // Verify constraints were inserted correctly
1139        for (id, constraint) in &constraints {
1140            assert_eq!(instance.constraints().get(id), Some(constraint));
1141        }
1142    }
1143
1144    #[test]
1145    fn test_insert_constraints_bulk_with_undefined_variable() {
1146        // Create instance with only variables 1 and 2
1147        let decision_variables = btreemap! {
1148            VariableID::from(1) => DecisionVariable::binary(),
1149            VariableID::from(2) => DecisionVariable::binary(),
1150        };
1151        let objective = linear!(1) + coeff!(1.0);
1152        let mut instance = Instance::new(
1153            Sense::Minimize,
1154            objective.into(),
1155            decision_variables,
1156            BTreeMap::new(),
1157        )
1158        .unwrap();
1159
1160        // Try to insert constraints where one uses undefined variable 999
1161        let constraints = vec![
1162            (
1163                ConstraintID::from(1),
1164                Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into()),
1165            ),
1166            (
1167                ConstraintID::from(2),
1168                Constraint::equal_to_zero((linear!(999) + coeff!(2.0)).into()),
1169            ),
1170            (
1171                ConstraintID::from(3),
1172                Constraint::equal_to_zero((linear!(2) + coeff!(3.0)).into()),
1173            ),
1174        ];
1175
1176        let result = instance.insert_constraints(constraints);
1177
1178        // Should fail with undefined variable error
1179        assert!(result.is_err());
1180        assert_eq!(
1181            result.unwrap_err().to_string(),
1182            "Undefined variable ID is used: VariableID(999)"
1183        );
1184        // Ensure no constraints were added (atomic operation)
1185        assert_eq!(instance.constraints().len(), 0);
1186    }
1187
1188    #[test]
1189    fn test_insert_constraints_bulk_replace_existing() {
1190        // Create instance with existing constraints
1191        let decision_variables = btreemap! {
1192            VariableID::from(1) => DecisionVariable::binary(),
1193            VariableID::from(2) => DecisionVariable::binary(),
1194        };
1195        let objective = linear!(1) + coeff!(1.0);
1196        let constraints = btreemap! {
1197            ConstraintID::from(1) => Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into(),
1198            ),
1199            ConstraintID::from(2) => Constraint::equal_to_zero((linear!(2) + coeff!(2.0)).into(),
1200            ),
1201        };
1202        let mut instance = Instance::new(
1203            Sense::Minimize,
1204            objective.into(),
1205            decision_variables,
1206            constraints,
1207        )
1208        .unwrap();
1209
1210        // Replace constraint 1, add constraint 3
1211        let new_constraints = vec![
1212            (
1213                ConstraintID::from(1),
1214                Constraint::equal_to_zero((linear!(2) + coeff!(10.0)).into()),
1215            ),
1216            (
1217                ConstraintID::from(3),
1218                Constraint::equal_to_zero((linear!(1) + coeff!(3.0)).into()),
1219            ),
1220        ];
1221
1222        let replaced = instance
1223            .insert_constraints(new_constraints.clone())
1224            .unwrap();
1225
1226        // Should have replaced constraint 1
1227        assert_eq!(replaced.len(), 1);
1228        assert!(replaced.contains_key(&ConstraintID::from(1)));
1229        assert_eq!(instance.constraints().len(), 3);
1230    }
1231
1232    #[test]
1233    fn test_insert_constraints_bulk_replace_removed() {
1234        // Create instance with a removed constraint
1235        let decision_variables = btreemap! {
1236            VariableID::from(1) => DecisionVariable::binary(),
1237            VariableID::from(2) => DecisionVariable::binary(),
1238        };
1239        let objective = linear!(1) + coeff!(1.0);
1240        let constraints = btreemap! {
1241            ConstraintID::from(1) => Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into(),
1242            ),
1243        };
1244        let mut instance = Instance::new(
1245            Sense::Minimize,
1246            objective.into(),
1247            decision_variables,
1248            constraints,
1249        )
1250        .unwrap();
1251
1252        // Remove constraint 1
1253        instance
1254            .relax_constraint(ConstraintID::from(1), "test".to_string(), [])
1255            .unwrap();
1256        assert_eq!(instance.constraints().len(), 0);
1257        assert_eq!(instance.removed_constraints().len(), 1);
1258
1259        // Replace the removed constraint
1260        let new_constraints = vec![(
1261            ConstraintID::from(1),
1262            Constraint::equal_to_zero((linear!(2) + coeff!(10.0)).into()),
1263        )];
1264
1265        let replaced = instance.insert_constraints(new_constraints).unwrap();
1266
1267        // Should have replaced the removed constraint
1268        assert_eq!(replaced.len(), 1);
1269        assert!(replaced.contains_key(&ConstraintID::from(1)));
1270        // Constraint is still in removed_constraints (with updated content)
1271        assert_eq!(instance.removed_constraints().len(), 1);
1272    }
1273
1274    #[test]
1275    fn test_insert_constraints_bulk_with_dependent_variable() {
1276        // Create instance with decision variables and dependency
1277        let decision_variables = btreemap! {
1278            VariableID::from(1) => DecisionVariable::binary(),
1279            VariableID::from(2) => DecisionVariable::binary(),
1280            VariableID::from(3) => DecisionVariable::binary(),
1281        };
1282        let objective = linear!(1) + coeff!(1.0);
1283        let mut instance = Instance::new(
1284            Sense::Minimize,
1285            objective.into(),
1286            decision_variables,
1287            BTreeMap::new(),
1288        )
1289        .unwrap();
1290
1291        // Add a dependency: x2 = x1 + 1
1292        instance.decision_variable_dependency = assign! {
1293            2 <- linear!(1) + coeff!(1.0)
1294        };
1295
1296        // Try to insert constraints using variable 2 (which is in dependency keys)
1297        let constraints = vec![
1298            (
1299                ConstraintID::from(1),
1300                Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into()),
1301            ),
1302            (
1303                ConstraintID::from(2),
1304                Constraint::equal_to_zero((linear!(2) + coeff!(2.0)).into()),
1305            ),
1306        ];
1307
1308        let result = instance.insert_constraints(constraints);
1309
1310        // Should fail with DependentVariableUsed error
1311        assert!(result.is_err());
1312        assert_eq!(
1313            result.unwrap_err().to_string(),
1314            "Dependent variable cannot be used in objectives or constraints: VariableID(2)"
1315        );
1316        // Ensure no constraints were added (atomic operation)
1317        assert_eq!(instance.constraints().len(), 0);
1318    }
1319
1320    #[test]
1321    fn test_next_constraint_id() {
1322        // Test basic case: empty instance
1323        let decision_variables = btreemap! {
1324            VariableID::from(1) => DecisionVariable::binary(),
1325        };
1326        let objective = (linear!(1) + coeff!(1.0)).into();
1327        let instance = Instance::new(
1328            Sense::Minimize,
1329            objective,
1330            decision_variables,
1331            BTreeMap::new(),
1332        )
1333        .unwrap();
1334        assert_eq!(instance.next_constraint_id(), ConstraintID::from(0));
1335
1336        // Test considering both active and removed constraints
1337        let decision_variables = btreemap! {
1338            VariableID::from(1) => DecisionVariable::binary(),
1339        };
1340        let objective = (linear!(1) + coeff!(1.0)).into();
1341        let constraints = btreemap! {
1342            ConstraintID::from(3) => Constraint::equal_to_zero((linear!(1) + coeff!(1.0)).into(),
1343            ),
1344            ConstraintID::from(15) => Constraint::equal_to_zero((linear!(1) + coeff!(2.0)).into(),
1345            ),
1346        };
1347        let mut instance =
1348            Instance::new(Sense::Minimize, objective, decision_variables, constraints).unwrap();
1349        instance
1350            .relax_constraint(ConstraintID::from(15), "test".to_string(), [])
1351            .unwrap();
1352
1353        // Should return 16 (max(3, 15) + 1)
1354        assert_eq!(instance.next_constraint_id(), ConstraintID::from(16));
1355    }
1356}