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)]
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 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 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;