problemreductions/models/misc/
production_planning.rs1use 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 num_periods: usize,
42 demands: Vec<i64>,
44 capacities: Vec<i64>,
46 setup_costs: Vec<i64>,
48 production_costs: Vec<i64>,
50 inventory_costs: Vec<i64>,
52 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 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;