Skip to main content

sim_lib_discrete_graph/
control.rs

1//! Work, memory, deadline, and cancellation control for graph algorithms.
2
3use std::time::{Duration, Instant};
4
5use crate::GraphError;
6
7/// Work charged for one dynamic-programming cell and one examined edge.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct AlgorithmWorkCosts {
10    /// Work units charged before evaluating a table cell.
11    pub cell: u64,
12    /// Work units charged before examining a transition or residual edge.
13    pub edge: u64,
14}
15
16impl Default for AlgorithmWorkCosts {
17    fn default() -> Self {
18        Self { cell: 1, edge: 1 }
19    }
20}
21
22/// Bounds and accounting policy for a graph algorithm run.
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
24pub struct AlgorithmControl {
25    /// Optional maximum charged work.
26    pub max_work: Option<u64>,
27    /// Optional maximum simultaneously retained table cells.
28    pub max_memory_cells: Option<usize>,
29    /// Optional wall-clock deadline relative to the start of the call.
30    pub max_time: Option<Duration>,
31    /// Per-operation work costs.
32    pub costs: AlgorithmWorkCosts,
33}
34
35impl AlgorithmControl {
36    /// Returns a copy with a maximum work bound.
37    pub fn with_max_work(mut self, max_work: u64) -> Self {
38        self.max_work = Some(max_work);
39        self
40    }
41
42    /// Returns a copy with a maximum retained-cell bound.
43    pub fn with_max_memory_cells(mut self, max_memory_cells: usize) -> Self {
44        self.max_memory_cells = Some(max_memory_cells);
45        self
46    }
47
48    /// Returns a copy with a relative wall-clock deadline.
49    pub fn with_max_time(mut self, max_time: Duration) -> Self {
50        self.max_time = Some(max_time);
51        self
52    }
53
54    /// Returns a copy with explicit cell and edge charges.
55    pub fn with_costs(mut self, costs: AlgorithmWorkCosts) -> Self {
56        self.costs = costs;
57        self
58    }
59}
60
61/// Cooperative cancellation source for graph algorithms.
62pub trait AlgorithmInterrupt {
63    /// Returns true when the active computation should stop.
64    fn is_cancelled(&self) -> bool;
65}
66
67/// Cancellation source that never interrupts.
68#[derive(Clone, Copy, Debug, Default)]
69pub struct NeverInterrupt;
70
71impl AlgorithmInterrupt for NeverInterrupt {
72    fn is_cancelled(&self) -> bool {
73        false
74    }
75}
76
77/// Deterministic accounting evidence returned by a completed algorithm.
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct AlgorithmReceipt {
80    /// Total charged work.
81    pub work_used: u64,
82    /// Number of evaluated dynamic-programming cells.
83    pub cells: u64,
84    /// Number of examined transitions or residual edges.
85    pub edges: u64,
86    /// Maximum table cells retained simultaneously.
87    pub peak_memory_cells: usize,
88    /// Work charged per cell.
89    pub cell_work: u64,
90    /// Work charged per edge.
91    pub edge_work: u64,
92}
93
94impl AlgorithmReceipt {
95    pub(crate) fn validate(&self) -> Result<(), GraphError> {
96        let cell_work = self
97            .cells
98            .checked_mul(self.cell_work)
99            .ok_or_else(|| GraphError::CertificateInvalid("cell work overflowed".to_owned()))?;
100        let edge_work = self
101            .edges
102            .checked_mul(self.edge_work)
103            .ok_or_else(|| GraphError::CertificateInvalid("edge work overflowed".to_owned()))?;
104        let expected = cell_work
105            .checked_add(edge_work)
106            .ok_or_else(|| GraphError::CertificateInvalid("total work overflowed".to_owned()))?;
107        if self.cell_work == 0 || self.edge_work == 0 {
108            return Err(GraphError::CertificateInvalid(
109                "algorithm work charges must be positive".to_owned(),
110            ));
111        }
112        if self.work_used != expected {
113            return Err(GraphError::CertificateInvalid(
114                "algorithm receipt work total is inconsistent".to_owned(),
115            ));
116        }
117        Ok(())
118    }
119}
120
121pub(crate) struct WorkMeter<'a> {
122    control: &'a AlgorithmControl,
123    interrupt: &'a dyn AlgorithmInterrupt,
124    started: Instant,
125    receipt: AlgorithmReceipt,
126}
127
128impl<'a> WorkMeter<'a> {
129    pub(crate) fn new(
130        control: &'a AlgorithmControl,
131        interrupt: &'a dyn AlgorithmInterrupt,
132        peak_memory_cells: usize,
133    ) -> Result<Self, GraphError> {
134        if control.costs.cell == 0 || control.costs.edge == 0 {
135            return Err(GraphError::InvalidControl(
136                "cell and edge work costs must be positive".to_owned(),
137            ));
138        }
139        if control
140            .max_memory_cells
141            .is_some_and(|limit| peak_memory_cells > limit)
142        {
143            return Err(GraphError::ControlStopped(format!(
144                "memory-cell bound reached: required {peak_memory_cells}"
145            )));
146        }
147        Ok(Self {
148            control,
149            interrupt,
150            started: Instant::now(),
151            receipt: AlgorithmReceipt {
152                work_used: 0,
153                cells: 0,
154                edges: 0,
155                peak_memory_cells,
156                cell_work: control.costs.cell,
157                edge_work: control.costs.edge,
158            },
159        })
160    }
161
162    pub(crate) fn cell(&mut self) -> Result<(), GraphError> {
163        self.charge(self.control.costs.cell)?;
164        self.receipt.cells = self
165            .receipt
166            .cells
167            .checked_add(1)
168            .ok_or_else(|| GraphError::WeightOverflow("cell counter".to_owned()))?;
169        Ok(())
170    }
171
172    pub(crate) fn edge(&mut self) -> Result<(), GraphError> {
173        self.charge(self.control.costs.edge)?;
174        self.receipt.edges = self
175            .receipt
176            .edges
177            .checked_add(1)
178            .ok_or_else(|| GraphError::WeightOverflow("edge counter".to_owned()))?;
179        Ok(())
180    }
181
182    pub(crate) fn finish(self) -> AlgorithmReceipt {
183        self.receipt
184    }
185
186    fn charge(&mut self, work: u64) -> Result<(), GraphError> {
187        if self.interrupt.is_cancelled() {
188            return Err(GraphError::ControlStopped(
189                "algorithm interrupt cancelled the run".to_owned(),
190            ));
191        }
192        if self
193            .control
194            .max_time
195            .is_some_and(|limit| self.started.elapsed() >= limit)
196        {
197            return Err(GraphError::ControlStopped(
198                "algorithm time bound reached".to_owned(),
199            ));
200        }
201        let next = self
202            .receipt
203            .work_used
204            .checked_add(work)
205            .ok_or_else(|| GraphError::WeightOverflow("work counter".to_owned()))?;
206        if self.control.max_work.is_some_and(|limit| next > limit) {
207            return Err(GraphError::ControlStopped(
208                "algorithm work bound reached".to_owned(),
209            ));
210        }
211        self.receipt.work_used = next;
212        Ok(())
213    }
214}