problemreductions/models/misc/
production_planning.rs1use 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;