Skip to main content

problemreductions/models/algebraic/
ilp.rs

1//! Integer Linear Programming (ILP) intermediate representation.
2//!
3//! ILP stores integer variables with explicit possibly-unbounded intervals,
4//! sparse linear constraints, and a linear objective. The type parameters
5//! select the integer variable domain and coefficient type independently.
6
7use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry, VariantDimension};
8use crate::traits::{EvaluationError, Problem};
9use crate::types::{
10    i64_to_exact_f64, Extremum, NumericArithmeticError, NumericSize, WeightElement,
11};
12use serde::{Deserialize, Deserializer, Serialize};
13use std::fmt::Debug;
14use std::marker::PhantomData;
15
16inventory::submit! {
17    ProblemSchemaEntry {
18        name: "ILP",
19        display_name: "ILP",
20        aliases: &[],
21        dimensions: &[
22            VariantDimension::new("variable", "bool", &["bool", "i64"]),
23            VariantDimension::new("coefficient", "i64", &["i64", "f64"]),
24            VariantDimension::new("bounds", "general", &["general", "bounded"]),
25        ],
26        category: crate::registry::ProblemCategory::Algebraic,
27        module_path: module_path!(),
28        description: "Optimize a linear objective over bounded or unbounded integer variables",
29        fields: &[
30            FieldInfo { name: "variables", type_name: "Vec<IntegerVariable>", description: "Integer variable bounds; null means an unbounded side" },
31            FieldInfo { name: "constraints", type_name: "Vec<LinearConstraint<C>>", description: "Sparse finite linear constraints" },
32            FieldInfo { name: "objective", type_name: "Vec<(usize, C)>", description: "Sparse finite objective coefficients" },
33            FieldInfo { name: "sense", type_name: "ObjectiveSense", description: "Optimization direction" },
34        ],
35    }
36}
37
38/// Static certificate for a homogeneous ILP variable domain.
39pub trait VariableDomain: 'static + Clone + Debug + Send + Sync {
40    /// Name used by the registered variant dimension.
41    const NAME: &'static str;
42
43    /// Default stored variable used by homogeneous formulations.
44    fn default_variable() -> IntegerVariable;
45
46    /// Validate that stored bounds satisfy this static certificate.
47    fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError>;
48}
49
50/// Type-level requirement on the explicitly stored variable intervals.
51pub trait BoundsPolicy: 'static + Clone + Debug + Send + Sync {
52    /// Registered bounds dimension value.
53    const NAME: &'static str;
54    /// Validate the interval requirement independently of the variable domain.
55    fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError>;
56}
57
58/// Variable intervals may have infinite endpoints.
59#[derive(Debug, Clone, Copy)]
60pub struct General;
61
62impl BoundsPolicy for General {
63    const NAME: &'static str = "general";
64
65    fn validate_variables(_variables: &[IntegerVariable]) -> Result<(), ConstructionError> {
66        Ok(())
67    }
68}
69
70/// Every variable has explicit finite lower and upper bounds.
71#[derive(Debug, Clone, Copy)]
72pub struct Bounded;
73
74impl BoundsPolicy for Bounded {
75    const NAME: &'static str = "bounded";
76
77    fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError> {
78        if variables
79            .iter()
80            .any(|variable| variable.lower_bound().is_none() || variable.upper_bound().is_none())
81        {
82            return Err(ConstructionError::Conversion(
83                "bounded ILP requires finite lower and upper bounds for every variable".into(),
84            ));
85        }
86        Ok(())
87    }
88}
89
90/// Numeric domain shared by an ILP's constraints, right-hand sides, and objective.
91pub trait ILPCoefficient:
92    NumericSize + WeightElement<Sum = Self> + Copy + Debug + Send + Sync
93{
94    /// Name used by the registered variant dimension.
95    const NAME: &'static str;
96
97    /// Convert an integer variable value into this coefficient domain.
98    fn from_integer(value: i64) -> Result<Self, EvaluationError>;
99
100    /// Compare a finite evaluated row with its right-hand side.
101    fn satisfies(lhs: Self, comparison: Comparison, rhs: Self) -> bool;
102}
103
104impl ILPCoefficient for i64 {
105    const NAME: &'static str = "i64";
106
107    fn from_integer(value: i64) -> Result<Self, EvaluationError> {
108        Ok(value)
109    }
110
111    fn satisfies(lhs: Self, comparison: Comparison, rhs: Self) -> bool {
112        match comparison {
113            Comparison::Le => lhs <= rhs,
114            Comparison::Ge => lhs >= rhs,
115            Comparison::Eq => lhs == rhs,
116        }
117    }
118}
119
120impl ILPCoefficient for f64 {
121    const NAME: &'static str = "f64";
122
123    fn from_integer(value: i64) -> Result<Self, EvaluationError> {
124        i64_to_exact_f64(value).map_err(|_| {
125            EvaluationError::InexactFloatConversion(
126                "transporting an integer variable into an f64 ILP expression".into(),
127            )
128        })
129    }
130
131    fn satisfies(lhs: Self, comparison: Comparison, rhs: Self) -> bool {
132        let tolerance = 1e-9 * lhs.abs().max(rhs.abs()).max(1.0);
133        match comparison {
134            Comparison::Le => lhs <= rhs + tolerance,
135            Comparison::Ge => lhs >= rhs - tolerance,
136            Comparison::Eq => (lhs - rhs).abs() <= tolerance,
137        }
138    }
139}
140
141impl VariableDomain for bool {
142    const NAME: &'static str = "bool";
143
144    fn default_variable() -> IntegerVariable {
145        IntegerVariable::binary()
146    }
147
148    fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError> {
149        if variables
150            .iter()
151            .any(|variable| variable.lower_bound != Some(0) || variable.upper_bound != Some(1))
152        {
153            return Err(ConstructionError::Conversion(
154                "binary ILP variables must have bounds [0, 1]".into(),
155            ));
156        }
157        Ok(())
158    }
159}
160
161impl VariableDomain for i64 {
162    const NAME: &'static str = "i64";
163
164    fn default_variable() -> IntegerVariable {
165        IntegerVariable::nonnegative()
166    }
167
168    fn validate_variables(_variables: &[IntegerVariable]) -> Result<(), ConstructionError> {
169        Ok(())
170    }
171}
172
173/// Bounds of one mathematical integer variable.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
175pub struct IntegerVariable {
176    lower_bound: Option<i64>,
177    upper_bound: Option<i64>,
178}
179
180impl<'de> Deserialize<'de> for IntegerVariable {
181    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182    where
183        D: Deserializer<'de>,
184    {
185        #[derive(Deserialize)]
186        struct Bounds {
187            lower_bound: Option<i64>,
188            upper_bound: Option<i64>,
189        }
190
191        let bounds = Bounds::deserialize(deserializer)?;
192        Self::new(bounds.lower_bound, bounds.upper_bound).map_err(serde::de::Error::custom)
193    }
194}
195
196impl IntegerVariable {
197    /// Construct an integer variable. `None` denotes the corresponding
198    /// infinite bound.
199    pub fn new(
200        lower_bound: Option<i64>,
201        upper_bound: Option<i64>,
202    ) -> Result<Self, ConstructionError> {
203        if lower_bound
204            .zip(upper_bound)
205            .is_some_and(|(lower, upper)| lower > upper)
206        {
207            return Err(ConstructionError::Conversion(
208                "integer variable lower bound exceeds its upper bound".into(),
209            ));
210        }
211        Ok(Self {
212            lower_bound,
213            upper_bound,
214        })
215    }
216
217    /// A binary integer variable in `[0, 1]`.
218    pub const fn binary() -> Self {
219        Self {
220            lower_bound: Some(0),
221            upper_bound: Some(1),
222        }
223    }
224
225    /// A non-negative integer variable in `[0, +∞)`.
226    pub const fn nonnegative() -> Self {
227        Self {
228            lower_bound: Some(0),
229            upper_bound: None,
230        }
231    }
232
233    /// A free integer variable in `(-∞, +∞)`.
234    pub const fn free() -> Self {
235        Self {
236            lower_bound: None,
237            upper_bound: None,
238        }
239    }
240
241    /// Finite lower bound, or `None` for negative infinity.
242    pub const fn lower_bound(self) -> Option<i64> {
243        self.lower_bound
244    }
245
246    /// Finite upper bound, or `None` for positive infinity.
247    pub const fn upper_bound(self) -> Option<i64> {
248        self.upper_bound
249    }
250
251    /// Whether a mathematical value belongs to this interval.
252    pub fn contains(self, value: i64) -> bool {
253        self.lower_bound.is_none_or(|lower| value >= lower)
254            && self.upper_bound.is_none_or(|upper| value <= upper)
255    }
256}
257
258/// Comparison operator for a linear constraint.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
260pub enum Comparison {
261    /// Less than or equal (`<=`).
262    Le,
263    /// Greater than or equal (`>=`).
264    Ge,
265    /// Equal (`==`).
266    Eq,
267}
268
269/// One sparse linear constraint.
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub struct LinearConstraint<C = i64> {
272    terms: Vec<(usize, C)>,
273    comparison: Comparison,
274    rhs: C,
275}
276
277impl<C> LinearConstraint<C> {
278    fn new(terms: Vec<(usize, C)>, comparison: Comparison, rhs: C) -> Self {
279        Self {
280            terms,
281            comparison,
282            rhs,
283        }
284    }
285
286    /// Create a less-than-or-equal constraint.
287    pub fn le(terms: Vec<(usize, C)>, rhs: C) -> Self {
288        Self::new(terms, Comparison::Le, rhs)
289    }
290
291    /// Create a greater-than-or-equal constraint.
292    pub fn ge(terms: Vec<(usize, C)>, rhs: C) -> Self {
293        Self::new(terms, Comparison::Ge, rhs)
294    }
295
296    /// Create an equality constraint.
297    pub fn eq(terms: Vec<(usize, C)>, rhs: C) -> Self {
298        Self::new(terms, Comparison::Eq, rhs)
299    }
300
301    /// Canonical sparse row terms.
302    pub fn terms(&self) -> &[(usize, C)] {
303        &self.terms
304    }
305
306    /// Row comparison operator.
307    pub const fn comparison(&self) -> Comparison {
308        self.comparison
309    }
310
311    /// Row right-hand side.
312    pub fn variables(&self) -> impl Iterator<Item = usize> + '_ {
313        self.terms.iter().map(|&(variable, _)| variable)
314    }
315}
316
317impl<C: ILPCoefficient> LinearConstraint<C> {
318    /// Stored right-hand side.
319    pub fn rhs(&self) -> C {
320        self.rhs
321    }
322
323    /// Evaluate the left-hand side in the coefficient domain.
324    pub fn evaluate_lhs(&self, values: &[i64]) -> Result<C, EvaluationError> {
325        self.terms
326            .iter()
327            .try_fold(C::zero(), |sum, &(variable, coefficient)| {
328                let value = values.get(variable).copied().ok_or_else(|| {
329                    EvaluationError::InvalidConfiguration(format!(
330                        "an ILP constraint references variable {variable}, but the assignment has {} values",
331                        values.len()
332                    ))
333                })?;
334                let value = C::from_integer(value)?;
335                let product = C::checked_mul_sum(
336                    coefficient,
337                    value,
338                    "multiplying a term in an ILP constraint",
339                )?;
340                C::checked_add_to_sum(sum, product, "summing an ILP constraint")
341            })
342    }
343
344    /// Check whether this row is satisfied.
345    pub fn is_satisfied(&self, values: &[i64]) -> Result<bool, EvaluationError> {
346        let lhs = self.evaluate_lhs(values)?;
347        Ok(C::satisfies(lhs, self.comparison, self.rhs))
348    }
349}
350
351/// Optimization direction.
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
353pub enum ObjectiveSense {
354    /// Maximize the objective.
355    Maximize,
356    /// Minimize the objective.
357    Minimize,
358}
359
360/// Integer Linear Programming model.
361#[derive(Debug, Clone, Serialize)]
362pub struct ILP<V: VariableDomain = bool, C: ILPCoefficient = i64, B: BoundsPolicy = General> {
363    variables: Vec<IntegerVariable>,
364    constraints: Vec<LinearConstraint<C>>,
365    objective: Vec<(usize, C)>,
366    sense: ObjectiveSense,
367    #[serde(skip)]
368    marker: PhantomData<(V, B)>,
369}
370
371#[derive(Deserialize)]
372struct ILPData<C> {
373    variables: Vec<IntegerVariable>,
374    constraints: Vec<LinearConstraint<C>>,
375    objective: Vec<(usize, C)>,
376    sense: ObjectiveSense,
377}
378
379impl<'de, V, C, B> Deserialize<'de> for ILP<V, C, B>
380where
381    V: VariableDomain,
382    C: ILPCoefficient + Deserialize<'de>,
383    B: BoundsPolicy,
384{
385    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
386    where
387        D: Deserializer<'de>,
388    {
389        let data = ILPData::<C>::deserialize(deserializer)?;
390        Self::with_variables(data.variables, data.constraints, data.objective, data.sense)
391            .map_err(serde::de::Error::custom)
392    }
393}
394
395impl<V: VariableDomain, C: ILPCoefficient, B: BoundsPolicy> ILP<V, C, B> {
396    /// Construct a homogeneous model using the domain certificate's standard
397    /// variable interval: binary `[0, 1]` or integer `[0, +∞)`.
398    /// Bounded integer models with variables must use [`Self::with_variables`].
399    pub fn new(
400        num_variables: usize,
401        constraints: Vec<LinearConstraint<C>>,
402        objective: Vec<(usize, C)>,
403        sense: ObjectiveSense,
404    ) -> Result<Self, ConstructionError> {
405        Self::with_variables(
406            vec![V::default_variable(); num_variables],
407            constraints,
408            objective,
409            sense,
410        )
411    }
412
413    /// Construct a model with explicit possibly-unbounded variable intervals.
414    pub fn with_variables(
415        variables: Vec<IntegerVariable>,
416        constraints: Vec<LinearConstraint<C>>,
417        objective: Vec<(usize, C)>,
418        sense: ObjectiveSense,
419    ) -> Result<Self, ConstructionError> {
420        V::validate_variables(&variables)?;
421        B::validate_variables(&variables)?;
422        let num_variables = variables.len();
423        let constraints = constraints
424            .into_iter()
425            .enumerate()
426            .map(|(index, constraint)| normalize_constraint(constraint, num_variables, index))
427            .collect::<Result<_, _>>()?;
428        let objective = normalize_objective(objective, num_variables)?;
429        Ok(Self {
430            variables,
431            constraints,
432            objective,
433            sense,
434            marker: PhantomData,
435        })
436    }
437
438    /// Empty model.
439    pub fn empty() -> Self {
440        Self::new(0, vec![], vec![], ObjectiveSense::Minimize)
441            .expect("the empty ILP satisfies all construction invariants")
442    }
443
444    /// Stored variables and their bounds.
445    pub fn variables(&self) -> &[IntegerVariable] {
446        &self.variables
447    }
448
449    /// Canonical sparse constraints.
450    pub fn constraints(&self) -> &[LinearConstraint<C>] {
451        &self.constraints
452    }
453
454    /// Canonical sparse objective.
455    pub fn objective(&self) -> &[(usize, C)] {
456        &self.objective
457    }
458
459    /// Optimization direction.
460    pub const fn sense(&self) -> ObjectiveSense {
461        self.sense
462    }
463
464    /// Number of variables.
465    pub fn num_variables(&self) -> usize {
466        self.variables.len()
467    }
468
469    /// Canonical size getter alias.
470    pub fn num_vars(&self) -> usize {
471        self.num_variables()
472    }
473
474    /// Number of constraints.
475    pub fn num_constraints(&self) -> usize {
476        self.constraints.len()
477    }
478
479    /// Number of non-zero row coefficients.
480    pub fn num_nonzeros(&self) -> usize {
481        self.constraints
482            .iter()
483            .map(|constraint| constraint.terms.len())
484            .sum()
485    }
486
487    /// Smallest `h >= 1` for which every constraint coefficient, RHS and finite
488    /// variable endpoint has magnitude strictly below `2^h`.
489    ///
490    /// Measures normalized feasible-set data, excluding the objective. Infinite
491    /// endpoints are not numbers in this maximum; this parameter does not
492    /// certify boundedness. For floating coefficients it measures magnitude,
493    /// not mantissa precision.
494    pub fn max_constraint_magnitude_bits(&self) -> u64 {
495        let row_bits =
496            crate::types::max_numeric_magnitude_bits(self.constraints.iter().flat_map(|row| {
497                std::iter::once(row.rhs)
498                    .chain(row.terms.iter().map(|&(_, coefficient)| coefficient))
499            }));
500        let endpoint_bits = crate::types::max_numeric_magnitude_bits(
501            self.variables
502                .iter()
503                .flat_map(|variable| [variable.lower_bound, variable.upper_bound])
504                .flatten(),
505        );
506        row_bits.max(endpoint_bits)
507    }
508
509    /// Evaluate the objective in the coefficient domain.
510    pub fn evaluate_objective(&self, values: &[i64]) -> Result<C, EvaluationError> {
511        self.objective
512            .iter()
513            .try_fold(C::zero(), |sum, &(variable, coefficient)| {
514                let integer = values.get(variable).copied().ok_or_else(|| {
515                    EvaluationError::InvalidConfiguration(format!(
516                        "the ILP objective references variable {variable}, but the assignment has {} values",
517                        values.len()
518                    ))
519                })?;
520                let value = C::from_integer(integer)?;
521                let product = C::checked_mul_sum(
522                    coefficient,
523                    value,
524                    "multiplying a term in the ILP objective",
525                )?;
526                C::checked_add_to_sum(
527                    sum,
528                    product,
529                    "summing the ILP objective",
530                )
531            })
532    }
533
534    /// Check stored variable intervals and all rows.
535    pub fn is_feasible(&self, values: &[i64]) -> Result<bool, EvaluationError> {
536        if values.len() != self.variables.len() {
537            return Err(EvaluationError::InvalidConfiguration(
538                "variable assignment length does not match the ILP".into(),
539            ));
540        }
541        if self
542            .variables
543            .iter()
544            .zip(values)
545            .any(|(&variable, &value)| !variable.contains(value))
546        {
547            return Ok(false);
548        }
549        for constraint in &self.constraints {
550            if !constraint.is_satisfied(values)? {
551                return Ok(false);
552            }
553        }
554        Ok(true)
555    }
556}
557
558fn construction_arithmetic_error(
559    error: NumericArithmeticError,
560    context: String,
561) -> ConstructionError {
562    match error {
563        NumericArithmeticError::IntegerOverflow => ConstructionError::IntegerOverflow(context),
564        NumericArithmeticError::NonFiniteResult => ConstructionError::NonFiniteFloat(context),
565    }
566}
567
568fn normalize_constraint<C: ILPCoefficient>(
569    constraint: LinearConstraint<C>,
570    num_variables: usize,
571    constraint_index: usize,
572) -> Result<LinearConstraint<C>, ConstructionError> {
573    let mut terms = constraint.terms;
574    constraint
575        .rhs
576        .validate_element("ILP constraint right-hand side")?;
577    for &(variable, coefficient) in &terms {
578        if variable >= num_variables {
579            return Err(ConstructionError::Conversion(format!(
580                "ILP constraint {constraint_index} references variable {variable}, but the model has {num_variables} variables"
581            )));
582        }
583        coefficient.validate_element("ILP constraint coefficient")?;
584    }
585    terms.sort_by_key(|&(variable, _)| variable);
586    let mut normalized: Vec<(usize, C)> = Vec::with_capacity(terms.len());
587    for (variable, coefficient) in terms {
588        if let Some((previous_variable, previous_coefficient)) = normalized.last_mut() {
589            if *previous_variable == variable {
590                *previous_coefficient = previous_coefficient
591                    .checked_add_value(coefficient)
592                    .map_err(|error| {
593                        construction_arithmetic_error(
594                            error,
595                            format!(
596                        "merging duplicate variable {variable} in ILP constraint {constraint_index}"
597                    ),
598                        )
599                    })?;
600                continue;
601            }
602        }
603        normalized.push((variable, coefficient));
604    }
605    normalized.retain(|&(_, coefficient)| !coefficient.is_zero());
606    Ok(LinearConstraint::new(
607        normalized,
608        constraint.comparison,
609        constraint.rhs,
610    ))
611}
612
613fn normalize_objective<C: ILPCoefficient>(
614    mut objective: Vec<(usize, C)>,
615    num_variables: usize,
616) -> Result<Vec<(usize, C)>, ConstructionError> {
617    for &(variable, coefficient) in &objective {
618        if variable >= num_variables {
619            return Err(ConstructionError::Conversion(format!(
620                "ILP objective references variable {variable}, but the model has {num_variables} variables"
621            )));
622        }
623        coefficient.validate_element("ILP objective coefficient")?;
624    }
625    objective.sort_by_key(|&(variable, _)| variable);
626    let mut normalized: Vec<(usize, C)> = Vec::with_capacity(objective.len());
627    for (variable, coefficient) in objective {
628        if let Some((previous_variable, previous_coefficient)) = normalized.last_mut() {
629            if *previous_variable == variable {
630                *previous_coefficient = previous_coefficient
631                    .checked_add_value(coefficient)
632                    .map_err(|error| {
633                        construction_arithmetic_error(
634                            error,
635                            format!("merged objective coefficient of variable {variable}"),
636                        )
637                    })?;
638                continue;
639            }
640        }
641        normalized.push((variable, coefficient));
642    }
643    normalized.retain(|&(_, coefficient)| !coefficient.is_zero());
644    Ok(normalized)
645}
646
647impl<V: VariableDomain, C: ILPCoefficient, B: BoundsPolicy> Problem for ILP<V, C, B> {
648    const NAME: &'static str = "ILP";
649    type Solution = Vec<i64>;
650    type Value = Extremum<C>;
651
652    crate::problem_parameters![
653        (
654            "max_constraint_magnitude_bits",
655            max_constraint_magnitude_bits
656        ),
657        ("num_constraints", num_constraints),
658        ("num_nonzeros", num_nonzeros),
659        ("num_vars", num_vars),
660    ];
661
662    fn evaluate(&self, solution: &Self::Solution) -> Result<Self::Value, EvaluationError> {
663        if !self.is_feasible(solution)? {
664            return Ok(match self.sense {
665                ObjectiveSense::Maximize => Extremum::maximize(None),
666                ObjectiveSense::Minimize => Extremum::minimize(None),
667            });
668        }
669        let objective = self.evaluate_objective(solution)?;
670        Ok(match self.sense {
671            ObjectiveSense::Maximize => Extremum::maximize(Some(objective)),
672            ObjectiveSense::Minimize => Extremum::minimize(Some(objective)),
673        })
674    }
675
676    fn variant() -> Vec<(&'static str, &'static str)> {
677        vec![
678            ("variable", V::NAME),
679            ("coefficient", C::NAME),
680            ("bounds", B::NAME),
681        ]
682    }
683}
684
685crate::declare_variants! {
686    default ILP<bool, i64> => "2^num_vars",
687    ILP<i64, i64> => "num_vars^num_vars",
688    ILP<i64, i64, Bounded> => "num_vars^num_vars",
689    ILP<bool, f64> => "2^num_vars",
690    ILP<i64, f64> => "num_vars^num_vars",
691}
692
693#[cfg(feature = "example-db")]
694pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
695    vec![
696        crate::example_db::specs::ModelExampleSpec {
697            id: "ilp",
698            instance: Box::new(
699                ILP::<i64, i64>::new(
700                    2,
701                    vec![
702                        LinearConstraint::le(vec![(0, 1), (1, 1)], 5),
703                        LinearConstraint::le(vec![(0, 4), (1, 7)], 28),
704                    ],
705                    vec![(0, -5), (1, -6)],
706                    ObjectiveSense::Minimize,
707                )
708                .expect("canonical ILP construction must succeed"),
709            ),
710            optimal_config: serde_json::json!(vec![3, 2]),
711            optimal_value: serde_json::json!({
712                "sense": "Minimize",
713                "value": -27,
714            }),
715        },
716        crate::example_db::specs::ModelExampleSpec {
717            id: "bounded_ilp",
718            instance: Box::new(
719                ILP::<i64, i64, Bounded>::with_variables(
720                    vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()],
721                    vec![LinearConstraint::le(vec![(0, 1)], 1)],
722                    vec![(0, 3)],
723                    ObjectiveSense::Maximize,
724                )
725                .unwrap(),
726            ),
727            optimal_config: serde_json::json!([1]),
728            optimal_value: serde_json::json!({"sense": "Maximize", "value": 3}),
729        },
730    ]
731}
732
733#[cfg(test)]
734#[path = "../../unit_tests/models/algebraic/ilp.rs"]
735mod tests;