Skip to main content

problemreductions/models/misc/
production_planning.rs

1//! Production Planning problem implementation.
2//!
3//! Given per-period demands, production capacities, setup costs, production
4//! costs, inventory costs, and a total cost bound, determine whether there
5//! exists a feasible production plan that satisfies all demand without
6//! backlogging and stays within budget.
7
8use crate::registry::{CreateSpec, ProblemSchemaEntry};
9use crate::traits::Problem;
10use crate::types::Or;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "ProductionPlanning",
16        display_name: "Production Planning",
17        aliases: &[],
18        dimensions: &[],
19        category: crate::registry::ProblemCategory::Misc,
20        module_path: module_path!(),
21        description: "Determine whether a multi-period production plan can satisfy all demand within a cost bound",
22        fields: ProductionPlanningCreateSpec::FIELDS,
23    }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(try_from = "ProductionPlanningCreateSpec")]
28pub struct ProductionPlanning {
29    num_periods: usize,
30    demands: Vec<i64>,
31    capacities: Vec<i64>,
32    setup_costs: Vec<i64>,
33    production_costs: Vec<i64>,
34    inventory_costs: Vec<i64>,
35    cost_bound: i64,
36}
37
38#[derive(Debug, Deserialize, crate::CreateSpec)]
39struct ProductionPlanningCreateSpec {
40    /// Number of planning periods.
41    num_periods: usize,
42    /// Demand per period.
43    demands: Vec<i64>,
44    /// Production capacity per period.
45    capacities: Vec<i64>,
46    /// Setup cost per period.
47    setup_costs: Vec<i64>,
48    /// Per-unit production cost per period.
49    production_costs: Vec<i64>,
50    /// Per-unit inventory cost per period.
51    inventory_costs: Vec<i64>,
52    /// Total cost bound.
53    cost_bound: i64,
54}
55impl TryFrom<ProductionPlanningCreateSpec> for ProductionPlanning {
56    type Error = crate::registry::ConstructionError;
57    fn try_from(spec: ProductionPlanningCreateSpec) -> Result<Self, Self::Error> {
58        Self::try_new(
59            spec.num_periods,
60            spec.demands,
61            spec.capacities,
62            spec.setup_costs,
63            spec.production_costs,
64            spec.inventory_costs,
65            spec.cost_bound,
66        )
67    }
68}
69
70impl ProductionPlanning {
71    pub fn new(
72        num_periods: usize,
73        demands: Vec<i64>,
74        capacities: Vec<i64>,
75        setup_costs: Vec<i64>,
76        production_costs: Vec<i64>,
77        inventory_costs: Vec<i64>,
78        cost_bound: i64,
79    ) -> Self {
80        Self::try_new(
81            num_periods,
82            demands,
83            capacities,
84            setup_costs,
85            production_costs,
86            inventory_costs,
87            cost_bound,
88        )
89        .unwrap_or_else(|error| panic!("{error}"))
90    }
91
92    fn try_new(
93        num_periods: usize,
94        demands: Vec<i64>,
95        capacities: Vec<i64>,
96        setup_costs: Vec<i64>,
97        production_costs: Vec<i64>,
98        inventory_costs: Vec<i64>,
99        cost_bound: i64,
100    ) -> Result<Self, crate::registry::ConstructionError> {
101        if num_periods == 0 {
102            return Err("num_periods must be positive".into());
103        }
104        for len in [
105            demands.len(),
106            capacities.len(),
107            setup_costs.len(),
108            production_costs.len(),
109            inventory_costs.len(),
110        ] {
111            if len != num_periods {
112                return Err("all per-period vectors must have length num_periods".into());
113            }
114        }
115        if capacities.iter().any(|&capacity| {
116            usize::try_from(capacity)
117                .ok()
118                .and_then(|value| value.checked_add(1))
119                .is_none()
120        }) {
121            return Err("capacities must fit in usize for dims()".into());
122        }
123        if !(demands
124            .iter()
125            .chain(&capacities)
126            .chain(&setup_costs)
127            .chain(&production_costs)
128            .chain(&inventory_costs)
129            .all(|&value| value >= 0))
130        {
131            return Err("demands, capacities, and costs must be nonnegative".into());
132        }
133        if cost_bound < 0 {
134            return Err("cost bound must be nonnegative".into());
135        }
136
137        Ok(Self {
138            num_periods,
139            demands,
140            capacities,
141            setup_costs,
142            production_costs,
143            inventory_costs,
144            cost_bound,
145        })
146    }
147
148    pub fn num_periods(&self) -> usize {
149        self.num_periods
150    }
151
152    pub fn demands(&self) -> &[i64] {
153        &self.demands
154    }
155
156    pub fn capacities(&self) -> &[i64] {
157        &self.capacities
158    }
159
160    pub fn setup_costs(&self) -> &[i64] {
161        &self.setup_costs
162    }
163
164    pub fn production_costs(&self) -> &[i64] {
165        &self.production_costs
166    }
167
168    pub fn inventory_costs(&self) -> &[i64] {
169        &self.inventory_costs
170    }
171
172    pub fn cost_bound(&self) -> i64 {
173        self.cost_bound
174    }
175
176    /// Bit bound for every numeric construction input.
177    pub fn max_numeric_magnitude_bits(&self) -> u64 {
178        crate::types::max_numeric_magnitude_bits(
179            self.demands
180                .iter()
181                .chain(&self.capacities)
182                .chain(&self.setup_costs)
183                .chain(&self.production_costs)
184                .chain(&self.inventory_costs)
185                .copied()
186                .chain(std::iter::once(self.cost_bound)),
187        )
188    }
189
190    pub fn max_capacity(&self) -> i64 {
191        self.capacities.iter().copied().max().unwrap_or(0)
192    }
193}
194
195impl Problem for ProductionPlanning {
196    const NAME: &'static str = "ProductionPlanning";
197    type Solution = Vec<usize>;
198    type Value = Or;
199
200    crate::problem_parameters![
201        ("max_capacity", max_capacity),
202        ("num_periods", num_periods),
203        ("max_numeric_magnitude_bits", max_numeric_magnitude_bits),
204    ];
205
206    fn evaluate(&self, config: &Self::Solution) -> Result<Or, crate::traits::EvaluationError> {
207        Ok({
208            Or({
209                if config.len() != self.num_periods {
210                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
211                        "production-plan length does not match the periods".into(),
212                    ));
213                }
214
215                let mut cumulative_production = 0_i64;
216                let mut cumulative_demand = 0_i64;
217                let mut total_cost = 0_i64;
218
219                for (i, &production) in config.iter().enumerate() {
220                    let capacity = match usize::try_from(self.capacities[i]) {
221                        Ok(value) => value,
222                        Err(_) => return Ok(Or(false)),
223                    };
224                    if production > capacity {
225                        return Ok(Or(false));
226                    }
227
228                    let production = i64::try_from(production).map_err(|_| {
229                        crate::traits::EvaluationError::IntegerOverflow(
230                            "converting production quantity to i64".into(),
231                        )
232                    })?;
233                    cumulative_production = cumulative_production
234                        .checked_add(production)
235                        .ok_or_else(|| {
236                            crate::traits::EvaluationError::IntegerOverflow(
237                                "summing cumulative production".to_string(),
238                            )
239                        })?;
240                    cumulative_demand =
241                        cumulative_demand
242                            .checked_add(self.demands[i])
243                            .ok_or_else(|| {
244                                crate::traits::EvaluationError::IntegerOverflow(
245                                    "summing cumulative demand".to_string(),
246                                )
247                            })?;
248
249                    if cumulative_production < cumulative_demand {
250                        return Ok(Or(false));
251                    }
252
253                    let inventory = cumulative_production
254                        .checked_sub(cumulative_demand)
255                        .ok_or_else(|| {
256                            crate::traits::EvaluationError::IntegerOverflow(
257                                "computing production inventory".into(),
258                            )
259                        })?;
260                    let production_cost = self.production_costs[i]
261                        .checked_mul(production)
262                        .ok_or_else(|| {
263                            crate::traits::EvaluationError::IntegerOverflow(
264                                "multiplying production cost".to_string(),
265                            )
266                        })?;
267                    total_cost = total_cost.checked_add(production_cost).ok_or_else(|| {
268                        crate::traits::EvaluationError::IntegerOverflow(
269                            "summing production-planning costs".to_string(),
270                        )
271                    })?;
272                    let inventory_cost = self.inventory_costs[i]
273                        .checked_mul(inventory)
274                        .ok_or_else(|| {
275                            crate::traits::EvaluationError::IntegerOverflow(
276                                "multiplying inventory cost".to_string(),
277                            )
278                        })?;
279                    total_cost = total_cost.checked_add(inventory_cost).ok_or_else(|| {
280                        crate::traits::EvaluationError::IntegerOverflow(
281                            "summing production-planning costs".to_string(),
282                        )
283                    })?;
284                    if production > 0 {
285                        total_cost =
286                            total_cost.checked_add(self.setup_costs[i]).ok_or_else(|| {
287                                crate::traits::EvaluationError::IntegerOverflow(
288                                    "adding production setup cost".to_string(),
289                                )
290                            })?;
291                    }
292
293                    if total_cost > self.cost_bound {
294                        return Ok(Or(false));
295                    }
296                }
297
298                total_cost <= self.cost_bound
299            })
300        })
301    }
302
303    fn variant() -> Vec<(&'static str, &'static str)> {
304        crate::variant_params![]
305    }
306}
307
308impl crate::solvers::BruteForceProblem for ProductionPlanning {
309    fn dimensions(&self) -> Vec<usize> {
310        self.capacities
311            .iter()
312            .map(|&capacity| {
313                usize::try_from(capacity)
314                    .ok()
315                    .and_then(|value| value.checked_add(1))
316                    .expect("capacities validated in constructor")
317            })
318            .collect()
319    }
320}
321
322crate::declare_variants! {
323    default ProductionPlanning => "(max_capacity + 1)^num_periods" create ProductionPlanningCreateSpec,
324}
325
326crate::register_brute_force! {
327    ProductionPlanning,
328}
329
330#[cfg(feature = "example-db")]
331pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
332    vec![crate::example_db::specs::ModelExampleSpec {
333        id: "production_planning",
334        instance: Box::new(ProductionPlanning::new(
335            4,
336            vec![2, 1, 3, 2],
337            vec![4, 4, 4, 4],
338            vec![2, 2, 2, 2],
339            vec![1, 1, 1, 1],
340            vec![1, 1, 1, 1],
341            16,
342        )),
343        optimal_config: serde_json::json!(vec![3, 0, 4, 1]),
344        optimal_value: serde_json::json!(true),
345    }]
346}
347
348#[cfg(test)]
349#[path = "../../unit_tests/models/misc/production_planning.rs"]
350mod tests;