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::{FieldInfo, 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        module_path: module_path!(),
20        description: "Determine whether a multi-period production plan can satisfy all demand within a cost bound",
21        fields: &[
22            FieldInfo { name: "num_periods", type_name: "usize", description: "Number of planning periods n" },
23            FieldInfo { name: "demands", type_name: "Vec<u64>", description: "Demand r_i for each period" },
24            FieldInfo { name: "capacities", type_name: "Vec<u64>", description: "Production capacity c_i for each period" },
25            FieldInfo { name: "setup_costs", type_name: "Vec<u64>", description: "Setup cost b_i incurred when x_i > 0" },
26            FieldInfo { name: "production_costs", type_name: "Vec<u64>", description: "Per-unit production cost coefficient p_i" },
27            FieldInfo { name: "inventory_costs", type_name: "Vec<u64>", description: "Per-unit inventory cost coefficient h_i" },
28            FieldInfo { name: "cost_bound", type_name: "u64", description: "Total cost bound B" },
29        ],
30    }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ProductionPlanning {
35    #[serde(deserialize_with = "positive_usize::deserialize")]
36    num_periods: usize,
37    demands: Vec<u64>,
38    capacities: Vec<u64>,
39    setup_costs: Vec<u64>,
40    production_costs: Vec<u64>,
41    inventory_costs: Vec<u64>,
42    cost_bound: u64,
43}
44
45impl ProductionPlanning {
46    pub fn new(
47        num_periods: usize,
48        demands: Vec<u64>,
49        capacities: Vec<u64>,
50        setup_costs: Vec<u64>,
51        production_costs: Vec<u64>,
52        inventory_costs: Vec<u64>,
53        cost_bound: u64,
54    ) -> Self {
55        assert!(num_periods > 0, "num_periods must be positive");
56        for len in [
57            demands.len(),
58            capacities.len(),
59            setup_costs.len(),
60            production_costs.len(),
61            inventory_costs.len(),
62        ] {
63            assert_eq!(
64                len, num_periods,
65                "all per-period vectors must have length num_periods"
66            );
67        }
68        assert!(
69            capacities.iter().all(|&capacity| {
70                usize::try_from(capacity)
71                    .ok()
72                    .and_then(|value| value.checked_add(1))
73                    .is_some()
74            }),
75            "capacities must fit in usize for dims()"
76        );
77
78        Self {
79            num_periods,
80            demands,
81            capacities,
82            setup_costs,
83            production_costs,
84            inventory_costs,
85            cost_bound,
86        }
87    }
88
89    pub fn num_periods(&self) -> usize {
90        self.num_periods
91    }
92
93    pub fn demands(&self) -> &[u64] {
94        &self.demands
95    }
96
97    pub fn capacities(&self) -> &[u64] {
98        &self.capacities
99    }
100
101    pub fn setup_costs(&self) -> &[u64] {
102        &self.setup_costs
103    }
104
105    pub fn production_costs(&self) -> &[u64] {
106        &self.production_costs
107    }
108
109    pub fn inventory_costs(&self) -> &[u64] {
110        &self.inventory_costs
111    }
112
113    pub fn cost_bound(&self) -> u64 {
114        self.cost_bound
115    }
116
117    pub fn max_capacity(&self) -> u64 {
118        self.capacities.iter().copied().max().unwrap_or(0)
119    }
120}
121
122impl Problem for ProductionPlanning {
123    const NAME: &'static str = "ProductionPlanning";
124    type Value = Or;
125
126    fn dims(&self) -> Vec<usize> {
127        self.capacities
128            .iter()
129            .map(|&capacity| {
130                usize::try_from(capacity)
131                    .ok()
132                    .and_then(|value| value.checked_add(1))
133                    .expect("capacities validated in constructor")
134            })
135            .collect()
136    }
137
138    fn evaluate(&self, config: &[usize]) -> Or {
139        Or({
140            if config.len() != self.num_periods {
141                return Or(false);
142            }
143
144            let mut cumulative_production = 0u128;
145            let mut cumulative_demand = 0u128;
146            let mut total_cost = 0u128;
147            let cost_bound = self.cost_bound as u128;
148
149            for (i, &production) in config.iter().enumerate() {
150                let capacity = match usize::try_from(self.capacities[i]) {
151                    Ok(value) => value,
152                    Err(_) => return Or(false),
153                };
154                if production > capacity {
155                    return Or(false);
156                }
157
158                let production = production as u128;
159                cumulative_production += production;
160                cumulative_demand += self.demands[i] as u128;
161
162                if cumulative_production < cumulative_demand {
163                    return Or(false);
164                }
165
166                let inventory = cumulative_production - cumulative_demand;
167                total_cost += self.production_costs[i] as u128 * production;
168                total_cost += self.inventory_costs[i] as u128 * inventory;
169                if production > 0 {
170                    total_cost += self.setup_costs[i] as u128;
171                }
172
173                if total_cost > cost_bound {
174                    return Or(false);
175                }
176            }
177
178            total_cost <= cost_bound
179        })
180    }
181
182    fn variant() -> Vec<(&'static str, &'static str)> {
183        crate::variant_params![]
184    }
185}
186
187crate::declare_variants! {
188    default ProductionPlanning => "(max_capacity + 1)^num_periods",
189}
190
191#[cfg(feature = "example-db")]
192pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
193    vec![crate::example_db::specs::ModelExampleSpec {
194        id: "production_planning",
195        instance: Box::new(ProductionPlanning::new(
196            4,
197            vec![2, 1, 3, 2],
198            vec![4, 4, 4, 4],
199            vec![2, 2, 2, 2],
200            vec![1, 1, 1, 1],
201            vec![1, 1, 1, 1],
202            16,
203        )),
204        optimal_config: vec![3, 0, 4, 1],
205        optimal_value: serde_json::json!(true),
206    }]
207}
208
209mod positive_usize {
210    use serde::de::Error;
211    use serde::{Deserialize, Deserializer};
212
213    pub fn deserialize<'de, D>(deserializer: D) -> Result<usize, D::Error>
214    where
215        D: Deserializer<'de>,
216    {
217        let value = usize::deserialize(deserializer)?;
218        if value == 0 {
219            return Err(D::Error::custom("expected positive integer, got 0"));
220        }
221        Ok(value)
222    }
223}
224
225#[cfg(test)]
226#[path = "../../unit_tests/models/misc/production_planning.rs"]
227mod tests;