Skip to main content

lot_sizing/
lot_sizing.rs

1//! Capacitated lot-sizing MILP.
2//!
3//! A manufacturer plans monthly production over T = 12 periods to meet known
4//! seasonal demand. Each period has a variable production cost, a fixed setup
5//! cost (only incurred when the line runs), and a per-unit holding cost.
6//! Production is bounded by a capacity limit, and a safety-stock requirement
7//! must hold at the end of the horizon.
8//!
9//! This model is a simplified single-item version of the classic capacitated
10//! lot-sizing problem, as described by Wilson et al. (2003) and other works.
11//!
12//! # Model
13//!
14//! ```text
15//! Variables
16//!   x[t] in [0, cap] - units produced in period t (continuous)
17//!   h[t] in [0, +inf) - inventory at end of period t (continuous)
18//!   s[t] in {0, 1} - 1 if and only if the production line runs in period t (binary)
19//!
20//! minimize
21//!   sum_t  prod_cost[t]*x[t] + setup_cost*s[t] + hold_cost*h[t]
22//!
23//! s.t.
24//!   h[0] - x[0] = initial_inventory - demand[0]
25//!   h[t] - h[t-1] - x[t] = -demand[t], for t >= 1
26//!   x[t] - capacity*s[t] <= 0, for all t
27//!   h[T-1] >= safety_stock
28//! ```
29//!
30//! Run with HiGHS (default):
31//! ```text
32//! cargo run --example lot_sizing
33//! ```
34//!
35//! Run with Gurobi:
36//! ```text
37//! cargo run --example lot_sizing --features gurobi
38//! ```
39//!
40//! References:
41//! Karimi, B., Fatemi Ghomi, S.M.T., Wilson, J.M.
42//! "The capacitated lot sizing problem: a review of models
43//! and algorithms", Omega, 31(5), 365-378, 2003.
44
45#![allow(clippy::many_single_char_names)]
46
47#[cfg(any(feature = "gurobi", feature = "highs"))]
48use oximo::prelude::*;
49
50#[cfg(feature = "gurobi")]
51use oximo::{GurobiOptions, solvers::Gurobi};
52
53#[cfg(all(feature = "highs", not(feature = "gurobi")))]
54use oximo::{HighsOptions, solvers::Highs};
55
56#[cfg(any(feature = "gurobi", feature = "highs"))]
57fn main() -> Result<(), Box<dyn std::error::Error>> {
58    const T: usize = 12;
59
60    let demand: [f64; T] =
61        [120.0, 90.0, 80.0, 140.0, 160.0, 200.0, 220.0, 190.0, 150.0, 130.0, 100.0, 170.0];
62    let prod_cost: [f64; T] = [5.0, 5.0, 5.0, 5.5, 6.0, 6.5, 6.5, 6.0, 5.5, 5.0, 5.0, 5.5];
63    let setup_cost = 500.0;
64    let hold_cost = 2.0;
65    let capacity = 300.0;
66    let initial_inventory = 50.0;
67    let safety_stock = 30.0;
68
69    let m = Model::new("lot_sizing");
70    let periods = Set::range(0..T);
71
72    let x = m.indexed_var("x", &periods).lb(0.0).ub(capacity).build();
73    let h = m.indexed_var("h", &periods).lb(0.0).build();
74    let s = m.indexed_var("s", &periods).binary().build();
75
76    m.constraint("inv_bal[0]", (h[0] - x[0]).eq(initial_inventory - demand[0]));
77    m.add_constraints_over("inv_bal", &periods.filter(|k| k.as_i64().unwrap() > 0), |t: usize| {
78        (h[t] - h[t - 1] - x[t]).eq(-demand[t])
79    });
80    m.add_constraints_over("setup", &periods, |t: usize| (x[t] - capacity * s[t]).le(0.0));
81    m.constraint("safety_stock", h[T - 1].ge(safety_stock));
82
83    let cost =
84        sum_over(&periods, |t: usize| prod_cost[t] * x[t] + setup_cost * s[t] + hold_cost * h[t]);
85    m.minimize(cost);
86
87    #[cfg(feature = "gurobi")]
88    let result = {
89        let opts = GurobiOptions::default()
90            .time_limit(std::time::Duration::from_secs(60))
91            .mip_gap(1e-4)
92            .verbose(true);
93        Gurobi.solve(&m, &opts)?
94    };
95
96    #[cfg(all(feature = "highs", not(feature = "gurobi")))]
97    let result = {
98        let opts = HighsOptions::default()
99            .time_limit(std::time::Duration::from_secs(60))
100            .mip_gap(1e-4)
101            .verbose(true);
102        Highs.solve(&m, &opts)?
103    };
104
105    println!("\nLot-Sizing Result");
106    println!("Status    : {:?}", result.status);
107    if let Some(obj) = result.objective {
108        println!("Total cost: {obj:.2}");
109    }
110
111    println!(
112        "\n{:<8} {:>10} {:>10} {:>8} {:>12}",
113        "Period", "Produce", "Inventory", "Active", "Period cost"
114    );
115    println!("{}", "-".repeat(55));
116
117    let mut total_check = 0.0;
118    for t in 0..T {
119        let xt = result.value_of(x[t]).unwrap_or(0.0);
120        let ht = result.value_of(h[t]).unwrap_or(0.0);
121        let st = result.value_of(s[t]).unwrap_or(0.0);
122        let period_cost = prod_cost[t] * xt + setup_cost * st + hold_cost * ht;
123        total_check += period_cost;
124        println!(
125            "{:<8} {:>10.1} {:>10.1} {:>8} {:>12.2}",
126            t + 1,
127            xt,
128            ht,
129            if (st - 1.0).abs() < 1e-6 { "Yes" } else { "No" },
130            period_cost
131        );
132    }
133    println!("{}", "-".repeat(55));
134    println!("{:<8} {:>10} {:>10} {:>8} {:>12.2}", "TOTAL", "", "", "", total_check);
135
136    Ok(())
137}
138
139#[cfg(not(any(feature = "gurobi", feature = "highs")))]
140fn main() {
141    println!("Enable at least one solver feature:");
142    println!("  cargo run --example lot_sizing                   # HiGHS (default)");
143    println!("  cargo run --example lot_sizing --features gurobi # Gurobi");
144}