Skip to main content

sim_lib_discrete_search/
receipt.rs

1//! Search receipts and run summaries.
2
3use crate::SearchControl;
4
5/// Completion status recorded by a bounded search run.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum SearchStatus {
8    /// The frontier exhausted after emitting at least one output.
9    Complete,
10    /// The run stopped at a configured bound and may have partial outputs.
11    Partial,
12    /// The interrupt source cancelled the run.
13    Cancelled,
14    /// The frontier exhausted without any output.
15    Infeasible,
16}
17
18/// Receipt describing how a bounded search run ended.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct SearchReceipt {
21    /// Final run status.
22    pub status: SearchStatus,
23    /// Stable reason for non-complete statuses.
24    pub reason: Option<String>,
25    /// Total charged work.
26    pub work_used: u64,
27    /// Number of states expanded.
28    pub expanded: u64,
29    /// Number of states scored.
30    pub scored: u64,
31    /// Number of states propagated.
32    pub propagated: u64,
33    /// Number of outputs emitted.
34    pub emitted: u64,
35    /// Number of locally pruned prefixes or beam drops.
36    pub pruned: u64,
37    /// Largest observed frontier length.
38    pub max_frontier: usize,
39    /// Number of outputs returned to the caller.
40    pub result_count: usize,
41    /// Caller-supplied deterministic seed.
42    pub seed: u64,
43    /// Stable digest of the control policy.
44    pub policy_digest: String,
45    /// Stable digest of the policy, metrics, status, and output order.
46    pub digest: String,
47}
48
49impl SearchReceipt {
50    pub(crate) fn invalid(control: &SearchControl, reason: String) -> Self {
51        Self::finalize(
52            SearchStatus::Partial,
53            Some(format!("invalid control: {reason}")),
54            Metrics::default(),
55            control,
56            &[],
57        )
58    }
59
60    pub(crate) fn finalize(
61        status: SearchStatus,
62        reason: Option<String>,
63        metrics: Metrics,
64        control: &SearchControl,
65        output_material: &[String],
66    ) -> Self {
67        let policy_material = control.policy_material();
68        let policy_digest = stable_digest(&[policy_material.as_str()]);
69        let status_material = format!("{status:?}");
70        let reason_material = reason.clone().unwrap_or_default();
71        let metrics_material = format!(
72            "work={};expanded={};scored={};propagated={};emitted={};pruned={};max_frontier={};result_count={}",
73            metrics.work_used,
74            metrics.expanded,
75            metrics.scored,
76            metrics.propagated,
77            metrics.emitted,
78            metrics.pruned,
79            metrics.max_frontier,
80            metrics.result_count,
81        );
82        let mut parts = vec![
83            policy_material.as_str(),
84            status_material.as_str(),
85            reason_material.as_str(),
86            metrics_material.as_str(),
87        ];
88        parts.extend(output_material.iter().map(String::as_str));
89        let digest = stable_digest(&parts);
90
91        Self {
92            status,
93            reason,
94            work_used: metrics.work_used,
95            expanded: metrics.expanded,
96            scored: metrics.scored,
97            propagated: metrics.propagated,
98            emitted: metrics.emitted,
99            pruned: metrics.pruned,
100            max_frontier: metrics.max_frontier,
101            result_count: metrics.result_count,
102            seed: control.seed,
103            policy_digest,
104            digest,
105        }
106    }
107}
108
109/// Outputs and receipt produced by one search run.
110#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct SearchRun<Output> {
112    /// Outputs emitted before the run stopped.
113    pub outputs: Vec<Output>,
114    /// Receipt covering status, bounds, work, and deterministic digests.
115    pub receipt: SearchReceipt,
116}
117
118/// Mutable counters used while solving.
119#[derive(Clone, Debug, Default, PartialEq, Eq)]
120pub(crate) struct Metrics {
121    pub(crate) work_used: u64,
122    pub(crate) expanded: u64,
123    pub(crate) scored: u64,
124    pub(crate) propagated: u64,
125    pub(crate) emitted: u64,
126    pub(crate) pruned: u64,
127    pub(crate) max_frontier: usize,
128    pub(crate) result_count: usize,
129}
130
131pub(crate) fn stable_digest(parts: &[&str]) -> String {
132    let mut hash = 0xcbf29ce484222325u64;
133    for part in parts {
134        for byte in part.as_bytes() {
135            hash ^= u64::from(*byte);
136            hash = hash.wrapping_mul(0x100000001b3);
137        }
138        hash ^= 0xff;
139        hash = hash.wrapping_mul(0x100000001b3);
140    }
141    format!("{hash:016x}")
142}