Skip to main content

sim_lib_discrete_graph/
layered.rs

1//! Certified shortest paths through staged state layers.
2
3// conformance: layered dynamic programming verifies stable backpointer
4// certificates, forbidden transitions, work bounds, and finite costs.
5
6use core::cmp::Ordering;
7
8use crate::{
9    AlgorithmControl, AlgorithmInterrupt, AlgorithmReceipt, FiniteCost, GraphError, NeverInterrupt,
10    control::WorkMeter,
11    cost::{add, compare, validate},
12};
13
14/// One Bellman-table cell in a layered shortest-path certificate.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct LayeredCell<C> {
17    /// Best total cost from the first layer through this state.
18    pub total_cost: Option<C>,
19    /// Stable index of the predecessor in the previous layer.
20    pub predecessor: Option<usize>,
21}
22
23/// Bellman values and backpointers proving a layered optimum.
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct LayeredCertificate<C> {
26    /// One row per input layer, in input state order.
27    pub layers: Vec<Vec<LayeredCell<C>>>,
28}
29
30/// A minimum-cost path selecting one state from every layer.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct LayeredPath<S, C> {
33    /// Selected state from each layer.
34    pub states: Vec<S>,
35    /// Stable input index selected from each layer.
36    pub indices: Vec<usize>,
37    /// Exact total transition cost.
38    pub total_cost: C,
39    /// Checkable Bellman values and backpointers.
40    pub certificate: LayeredCertificate<C>,
41    /// Deterministic cell/edge work accounting.
42    pub receipt: AlgorithmReceipt,
43}
44
45/// Finds the minimum-cost path selecting one state from every non-empty layer.
46///
47/// Every transition is allowed. Equal-cost candidates prefer the lower
48/// predecessor index, and equal-cost endpoints prefer the lower final index.
49pub fn layered_shortest_path<S: Clone, C: FiniteCost>(
50    layers: &[Vec<S>],
51    cost: impl Fn(&S, &S) -> C,
52) -> Result<LayeredPath<S, C>, GraphError> {
53    layered_shortest_path_with_control(
54        layers,
55        |left, right| Some(cost(left, right)),
56        &AlgorithmControl::default(),
57        &NeverInterrupt,
58    )
59}
60
61/// Finds a layered shortest path with forbidden transitions and explicit
62/// resource control.
63///
64/// The transition callback returns `None` for a forbidden edge.
65pub fn layered_shortest_path_with_control<S: Clone, C: FiniteCost>(
66    layers: &[Vec<S>],
67    transition: impl Fn(&S, &S) -> Option<C>,
68    control: &AlgorithmControl,
69    interrupt: &dyn AlgorithmInterrupt,
70) -> Result<LayeredPath<S, C>, GraphError> {
71    validate_layers(layers)?;
72    let memory = layer_cell_count(layers)?;
73    let mut meter = WorkMeter::new(control, interrupt, memory)?;
74    let certificate = build_certificate(layers, &transition, Some(&mut meter))?;
75    let (indices, total_cost) = reconstruct(&certificate)?;
76    let states = indices
77        .iter()
78        .enumerate()
79        .map(|(layer, index)| layers[layer][*index].clone())
80        .collect();
81    Ok(LayeredPath {
82        states,
83        indices,
84        total_cost,
85        certificate,
86        receipt: meter.finish(),
87    })
88}
89
90/// Verifies the Bellman recurrence, stable backpointers, selected states, and
91/// accounting evidence of a layered shortest path.
92pub fn verify_layered_path<S: Clone + PartialEq, C: FiniteCost>(
93    layers: &[Vec<S>],
94    transition: impl Fn(&S, &S) -> Option<C>,
95    path: &LayeredPath<S, C>,
96) -> Result<(), GraphError> {
97    validate_layers(layers)?;
98    path.receipt.validate()?;
99    let expected = build_certificate(layers, &transition, None)?;
100    if path.certificate != expected {
101        return Err(GraphError::CertificateInvalid(
102            "layered certificate violates the Bellman recurrence or stable ties".to_owned(),
103        ));
104    }
105    let (indices, total_cost) = reconstruct(&expected)?;
106    if path.indices != indices || path.total_cost != total_cost {
107        return Err(GraphError::CertificateInvalid(
108            "layered path does not select the certified optimum".to_owned(),
109        ));
110    }
111    let states = indices
112        .iter()
113        .enumerate()
114        .map(|(layer, index)| layers[layer][*index].clone())
115        .collect::<Vec<_>>();
116    if path.states != states {
117        return Err(GraphError::CertificateInvalid(
118            "layered path states do not match their certified indices".to_owned(),
119        ));
120    }
121    verify_receipt(layers, &path.receipt)
122}
123
124fn build_certificate<S, C: FiniteCost>(
125    layers: &[Vec<S>],
126    transition: &impl Fn(&S, &S) -> Option<C>,
127    mut meter: Option<&mut WorkMeter<'_>>,
128) -> Result<LayeredCertificate<C>, GraphError> {
129    let mut table = Vec::with_capacity(layers.len());
130    let mut first = Vec::with_capacity(layers[0].len());
131    for _ in &layers[0] {
132        charge_cell(&mut meter)?;
133        first.push(LayeredCell {
134            total_cost: Some(C::zero()),
135            predecessor: None,
136        });
137    }
138    table.push(first);
139
140    for layer_index in 1..layers.len() {
141        let previous_states = &layers[layer_index - 1];
142        let current_states = &layers[layer_index];
143        let previous_cells = &table[layer_index - 1];
144        let mut current_cells = Vec::with_capacity(current_states.len());
145        for current in current_states {
146            charge_cell(&mut meter)?;
147            let mut best: Option<(C, usize)> = None;
148            for (predecessor, previous) in previous_states.iter().enumerate() {
149                charge_edge(&mut meter)?;
150                let Some(previous_cost) = &previous_cells[predecessor].total_cost else {
151                    continue;
152                };
153                let Some(edge_cost) = transition(previous, current) else {
154                    continue;
155                };
156                validate(&edge_cost, "layered transition")?;
157                let candidate = add(previous_cost, &edge_cost, "layered path transition")?;
158                let replace = match &best {
159                    Some((best_cost, _)) => {
160                        compare(&candidate, best_cost, "layered path ordering")? == Ordering::Less
161                    }
162                    None => true,
163                };
164                if replace {
165                    best = Some((candidate, predecessor));
166                }
167            }
168            current_cells.push(match best {
169                Some((total_cost, predecessor)) => LayeredCell {
170                    total_cost: Some(total_cost),
171                    predecessor: Some(predecessor),
172                },
173                None => LayeredCell {
174                    total_cost: None,
175                    predecessor: None,
176                },
177            });
178        }
179        table.push(current_cells);
180    }
181    Ok(LayeredCertificate { layers: table })
182}
183
184fn reconstruct<C: FiniteCost>(
185    certificate: &LayeredCertificate<C>,
186) -> Result<(Vec<usize>, C), GraphError> {
187    let last = certificate.layers.last().ok_or_else(|| {
188        GraphError::Unsupported("layered path requires at least one layer".to_owned())
189    })?;
190    let mut endpoint: Option<(usize, &C)> = None;
191    for (index, cell) in last.iter().enumerate() {
192        let Some(cost) = &cell.total_cost else {
193            continue;
194        };
195        let replace = match endpoint {
196            Some((_, best)) => compare(cost, best, "layered endpoint ordering")? == Ordering::Less,
197            None => true,
198        };
199        if replace {
200            endpoint = Some((index, cost));
201        }
202    }
203    let (mut index, total_cost) = endpoint.ok_or(GraphError::Disconnected)?;
204    let mut indices = vec![0; certificate.layers.len()];
205    for layer in (0..certificate.layers.len()).rev() {
206        indices[layer] = index;
207        if layer > 0 {
208            index = certificate.layers[layer][index]
209                .predecessor
210                .ok_or_else(|| {
211                    GraphError::CertificateInvalid(
212                        "reachable layered cell has no backpointer".to_owned(),
213                    )
214                })?;
215        }
216    }
217    Ok((indices, total_cost.clone()))
218}
219
220fn validate_layers<S>(layers: &[Vec<S>]) -> Result<(), GraphError> {
221    if layers.is_empty() {
222        return Err(GraphError::Unsupported(
223            "layered path requires at least one layer".to_owned(),
224        ));
225    }
226    if layers.iter().any(Vec::is_empty) {
227        return Err(GraphError::Disconnected);
228    }
229    layer_cell_count(layers).map(|_| ())
230}
231
232fn layer_cell_count<S>(layers: &[Vec<S>]) -> Result<usize, GraphError> {
233    layers.iter().try_fold(0usize, |total, layer| {
234        total
235            .checked_add(layer.len())
236            .ok_or_else(|| GraphError::WeightOverflow("layer cell count".to_owned()))
237    })
238}
239
240fn verify_receipt<S>(layers: &[Vec<S>], receipt: &AlgorithmReceipt) -> Result<(), GraphError> {
241    let cells = u64::try_from(layer_cell_count(layers)?)
242        .map_err(|_| GraphError::CertificateInvalid("layer cell count exceeds u64".to_owned()))?;
243    let edges = layers.windows(2).try_fold(0u64, |total, pair| {
244        let count = pair[0]
245            .len()
246            .checked_mul(pair[1].len())
247            .and_then(|count| u64::try_from(count).ok())
248            .ok_or_else(|| {
249                GraphError::CertificateInvalid("layer edge count exceeds u64".to_owned())
250            })?;
251        total
252            .checked_add(count)
253            .ok_or_else(|| GraphError::CertificateInvalid("layer edge count overflowed".to_owned()))
254    })?;
255    if receipt.cells != cells
256        || receipt.edges != edges
257        || receipt.peak_memory_cells != layer_cell_count(layers)?
258    {
259        return Err(GraphError::CertificateInvalid(
260            "layered path receipt counts do not match the input".to_owned(),
261        ));
262    }
263    Ok(())
264}
265
266fn charge_cell(meter: &mut Option<&mut WorkMeter<'_>>) -> Result<(), GraphError> {
267    if let Some(meter) = meter.as_deref_mut() {
268        meter.cell()?;
269    }
270    Ok(())
271}
272
273fn charge_edge(meter: &mut Option<&mut WorkMeter<'_>>) -> Result<(), GraphError> {
274    if let Some(meter) = meter.as_deref_mut() {
275        meter.edge()?;
276    }
277    Ok(())
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn staged_path_has_stable_backpointer_certificate() {
286        let layers = vec![vec!['a', 'b'], vec!['c', 'd'], vec!['e']];
287        let path = layered_shortest_path(&layers, |left, right| {
288            if (*left, *right) == ('a', 'd') || (*left, *right) == ('d', 'e') {
289                1_i64
290            } else {
291                5
292            }
293        })
294        .expect("layered path");
295
296        assert_eq!(path.indices, vec![0, 1, 0]);
297        assert_eq!(path.states, vec!['a', 'd', 'e']);
298        assert_eq!(path.total_cost, 2);
299        assert_eq!(path.receipt.cells, 5);
300        assert_eq!(path.receipt.edges, 6);
301        verify_layered_path(
302            &layers,
303            |left, right| {
304                Some(
305                    if (*left, *right) == ('a', 'd') || (*left, *right) == ('d', 'e') {
306                        1_i64
307                    } else {
308                        5
309                    },
310                )
311            },
312            &path,
313        )
314        .expect("certificate");
315    }
316
317    #[test]
318    fn forbidden_edges_bounds_and_non_finite_costs_fail_closed() {
319        let layers = vec![vec![0, 1], vec![2], vec![3]];
320        let path = layered_shortest_path_with_control(
321            &layers,
322            |left, right| (*left != 0 || *right != 2).then_some(1_i64),
323            &AlgorithmControl::default(),
324            &NeverInterrupt,
325        )
326        .expect("reachable through state one");
327        assert_eq!(path.indices, vec![1, 0, 0]);
328
329        assert!(matches!(
330            layered_shortest_path_with_control(
331                &layers,
332                |_left, _right| Some(1_i64),
333                &AlgorithmControl::default().with_max_work(2),
334                &NeverInterrupt,
335            ),
336            Err(GraphError::ControlStopped(_))
337        ));
338        assert!(matches!(
339            layered_shortest_path(&layers, |_left, _right| f64::INFINITY),
340            Err(GraphError::NonFiniteCost(_))
341        ));
342    }
343
344    #[test]
345    fn tampered_backpointer_fails_verification() {
346        let layers = vec![vec![0, 1], vec![2, 3]];
347        let mut path = layered_shortest_path(&layers, |_left, _right| 1_i64).expect("layered path");
348        path.certificate.layers[1][0].predecessor = Some(1);
349        assert!(matches!(
350            verify_layered_path(&layers, |_left, _right| Some(1_i64), &path),
351            Err(GraphError::CertificateInvalid(_))
352        ));
353    }
354}