problemreductions/models/graph/
minimum_cost_circulation.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry};
36use crate::topology::DirectedGraph;
37use crate::traits::Problem;
38use serde::{Deserialize, Serialize};
39
40inventory::submit! {
41 ProblemSchemaEntry {
42 name: "MinimumCostCirculation",
43 display_name: "Minimum-Cost Circulation",
44 aliases: &["MCC"],
45 dimensions: &[],
46 category: crate::registry::ProblemCategory::Graph,
47 module_path: module_path!(),
48 description: "Integral circulation on a directed multigraph minimizing total signed arc cost",
49 fields: &[
50 FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed multigraph G = (V, A); loops and parallel arcs allowed" },
51 FieldInfo { name: "capacities", type_name: "Vec<i64>", description: "Arc capacity c(a) in graph arc order (non-negative)" },
52 FieldInfo { name: "costs", type_name: "Vec<i64>", description: "Signed arc cost a(a) in graph arc order (negative values allowed)" },
53 ],
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(try_from = "MinimumCostCirculationData")]
89pub struct MinimumCostCirculation {
90 graph: DirectedGraph,
92 capacities: Vec<i64>,
94 costs: Vec<i64>,
96}
97
98#[derive(Deserialize)]
99struct MinimumCostCirculationData {
100 graph: DirectedGraph,
101 capacities: Vec<i64>,
102 costs: Vec<i64>,
103}
104
105impl TryFrom<MinimumCostCirculationData> for MinimumCostCirculation {
106 type Error = crate::registry::ConstructionError;
107 fn try_from(data: MinimumCostCirculationData) -> Result<Self, Self::Error> {
108 Self::try_new(data.graph, data.capacities, data.costs)
109 }
110}
111
112impl MinimumCostCirculation {
113 pub fn new(graph: DirectedGraph, capacities: Vec<i64>, costs: Vec<i64>) -> Self {
124 Self::try_new(graph, capacities, costs).unwrap_or_else(|error| panic!("{error}"))
125 }
126
127 fn try_new(
128 graph: DirectedGraph,
129 capacities: Vec<i64>,
130 costs: Vec<i64>,
131 ) -> Result<Self, crate::registry::ConstructionError> {
132 let m = graph.num_arcs();
133 if capacities.len() != m {
134 return Err(format!(
135 "capacities length ({}) must match num_arcs ({m})",
136 capacities.len()
137 )
138 .into());
139 }
140 if costs.len() != m {
141 return Err(format!("costs length ({}) must match num_arcs ({m})", costs.len()).into());
142 }
143 for (i, &c) in capacities.iter().enumerate() {
144 if c < 0 {
145 return Err(format!("capacity[{i}] = {c} is negative").into());
146 }
147 }
148 Ok(Self {
149 graph,
150 capacities,
151 costs,
152 })
153 }
154
155 pub fn graph(&self) -> &DirectedGraph {
157 &self.graph
158 }
159
160 pub fn capacities(&self) -> &[i64] {
162 &self.capacities
163 }
164
165 pub fn costs(&self) -> &[i64] {
167 &self.costs
168 }
169
170 pub fn num_vertices(&self) -> usize {
172 self.graph.num_vertices()
173 }
174
175 pub fn num_arcs(&self) -> usize {
177 self.graph.num_arcs()
178 }
179
180 pub fn is_feasible(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
189 let m = self.graph.num_arcs();
190 if config.len() != m {
191 return Ok(false);
192 }
193 for (flow, cap) in config.iter().zip(self.capacities.iter()) {
195 let flow = i64::try_from(*flow).map_err(|_| {
196 crate::traits::EvaluationError::IntegerOverflow(
197 "converting circulation configuration value".to_string(),
198 )
199 })?;
200 if flow > *cap {
201 return Ok(false);
202 }
203 }
204 let n = self.graph.num_vertices();
206 let mut balance = vec![0_i64; n];
207 for (a, &(u, v)) in self.graph.arcs().iter().enumerate() {
208 let flow = i64::try_from(config[a]).map_err(|_| {
209 crate::traits::EvaluationError::IntegerOverflow(
210 "converting circulation configuration value".to_string(),
211 )
212 })?;
213 balance[u] = balance[u].checked_sub(flow).ok_or_else(|| {
214 crate::traits::EvaluationError::IntegerOverflow(
215 "computing circulation vertex balance".to_string(),
216 )
217 })?;
218 balance[v] = balance[v].checked_add(flow).ok_or_else(|| {
219 crate::traits::EvaluationError::IntegerOverflow(
220 "computing circulation vertex balance".to_string(),
221 )
222 })?;
223 }
224 Ok(balance.iter().all(|&b| b == 0))
225 }
226
227 pub fn total_cost(&self, config: &[usize]) -> Result<i64, crate::traits::EvaluationError> {
229 let mut total = 0_i64;
230 for (&flow, &cost) in config.iter().zip(self.costs.iter()) {
231 let flow = i64::try_from(flow).map_err(|_| {
232 crate::traits::EvaluationError::IntegerOverflow(
233 "converting circulation configuration value".to_string(),
234 )
235 })?;
236 let term = flow.checked_mul(cost).ok_or_else(|| {
237 crate::traits::EvaluationError::IntegerOverflow(
238 "multiplying circulation arc cost".to_string(),
239 )
240 })?;
241 total = total.checked_add(term).ok_or_else(|| {
242 crate::traits::EvaluationError::IntegerOverflow(
243 "summing circulation costs".to_string(),
244 )
245 })?;
246 }
247 Ok(total)
248 }
249}
250
251impl Problem for MinimumCostCirculation {
252 const NAME: &'static str = "MinimumCostCirculation";
253 type Solution = Vec<usize>;
254 type Value = crate::types::Min<i64>;
255
256 crate::problem_parameters![("num_arcs", num_arcs), ("num_vertices", num_vertices),];
257
258 fn evaluate(
259 &self,
260 config: &Self::Solution,
261 ) -> Result<crate::types::Min<i64>, crate::traits::EvaluationError> {
262 if config.len() != self.graph.num_arcs() {
263 return Err(crate::traits::EvaluationError::InvalidConfiguration(
264 "flow vector length does not match the graph arcs".into(),
265 ));
266 }
267 Ok({
268 if !self.is_feasible(config)? {
269 return Ok(crate::types::Min(None));
270 }
271 crate::types::Min(Some(self.total_cost(config)?))
272 })
273 }
274
275 fn variant() -> Vec<(&'static str, &'static str)> {
276 crate::variant_params![]
277 }
278}
279
280impl crate::solvers::BruteForceProblem for MinimumCostCirculation {
281 fn dimensions(&self) -> Vec<usize> {
282 self.capacities.iter().map(|&c| (c as usize) + 1).collect()
283 }
284}
285
286crate::declare_variants! {
287 default MinimumCostCirculation => "(num_vertices + num_arcs)^6",
288}
289
290crate::register_brute_force! {
291 MinimumCostCirculation,
292}
293
294#[cfg(feature = "example-db")]
295pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
296 let problem = MinimumCostCirculation::new(
303 crate::topology::DirectedGraph::new(3, vec![(0, 1), (1, 0), (0, 2), (2, 0)]),
304 vec![2, 2, 1, 1],
305 vec![2, -3, 1, -4],
306 );
307 let optimal_config = vec![2, 2, 1, 1];
308 let optimal_value = problem
309 .evaluate(&optimal_config)
310 .expect("canonical example evaluation must succeed");
311 let scalar = match optimal_value {
312 crate::types::Min(Some(v)) => v,
313 crate::types::Min(None) => panic!("canonical example must be feasible"),
314 };
315 vec![crate::example_db::specs::ModelExampleSpec {
316 id: "minimum_cost_circulation",
317 instance: Box::new(problem),
318 optimal_config: serde_json::to_value(optimal_config)
319 .expect("solution serialization must succeed"),
320 optimal_value: serde_json::json!(scalar),
321 }]
322}
323
324#[cfg(test)]
325#[path = "../../unit_tests/models/graph/minimum_cost_circulation.rs"]
326mod tests;