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)]
55#[serde(try_from = "ShortestWeightConstrainedPathData<G, N>")]
56#[serde(bound(
57    deserialize = "G: Graph + Deserialize<'de>, N: WeightElement + Deserialize<'de>, N::Sum: Deserialize<'de>"
58))]
59pub struct ShortestWeightConstrainedPath<G, N: WeightElement> {
60    /// The underlying graph.
61    graph: G,
62    /// Length for each edge in graph-edge order.
63    edge_lengths: Vec<N>,
64    /// Weight for each edge in graph-edge order.
65    edge_weights: Vec<N>,
66    /// Source vertex s.
67    source_vertex: usize,
68    /// Target vertex t.
69    target_vertex: usize,
70    /// Upper bound W on total path weight.
71    weight_bound: N::Sum,
72}
73
74#[derive(Deserialize)]
75#[serde(bound(
76    deserialize = "G: Graph + Deserialize<'de>, N: WeightElement + Deserialize<'de>, N::Sum: Deserialize<'de>"
77))]
78struct ShortestWeightConstrainedPathData<G, N: WeightElement> {
79    graph: G,
80    edge_lengths: Vec<N>,
81    edge_weights: Vec<N>,
82    source_vertex: usize,
83    target_vertex: usize,
84    weight_bound: N::Sum,
85}
86
87impl<G, N> TryFrom<ShortestWeightConstrainedPathData<G, N>> for ShortestWeightConstrainedPath<G, N>
88where
89    G: Graph,
90    N: WeightElement,
91{
92    type Error = crate::registry::ConstructionError;
93
94    fn try_from(data: ShortestWeightConstrainedPathData<G, N>) -> Result<Self, Self::Error> {
95        Self::try_new(
96            data.graph,
97            data.edge_lengths,
98            data.edge_weights,
99            data.source_vertex,
100            data.target_vertex,
101            data.weight_bound,
102        )
103    }
104}
105
106#[derive(Debug, Deserialize, crate::CreateSpec)]
107struct ShortestWeightConstrainedPathCreateSpec {
108    /// The underlying graph G=(V,E).
109    graph: SimpleGraph,
110    /// Positive edge lengths in graph edge order.
111    edge_lengths: Vec<i64>,
112    /// Positive edge weights in graph edge order.
113    edge_weights: Vec<i64>,
114    /// Source vertex s.
115    source_vertex: usize,
116    /// Target vertex t.
117    target_vertex: usize,
118    /// Positive upper bound on total path weight.
119    weight_bound: i64,
120}
121
122impl TryFrom<ShortestWeightConstrainedPathCreateSpec>
123    for ShortestWeightConstrainedPath<SimpleGraph, i64>
124{
125    type Error = crate::registry::ConstructionError;
126    fn try_from(spec: ShortestWeightConstrainedPathCreateSpec) -> Result<Self, Self::Error> {
127        Self::try_new(
128            spec.graph,
129            spec.edge_lengths,
130            spec.edge_weights,
131            spec.source_vertex,
132            spec.target_vertex,
133            spec.weight_bound,
134        )
135    }
136}
137
138impl<G: Graph, N: WeightElement> ShortestWeightConstrainedPath<G, N> {
139    fn check_edge_values(
140        graph: &G,
141        values: &[N],
142        label: &str,
143    ) -> Result<(), crate::registry::ConstructionError> {
144        if values.len() != graph.num_edges() {
145            return Err(crate::registry::ConstructionError::length_mismatch(
146                label,
147                values.len(),
148                graph.num_edges(),
149            ));
150        }
151        if !values.iter().all(|value| value.to_sum() > N::Sum::zero()) {
152            return Err(format!("All {label} must be positive (> 0)").into());
153        }
154        Ok(())
155    }
156
157    /// Create a new ShortestWeightConstrainedPath instance.
158    ///
159    /// # Panics
160    ///
161    /// Panics if either edge vector length does not match the graph's edge
162    /// count, or if the source / target vertices are out of bounds.
163    pub fn new(
164        graph: G,
165        edge_lengths: Vec<N>,
166        edge_weights: Vec<N>,
167        source_vertex: usize,
168        target_vertex: usize,
169        weight_bound: N::Sum,
170    ) -> Self {
171        Self::try_new(
172            graph,
173            edge_lengths,
174            edge_weights,
175            source_vertex,
176            target_vertex,
177            weight_bound,
178        )
179        .unwrap_or_else(|error| panic!("{error}"))
180    }
181
182    fn try_new(
183        graph: G,
184        edge_lengths: Vec<N>,
185        edge_weights: Vec<N>,
186        source_vertex: usize,
187        target_vertex: usize,
188        weight_bound: N::Sum,
189    ) -> Result<Self, crate::registry::ConstructionError> {
190        Self::check_edge_values(&graph, &edge_lengths, "edge lengths")?;
191        Self::check_edge_values(&graph, &edge_weights, "edge weights")?;
192        if source_vertex >= graph.num_vertices() {
193            return Err(format!(
194                "source_vertex {} out of bounds (graph has {} vertices)",
195                source_vertex,
196                graph.num_vertices()
197            )
198            .into());
199        }
200        if target_vertex >= graph.num_vertices() {
201            return Err(format!(
202                "target_vertex {} out of bounds (graph has {} vertices)",
203                target_vertex,
204                graph.num_vertices()
205            )
206            .into());
207        }
208        if weight_bound.partial_cmp(&N::Sum::zero()) != Some(std::cmp::Ordering::Greater) {
209            return Err("weight_bound must be positive (> 0)".into());
210        }
211        Ok(Self {
212            graph,
213            edge_lengths,
214            edge_weights,
215            source_vertex,
216            target_vertex,
217            weight_bound,
218        })
219    }
220
221    /// Get a reference to the underlying graph.
222    pub fn graph(&self) -> &G {
223        &self.graph
224    }
225
226    /// Get the edge lengths.
227    pub fn edge_lengths(&self) -> &[N] {
228        &self.edge_lengths
229    }
230
231    /// Get the edge weights.
232    pub fn edge_weights(&self) -> &[N] {
233        &self.edge_weights
234    }
235
236    /// Set new edge lengths.
237    pub fn set_lengths(&mut self, edge_lengths: Vec<N>) {
238        Self::check_edge_values(&self.graph, &edge_lengths, "edge lengths")
239            .unwrap_or_else(|error| panic!("{error}"));
240        self.edge_lengths = edge_lengths;
241    }
242
243    /// Set new edge weights.
244    pub fn set_weights(&mut self, edge_weights: Vec<N>) {
245        Self::check_edge_values(&self.graph, &edge_weights, "edge weights")
246            .unwrap_or_else(|error| panic!("{error}"));
247        self.edge_weights = edge_weights;
248    }
249
250    /// Get the source vertex.
251    pub fn source_vertex(&self) -> usize {
252        self.source_vertex
253    }
254
255    /// Get the target vertex.
256    pub fn target_vertex(&self) -> usize {
257        self.target_vertex
258    }
259
260    /// Get the weight bound.
261    pub fn weight_bound(&self) -> &N::Sum {
262        &self.weight_bound
263    }
264
265    /// Check whether this problem uses a non-unit weight type.
266    pub fn is_weighted(&self) -> bool {
267        !N::IS_UNIT
268    }
269
270    /// Smallest h >= 1 with every edge weight and the weight limit strictly below 2^h.
271    pub fn max_weight_bits(&self) -> u64 {
272        crate::types::max_numeric_magnitude_bits(
273            self.edge_weights
274                .iter()
275                .map(|weight| weight.to_sum())
276                .chain(std::iter::once(self.weight_bound.clone())),
277        )
278    }
279
280    /// Get the number of vertices in the graph.
281    pub fn num_vertices(&self) -> usize {
282        self.graph.num_vertices()
283    }
284
285    /// Get the number of edges in the graph.
286    pub fn num_edges(&self) -> usize {
287        self.graph.num_edges()
288    }
289
290    /// Check if a configuration is a valid weight-constrained s-t path.
291    ///
292    /// Returns `Some(total_length)` for a valid simple s-t path whose total
293    /// weight is within the weight bound, or `None` otherwise.
294    pub fn is_valid_solution(
295        &self,
296        config: &[bool],
297    ) -> Result<Option<N::Sum>, crate::traits::EvaluationError> {
298        if config.len() != self.graph.num_edges() {
299            return Ok(None);
300        }
301
302        if self.source_vertex == self.target_vertex {
303            if config.contains(&true) {
304                return Ok(None);
305            }
306            return Ok(Some(N::Sum::zero()));
307        }
308
309        let mut total_length = N::Sum::zero();
310        let mut total_weight = N::Sum::zero();
311
312        for (idx, &selected) in config.iter().enumerate() {
313            if !selected {
314                continue;
315            }
316            total_length = N::checked_add_to_sum(
317                total_length,
318                self.edge_lengths[idx].to_sum(),
319                "summing constrained path edge lengths",
320            )?;
321            total_weight = N::checked_add_to_sum(
322                total_weight,
323                self.edge_weights[idx].to_sum(),
324                "summing constrained path edge weights",
325            )?;
326        }
327
328        if total_weight > self.weight_bound.clone() {
329            return Ok(None);
330        }
331        if !is_simple_st_path(
332            self.graph.num_vertices(),
333            &self.graph.edges(),
334            self.source_vertex,
335            self.target_vertex,
336            config,
337        ) {
338            Ok(None)
339        } else {
340            Ok(Some(total_length))
341        }
342    }
343}
344
345impl<G, N> Problem for ShortestWeightConstrainedPath<G, N>
346where
347    G: Graph + crate::variant::VariantParam,
348    N: WeightElement + crate::variant::VariantParam,
349{
350    const NAME: &'static str = "ShortestWeightConstrainedPath";
351    type Solution = Vec<bool>;
352    type Value = Min<N::Sum>;
353
354    crate::problem_parameters![
355        ("max_weight_bits", max_weight_bits),
356        ("num_edges", num_edges),
357        ("num_vertices", num_vertices),
358    ];
359
360    fn variant() -> Vec<(&'static str, &'static str)> {
361        crate::variant_params![G, N]
362    }
363
364    fn evaluate(
365        &self,
366        config: &Self::Solution,
367    ) -> Result<Min<N::Sum>, crate::traits::EvaluationError> {
368        if config.len() != self.graph.num_edges() {
369            return Err(crate::traits::EvaluationError::InvalidConfiguration(
370                "edge-selection length does not match the graph".into(),
371            ));
372        }
373        Ok(Min(self.is_valid_solution(config)?))
374    }
375}
376
377impl<G, N> crate::solvers::BruteForceProblem for ShortestWeightConstrainedPath<G, N>
378where
379    G: Graph + crate::variant::VariantParam,
380    N: WeightElement + crate::variant::VariantParam,
381{
382    fn dimensions(&self) -> Vec<usize> {
383        vec![2; self.graph.num_edges()]
384    }
385}
386
387#[cfg(feature = "example-db")]
388pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
389    vec![crate::example_db::specs::ModelExampleSpec {
390        id: "shortest_weight_constrained_path_simplegraph",
391        instance: Box::new(ShortestWeightConstrainedPath::new(
392            SimpleGraph::new(
393                6,
394                vec![
395                    (0, 1),
396                    (0, 2),
397                    (1, 3),
398                    (2, 3),
399                    (2, 4),
400                    (3, 5),
401                    (4, 5),
402                    (1, 4),
403                ],
404            ),
405            vec![2, 4, 3, 1, 5, 4, 2, 6],
406            vec![5, 1, 2, 3, 2, 3, 1, 1],
407            0,
408            5,
409            8,
410        )),
411        optimal_config: serde_json::json!(vec![
412            false, true, false, true, false, true, false, false
413        ]),
414        optimal_value: serde_json::json!(9),
415    }]
416}
417
418crate::declare_variants! {
419    default ShortestWeightConstrainedPath<SimpleGraph, i64> => "2^num_edges" create ShortestWeightConstrainedPathCreateSpec,
420}
421
422crate::register_brute_force! {
423    ShortestWeightConstrainedPath<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
424}
425
426#[cfg(test)]
427#[path = "../../unit_tests/models/graph/shortest_weight_constrained_path.rs"]
428mod tests;