1use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
8use crate::topology::{is_simple_st_path, Graph, SimpleGraph};
9use crate::traits::Problem;
10use crate::types::{Min, WeightElement};
11use num_traits::Zero;
12use serde::{Deserialize, Serialize};
13
14inventory::submit! {
15 ProblemSchemaEntry {
16 name: "ShortestWeightConstrainedPath",
17 display_name: "Shortest Weight-Constrained Path",
18 aliases: &[],
19 dimensions: &[
20 VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
21 VariantDimension::new("weight", "i64", &["i64"]),
22 ],
23 category: crate::registry::ProblemCategory::Graph,
24 module_path: module_path!(),
25 description: "Find a simple s-t path minimizing total length subject to a weight budget",
26 fields: ShortestWeightConstrainedPathCreateSpec::FIELDS,
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ShortestWeightConstrainedPath<G, N: WeightElement> {
56 graph: G,
58 edge_lengths: Vec<N>,
60 edge_weights: Vec<N>,
62 source_vertex: usize,
64 target_vertex: usize,
66 weight_bound: N::Sum,
68}
69
70#[derive(Debug, Deserialize, crate::CreateSpec)]
71struct ShortestWeightConstrainedPathCreateSpec {
72 graph: SimpleGraph,
74 edge_lengths: Vec<i64>,
76 edge_weights: Vec<i64>,
78 source_vertex: usize,
80 target_vertex: usize,
82 weight_bound: i64,
84}
85
86impl TryFrom<ShortestWeightConstrainedPathCreateSpec>
87 for ShortestWeightConstrainedPath<SimpleGraph, i64>
88{
89 type Error = crate::registry::ConstructionError;
90 fn try_from(spec: ShortestWeightConstrainedPathCreateSpec) -> Result<Self, Self::Error> {
91 let edge_count = spec.graph.num_edges();
92 if spec.edge_lengths.len() != edge_count {
93 return Err(format!(
94 "edge_lengths has {} entries, expected {edge_count}",
95 spec.edge_lengths.len()
96 )
97 .into());
98 }
99 if spec.edge_weights.len() != edge_count {
100 return Err(format!(
101 "edge_weights has {} entries, expected {edge_count}",
102 spec.edge_weights.len()
103 )
104 .into());
105 }
106 if spec.edge_lengths.iter().any(|&value| value <= 0) {
107 return Err("edge_lengths must be positive".to_string().into());
108 }
109 if spec.edge_weights.iter().any(|&value| value <= 0) {
110 return Err("edge_weights must be positive".to_string().into());
111 }
112 let vertex_count = spec.graph.num_vertices();
113 if spec.source_vertex >= vertex_count {
114 return Err(format!(
115 "source_vertex {} is outside graph with {vertex_count} vertices",
116 spec.source_vertex
117 )
118 .into());
119 }
120 if spec.target_vertex >= vertex_count {
121 return Err(format!(
122 "target_vertex {} is outside graph with {vertex_count} vertices",
123 spec.target_vertex
124 )
125 .into());
126 }
127 if spec.weight_bound <= 0 {
128 return Err("weight_bound must be positive".to_string().into());
129 }
130 Ok(Self::new(
131 spec.graph,
132 spec.edge_lengths,
133 spec.edge_weights,
134 spec.source_vertex,
135 spec.target_vertex,
136 spec.weight_bound,
137 ))
138 }
139}
140
141impl<G: Graph, N: WeightElement> ShortestWeightConstrainedPath<G, N> {
142 fn assert_positive_edge_values(values: &[N], label: &str) {
143 let zero = N::Sum::zero();
144 assert!(
145 values.iter().all(|value| value.to_sum() > zero.clone()),
146 "All {label} must be positive (> 0)"
147 );
148 }
149
150 fn assert_positive_bound(bound: &N::Sum, label: &str) {
151 let zero = N::Sum::zero();
152 assert!(bound > &zero, "{label} must be positive (> 0)");
153 }
154
155 pub fn new(
162 graph: G,
163 edge_lengths: Vec<N>,
164 edge_weights: Vec<N>,
165 source_vertex: usize,
166 target_vertex: usize,
167 weight_bound: N::Sum,
168 ) -> Self {
169 assert_eq!(
170 edge_lengths.len(),
171 graph.num_edges(),
172 "edge_lengths length must match num_edges"
173 );
174 assert_eq!(
175 edge_weights.len(),
176 graph.num_edges(),
177 "edge_weights length must match num_edges"
178 );
179 Self::assert_positive_edge_values(&edge_lengths, "edge lengths");
180 Self::assert_positive_edge_values(&edge_weights, "edge weights");
181 assert!(
182 source_vertex < graph.num_vertices(),
183 "source_vertex {} out of bounds (graph has {} vertices)",
184 source_vertex,
185 graph.num_vertices()
186 );
187 assert!(
188 target_vertex < graph.num_vertices(),
189 "target_vertex {} out of bounds (graph has {} vertices)",
190 target_vertex,
191 graph.num_vertices()
192 );
193 Self::assert_positive_bound(&weight_bound, "weight_bound");
194 Self {
195 graph,
196 edge_lengths,
197 edge_weights,
198 source_vertex,
199 target_vertex,
200 weight_bound,
201 }
202 }
203
204 pub fn graph(&self) -> &G {
206 &self.graph
207 }
208
209 pub fn edge_lengths(&self) -> &[N] {
211 &self.edge_lengths
212 }
213
214 pub fn edge_weights(&self) -> &[N] {
216 &self.edge_weights
217 }
218
219 pub fn set_lengths(&mut self, edge_lengths: Vec<N>) {
221 assert_eq!(
222 edge_lengths.len(),
223 self.graph.num_edges(),
224 "edge_lengths length must match num_edges"
225 );
226 Self::assert_positive_edge_values(&edge_lengths, "edge lengths");
227 self.edge_lengths = edge_lengths;
228 }
229
230 pub fn set_weights(&mut self, edge_weights: Vec<N>) {
232 assert_eq!(
233 edge_weights.len(),
234 self.graph.num_edges(),
235 "edge_weights length must match num_edges"
236 );
237 Self::assert_positive_edge_values(&edge_weights, "edge weights");
238 self.edge_weights = edge_weights;
239 }
240
241 pub fn source_vertex(&self) -> usize {
243 self.source_vertex
244 }
245
246 pub fn target_vertex(&self) -> usize {
248 self.target_vertex
249 }
250
251 pub fn weight_bound(&self) -> &N::Sum {
253 &self.weight_bound
254 }
255
256 pub fn is_weighted(&self) -> bool {
258 !N::IS_UNIT
259 }
260
261 pub fn num_vertices(&self) -> usize {
263 self.graph.num_vertices()
264 }
265
266 pub fn num_edges(&self) -> usize {
268 self.graph.num_edges()
269 }
270
271 pub fn is_valid_solution(
276 &self,
277 config: &[bool],
278 ) -> Result<Option<N::Sum>, crate::traits::EvaluationError> {
279 if config.len() != self.graph.num_edges() {
280 return Ok(None);
281 }
282
283 if self.source_vertex == self.target_vertex {
284 if config.contains(&true) {
285 return Ok(None);
286 }
287 return Ok(Some(N::Sum::zero()));
288 }
289
290 let mut total_length = N::Sum::zero();
291 let mut total_weight = N::Sum::zero();
292
293 for (idx, &selected) in config.iter().enumerate() {
294 if !selected {
295 continue;
296 }
297 total_length = N::checked_add_to_sum(
298 total_length,
299 self.edge_lengths[idx].to_sum(),
300 "summing constrained path edge lengths",
301 )?;
302 total_weight = N::checked_add_to_sum(
303 total_weight,
304 self.edge_weights[idx].to_sum(),
305 "summing constrained path edge weights",
306 )?;
307 }
308
309 if total_weight > self.weight_bound.clone() {
310 return Ok(None);
311 }
312 if !is_simple_st_path(
313 self.graph.num_vertices(),
314 &self.graph.edges(),
315 self.source_vertex,
316 self.target_vertex,
317 config,
318 ) {
319 Ok(None)
320 } else {
321 Ok(Some(total_length))
322 }
323 }
324}
325
326impl<G, N> Problem for ShortestWeightConstrainedPath<G, N>
327where
328 G: Graph + crate::variant::VariantParam,
329 N: WeightElement + crate::variant::VariantParam,
330{
331 const NAME: &'static str = "ShortestWeightConstrainedPath";
332 type Solution = Vec<bool>;
333 type Value = Min<N::Sum>;
334
335 crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),];
336
337 fn variant() -> Vec<(&'static str, &'static str)> {
338 crate::variant_params![G, N]
339 }
340
341 fn evaluate(
342 &self,
343 config: &Self::Solution,
344 ) -> Result<Min<N::Sum>, crate::traits::EvaluationError> {
345 if config.len() != self.graph.num_edges() {
346 return Err(crate::traits::EvaluationError::InvalidConfiguration(
347 "edge-selection length does not match the graph".into(),
348 ));
349 }
350 Ok(Min(self.is_valid_solution(config)?))
351 }
352}
353
354impl<G, N> crate::solvers::BruteForceProblem for ShortestWeightConstrainedPath<G, N>
355where
356 G: Graph + crate::variant::VariantParam,
357 N: WeightElement + crate::variant::VariantParam,
358{
359 fn dimensions(&self) -> Vec<usize> {
360 vec![2; self.graph.num_edges()]
361 }
362}
363
364#[cfg(feature = "example-db")]
365pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
366 vec![crate::example_db::specs::ModelExampleSpec {
367 id: "shortest_weight_constrained_path_simplegraph",
368 instance: Box::new(ShortestWeightConstrainedPath::new(
369 SimpleGraph::new(
370 6,
371 vec![
372 (0, 1),
373 (0, 2),
374 (1, 3),
375 (2, 3),
376 (2, 4),
377 (3, 5),
378 (4, 5),
379 (1, 4),
380 ],
381 ),
382 vec![2, 4, 3, 1, 5, 4, 2, 6],
383 vec![5, 1, 2, 3, 2, 3, 1, 1],
384 0,
385 5,
386 8,
387 )),
388 optimal_config: serde_json::json!(vec![
389 false, true, false, true, false, true, false, false
390 ]),
391 optimal_value: serde_json::json!(9),
392 }]
393}
394
395crate::declare_variants! {
396 default ShortestWeightConstrainedPath<SimpleGraph, i64> => "2^num_edges" create ShortestWeightConstrainedPathCreateSpec,
397}
398
399crate::register_brute_force! {
400 ShortestWeightConstrainedPath<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
401}
402
403#[cfg(test)]
404#[path = "../../unit_tests/models/graph/shortest_weight_constrained_path.rs"]
405mod tests;