Skip to main content

problemreductions/models/graph/
shortest_weight_constrained_path.rs

1//! Shortest Weight-Constrained Path problem implementation.
2//!
3//! The Shortest Weight-Constrained Path problem finds a simple path from a
4//! source vertex to a target vertex that minimizes total length while keeping
5//! the total weight within a prescribed bound.
6
7use 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/// The Shortest Weight-Constrained Path problem.
31///
32/// Given a graph G = (V, E) with positive edge lengths l(e) and edge weights
33/// w(e), designated vertices s and t, and a weight bound W, find a simple
34/// path from s to t that minimizes total length subject to total weight at
35/// most W.
36///
37/// # Representation
38///
39/// Each edge is assigned a binary variable:
40/// - 0: edge is not in the selected path
41/// - 1: edge is in the selected path
42///
43/// A valid configuration must:
44/// - form a single simple path from `source_vertex` to `target_vertex`
45/// - use only edges present in the graph
46/// - satisfy the weight bound
47///
48/// The objective value is the total length of the path (`Min<N::Sum>`).
49///
50/// # Type Parameters
51///
52/// * `G` - The graph type (e.g., `SimpleGraph`)
53/// * `N` - The edge length / weight type (e.g., `i64`, `f64`)
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ShortestWeightConstrainedPath<G, N: WeightElement> {
56    /// The underlying graph.
57    graph: G,
58    /// Length for each edge in graph-edge order.
59    edge_lengths: Vec<N>,
60    /// Weight for each edge in graph-edge order.
61    edge_weights: Vec<N>,
62    /// Source vertex s.
63    source_vertex: usize,
64    /// Target vertex t.
65    target_vertex: usize,
66    /// Upper bound W on total path weight.
67    weight_bound: N::Sum,
68}
69
70#[derive(Debug, Deserialize, crate::CreateSpec)]
71struct ShortestWeightConstrainedPathCreateSpec {
72    /// The underlying graph G=(V,E).
73    graph: SimpleGraph,
74    /// Positive edge lengths in graph edge order.
75    edge_lengths: Vec<i64>,
76    /// Positive edge weights in graph edge order.
77    edge_weights: Vec<i64>,
78    /// Source vertex s.
79    source_vertex: usize,
80    /// Target vertex t.
81    target_vertex: usize,
82    /// Positive upper bound on total path weight.
83    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    /// Create a new ShortestWeightConstrainedPath instance.
156    ///
157    /// # Panics
158    ///
159    /// Panics if either edge vector length does not match the graph's edge
160    /// count, or if the source / target vertices are out of bounds.
161    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    /// Get a reference to the underlying graph.
205    pub fn graph(&self) -> &G {
206        &self.graph
207    }
208
209    /// Get the edge lengths.
210    pub fn edge_lengths(&self) -> &[N] {
211        &self.edge_lengths
212    }
213
214    /// Get the edge weights.
215    pub fn edge_weights(&self) -> &[N] {
216        &self.edge_weights
217    }
218
219    /// Set new edge lengths.
220    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    /// Set new edge weights.
231    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    /// Get the source vertex.
242    pub fn source_vertex(&self) -> usize {
243        self.source_vertex
244    }
245
246    /// Get the target vertex.
247    pub fn target_vertex(&self) -> usize {
248        self.target_vertex
249    }
250
251    /// Get the weight bound.
252    pub fn weight_bound(&self) -> &N::Sum {
253        &self.weight_bound
254    }
255
256    /// Check whether this problem uses a non-unit weight type.
257    pub fn is_weighted(&self) -> bool {
258        !N::IS_UNIT
259    }
260
261    /// Get the number of vertices in the graph.
262    pub fn num_vertices(&self) -> usize {
263        self.graph.num_vertices()
264    }
265
266    /// Get the number of edges in the graph.
267    pub fn num_edges(&self) -> usize {
268        self.graph.num_edges()
269    }
270
271    /// Check if a configuration is a valid weight-constrained s-t path.
272    ///
273    /// Returns `Some(total_length)` for a valid simple s-t path whose total
274    /// weight is within the weight bound, or `None` otherwise.
275    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;