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)]
27pub struct ProductionPlanning {
28    #[serde(deserialize_with = "positive_usize::deserialize")]
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        if spec.num_periods == 0 {
59            return Err("num_periods must be positive".to_string().into());
60        }
61        for (name, len) in [
62            ("demands", spec.demands.len()),
63            ("capacities", spec.capacities.len()),
64            ("setup_costs", spec.setup_costs.len()),
65            ("production_costs", spec.production_costs.len()),
66            ("inventory_costs", spec.inventory_costs.len()),
67        ] {
68            if len != spec.num_periods {
69                return Err(
70                    format!("{name} has {len} entries, expected {}", spec.num_periods).into(),
71                );
72            }
73        }
74        if spec.capacities.iter().any(|&capacity| {
75            usize::try_from(capacity)
76                .ok()
77                .and_then(|v| v.checked_add(1))
78                .is_none()
79        }) {
80            return Err("capacities must fit in usize for dims()".to_string().into());
81        }
82        Ok(Self::new(
83            spec.num_periods,
84            spec.demands,
85            spec.capacities,
86            spec.setup_costs,
87            spec.production_costs,
88            spec.inventory_costs,
89            spec.cost_bound,
90        ))
91    }
92}
93
94impl ProductionPlanning {
95    pub fn new(
96        num_periods: usize,
97        demands: Vec<i64>,
98        capacities: Vec<i64>,
99        setup_costs: Vec<i64>,
100        production_costs: Vec<i64>,
101        inventory_costs: Vec<i64>,
102        cost_bound: i64,
103    ) -> Self {
104        assert!(num_periods > 0, "num_periods must be positive");
105        for len in [
106            demands.len(),
107            capacities.len(),
108            setup_costs.len(),
109            production_costs.len(),
110            inventory_costs.len(),
111        ] {
112            assert_eq!(
113                len, num_periods,
114                "all per-period vectors must have length num_periods"
115            );
116        }
117        assert!(
118            capacities.iter().all(|&capacity| {
119                usize::try_from(capacity)
120                    .ok()
121                    .and_then(|value| value.checked_add(1))
122                    .is_some()
123            }),
124            "capacities must fit in usize for dims()"
125        );
126        assert!(
127            demands
128                .iter()
129                .chain(&capacities)
130                .chain(&setup_costs)
131                .chain(&production_costs)
132                .chain(&inventory_costs)
133                .all(|&value| value >= 0),
134            "demands, capacities, and costs must be nonnegative"
135        );
136        assert!(cost_bound >= 0, "cost bound must be nonnegative");
137
138        Self {
139            num_periods,
140            demands,
141            capacities,
142            setup_costs,
143            production_costs,
144            inventory_costs,
145            cost_bound,
146        }
147    }
148
149    pub fn num_periods(&self) -> usize {
150        self.num_periods
151    }
152
153    pub fn demands(&self) -> &[i64] {
154        &self.demands
155    }
156
157    pub fn capacities(&self) -> &[i64] {
158        &self.capacities
159    }
160
161    pub fn setup_costs(&self) -> &[i64] {
162        &self.setup_costs
163    }
164
165    pub fn production_costs(&self) -> &[i64] {
166        &self.production_costs
167    }
168
169    pub fn inventory_costs(&self) -> &[i64] {
170        &self.inventory_costs
171    }
172
173    pub fn cost_bound(&self) -> i64 {
174        self.cost_bound
175    }
176
177    pub fn max_capacity(&self) -> i64 {
178        self.capacities.iter().copied().max().unwrap_or(0)
179    }
180}
181
182impl Problem for ProductionPlanning {
183    const NAME: &'static str = "ProductionPlanning";
184    type Solution = Vec<usize>;
185    type Value = Or;
186
187    crate::problem_parameters![("max_capacity", max_capacity), ("num_periods", num_periods),];
188
189    fn evaluate(&self, config: &Self::Solution) -> Result<Or, crate::traits::EvaluationError> {
190        Ok({
191            Or({
192                if config.len() != self.num_periods {
193                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
194                        "production-plan length does not match the periods".into(),
195                    ));
196                }
197
198                let mut cumulative_production = 0_i64;
199                let mut cumulative_demand = 0_i64;
200                let mut total_cost = 0_i64;
201
202                for (i, &production) in config.iter().enumerate() {
203                    let capacity = match usize::try_from(self.capacities[i]) {
204                        Ok(value) => value,
205                        Err(_) => return Ok(Or(false)),
206                    };
207                    if production > capacity {
208                        return Ok(Or(false));
209                    }
210
211                    let production = i64::try_from(production).map_err(|_| {
212                        crate::traits::EvaluationError::IntegerOverflow(
213                            "converting production quantity to i64".into(),
214                        )
215                    })?;
216                    cumulative_production = cumulative_production
217                        .checked_add(production)
218                        .ok_or_else(|| {
219                            crate::traits::EvaluationError::IntegerOverflow(
220                                "summing cumulative production".to_string(),
221                            )
222                        })?;
223                    cumulative_demand =
224                        cumulative_demand
225                            .checked_add(self.demands[i])
226                            .ok_or_else(|| {
227                                crate::traits::EvaluationError::IntegerOverflow(
228                                    "summing cumulative demand".to_string(),
229                                )
230                            })?;
231
232                    if cumulative_production < cumulative_demand {
233                        return Ok(Or(false));
234                    }
235
236                    let inventory = cumulative_production
237                        .checked_sub(cumulative_demand)
238                        .ok_or_else(|| {
239                            crate::traits::EvaluationError::IntegerOverflow(
240                                "computing production inventory".into(),
241                            )
242                        })?;
243                    let production_cost = self.production_costs[i]
244                        .checked_mul(production)
245                        .ok_or_else(|| {
246                            crate::traits::EvaluationError::IntegerOverflow(
247                                "multiplying production cost".to_string(),
248                            )
249                        })?;
250                    total_cost = total_cost.checked_add(production_cost).ok_or_else(|| {
251                        crate::traits::EvaluationError::IntegerOverflow(
252                            "summing production-planning costs".to_string(),
253                        )
254                    })?;
255                    let inventory_cost = self.inventory_costs[i]
256                        .checked_mul(inventory)
257                        .ok_or_else(|| {
258                            crate::traits::EvaluationError::IntegerOverflow(
259                                "multiplying inventory cost".to_string(),
260                            )
261                        })?;
262                    total_cost = total_cost.checked_add(inventory_cost).ok_or_else(|| {
263                        crate::traits::EvaluationError::IntegerOverflow(
264                            "summing production-planning costs".to_string(),
265                        )
266                    })?;
267                    if production > 0 {
268                        total_cost =
269                            total_cost.checked_add(self.setup_costs[i]).ok_or_else(|| {
270                                crate::traits::EvaluationError::IntegerOverflow(
271                                    "adding production setup cost".to_string(),
272                                )
273                            })?;
274                    }
275
276                    if total_cost > self.cost_bound {
277                        return Ok(Or(false));
278                    }
279                }
280
281                total_cost <= self.cost_bound
282            })
283        })
284    }
285
286    fn variant() -> Vec<(&'static str, &'static str)> {
287        crate::variant_params![]
288    }
289}
290
291impl crate::solvers::BruteForceProblem for ProductionPlanning {
292    fn dimensions(&self) -> Vec<usize> {
293        self.capacities
294            .iter()
295            .map(|&capacity| {
296                usize::try_from(capacity)
297                    .ok()
298                    .and_then(|value| value.checked_add(1))
299                    .expect("capacities validated in constructor")
300            })
301            .collect()
302    }
303}
304
305crate::declare_variants! {
306    default ProductionPlanning => "(max_capacity + 1)^num_periods" create ProductionPlanningCreateSpec,
307}
308
309crate::register_brute_force! {
310    ProductionPlanning,
311}
312
313#[cfg(feature = "example-db")]
314pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
315    vec![crate::example_db::specs::ModelExampleSpec {
316        id: "production_planning",
317        instance: Box::new(ProductionPlanning::new(
318            4,
319            vec![2, 1, 3, 2],
320            vec![4, 4, 4, 4],
321            vec![2, 2, 2, 2],
322            vec![1, 1, 1, 1],
323            vec![1, 1, 1, 1],
324            16,
325        )),
326        optimal_config: serde_json::json!(vec![3, 0, 4, 1]),
327        optimal_value: serde_json::json!(true),
328    }]
329}
330
331mod positive_usize {
332    use serde::de::Error;
333    use serde::{Deserialize, Deserializer};
334
335    pub fn deserialize<'de, D>(deserializer: D) -> Result<usize, D::Error>
336    where
337        D: Deserializer<'de>,
338    {
339        let value = usize::deserialize(deserializer)?;
340        if value == 0 {
341            return Err(D::Error::custom("expected positive integer, got 0"));
342        }
343        Ok(value)
344    }
345}
346
347#[cfg(test)]
348#[path = "../../unit_tests/models/misc/production_planning.rs"]
349mod tests;