Skip to main content

hugr_passes/
hash.rs

1//! Hugr hashing.
2
3use derive_more::{Display, Error};
4use fxhash::{FxHashMap, FxHasher64};
5use hugr_core::HugrView;
6use hugr_core::hugr::internal::PortgraphNodeMap;
7use hugr_core::ops::OpType;
8use hugr_core::ops::{OpTag, OpTrait};
9use petgraph::visit::{self as pg, Walker};
10use std::hash::{Hash, Hasher};
11
12/// Hugr hashing utilities.
13#[deprecated(
14    note = "`hugr-passes` is deprecated. Use tket::passes instead",
15    since = "0.26.2"
16)]
17pub trait HugrHash: HugrView {
18    /// Compute a hash for the entire HUGR structure.
19    ///
20    /// This corresponds to the hash of the root node.
21    fn hugr_hash(&self) -> Result<u64, HashError> {
22        self.region_hash(self.module_root())
23    }
24
25    /// Compute a hash for a hugr region.
26    ///
27    /// If the hugr is a dfg, we compute a hash for each node from its operation
28    /// and the hash of the predecessors. The hash of the hugr corresponds to
29    /// the hash of its output node.
30    /// Otherwise, we compute a generic hash combining the hashes of its children.
31    ///
32    /// This hash is independent from the children node order.
33    fn region_hash(&self, node: Self::Node) -> Result<u64, HashError>;
34}
35
36impl<H: HugrView> HugrHash for H {
37    fn region_hash(&self, node: Self::Node) -> Result<u64, HashError> {
38        let node_op = self.get_optype(node);
39        let mut hasher = FxHasher64::default();
40
41        let op: &OpType = self.get_optype(node);
42        hashable_op(op).hash(&mut hasher);
43
44        if OpTag::DataflowParent.is_superset(node_op.tag()) {
45            // In this case, we have a dataflow container
46            dfg_hash(self, node)?.hash(&mut hasher);
47        } else {
48            // otherwise, use generic hash
49            generic_hugr_hash(self, node)?.hash(&mut hasher);
50        }
51
52        Ok(hasher.finish())
53    }
54}
55
56fn dfg_hash<H: HugrView>(dfg_hugr: &H, node: H::Node) -> Result<u64, HashError> {
57    let mut node_hashes = HashState {
58        hashes: FxHashMap::default(),
59    };
60
61    let [_, output_node] = dfg_hugr.get_io(node).expect("DFG region missing I/O nodes");
62
63    let (region, node_map) = dfg_hugr.region_portgraph(node);
64    for pg_node in pg::Topo::new(&region).iter(&region) {
65        let child = node_map.from_portgraph(pg_node);
66        let hash = dfg_hash_node(dfg_hugr, child, &mut node_hashes)?;
67        if node_hashes.set_hash(child, hash).is_some() {
68            panic!("Hash already set for node {node}");
69        }
70    }
71
72    node_hashes
73        .get_hash(output_node)
74        .ok_or(HashError::CyclicDFG)
75}
76
77fn generic_hugr_hash<H: HugrView>(hugr: &H, node: H::Node) -> Result<u64, HashError> {
78    let mut child_hashes = Vec::new();
79
80    for child in hugr.children(node) {
81        child_hashes.push(hugr.region_hash(child)?);
82    }
83    // Combine child hashes in an order-independent way
84    child_hashes.sort_unstable();
85    Ok(fxhash::hash64(&child_hashes))
86}
87
88/// Auxiliary data for circuit hashing.
89///
90/// Contains previously computed hashes.
91#[derive(Clone, Default, Debug)]
92struct HashState<H: HugrView> {
93    /// Computed node hashes.
94    pub hashes: FxHashMap<H::Node, u64>,
95}
96impl<H: HugrView> HashState<H> {
97    /// Return the hash for a node.
98    #[inline]
99    fn get_hash(&self, node: H::Node) -> Option<u64> {
100        self.hashes.get(&node).copied()
101    }
102
103    /// Register the hash for a node.
104    ///
105    /// Returns the previous hash, if it was set.
106    #[inline]
107    fn set_hash(&mut self, node: H::Node, hash: u64) -> Option<u64> {
108        self.hashes.insert(node, hash)
109    }
110}
111
112/// Returns a hashable representation of an operation.
113///
114/// TODO(perf): String formatting here is a big bottleneck
115fn hashable_op(op: &OpType) -> impl Hash + use<> {
116    match op {
117        OpType::ExtensionOp(op) if !op.args().is_empty() => {
118            // TODO: Require hashing for TypeParams?
119            format!(
120                "{}[{}]",
121                op.def().name(),
122                serde_json::to_string(op.args()).unwrap()
123            )
124        }
125        OpType::OpaqueOp(op) if !op.args().is_empty() => {
126            format!(
127                "{}[{}]",
128                op.qualified_id(),
129                serde_json::to_string(op.args()).unwrap()
130            )
131        }
132        _ => op.to_string(),
133    }
134}
135
136/// Compute the hash of a circuit command.
137///
138/// Uses the hash of the operation and the node hash of its predecessors.
139///
140/// # Panics
141/// - If the command is a container node, or if it is a parametric CustomOp.
142/// - If the hash of any of its predecessors has not been set.
143fn dfg_hash_node<H: HugrView>(
144    hugr: &H,
145    node: H::Node,
146    state: &mut HashState<H>,
147) -> Result<u64, HashError> {
148    let mut hasher = FxHasher64::default();
149
150    hugr.region_hash(node)?.hash(&mut hasher);
151
152    // Add each each input neighbour hash, including the connected ports.
153    // TODO: Ignore state edges?
154    for input in hugr.node_inputs(node) {
155        // Combine the hash for each subport, ignoring their order.
156        let input_hash = hugr
157            .linked_ports(node, input)
158            .map(|(pred_node, pred_port)| {
159                let pred_node_hash = state.get_hash(pred_node);
160                fxhash::hash64(&(pred_node_hash, pred_port, input))
161            })
162            .fold(0, |total, hash| hash ^ total);
163        input_hash.hash(&mut hasher);
164    }
165    Ok(hasher.finish())
166}
167
168/// Errors that can occur while hashing a hugr.
169#[derive(Debug, Display, Clone, PartialEq, Eq, Error)]
170#[non_exhaustive]
171#[deprecated(
172    note = "`hugr-passes` is deprecated. Use tket::passes instead",
173    since = "0.26.2"
174)]
175pub enum HashError {
176    /// The hugr dfg contains a cycle.
177    #[display("The hugr dfg contains a cycle.")]
178    CyclicDFG,
179}
180
181#[cfg(test)]
182mod test {
183    use crate::utils::build_simple_hugr;
184    use crate::utils::test_quantum_extension::{cx_gate, h_gate};
185    use hugr_core::builder::{Dataflow, DataflowSubContainer};
186
187    use super::*;
188    #[test]
189    fn hash_equality() {
190        let hugr1 = build_simple_hugr(2, |mut f_build| {
191            // let wires = f_build.input_wires().map(Some).collect();
192
193            let mut linear = f_build.as_circuit(f_build.input_wires());
194
195            linear
196                .append(h_gate(), [0])?
197                .append(h_gate(), [1])?
198                .append(cx_gate(), [0, 1])?;
199
200            let outs = linear.finish();
201            f_build.finish_with_outputs(outs)
202        })
203        .unwrap();
204
205        let hash1 = hugr1.hugr_hash().unwrap();
206
207        // A circuit built in a different order should have the same hash
208        let hugr2 = build_simple_hugr(2, |mut f_build| {
209            let mut linear = f_build.as_circuit(f_build.input_wires());
210
211            linear
212                .append(h_gate(), [1])?
213                .append(h_gate(), [0])?
214                .append(cx_gate(), [0, 1])?;
215
216            let outs = linear.finish();
217            f_build.finish_with_outputs(outs)
218        })
219        .unwrap();
220
221        let hash2 = hugr2.hugr_hash().unwrap();
222
223        assert_eq!(hash1, hash2);
224
225        // Inverting the CX control and target should produce a different hash
226        let hugr3 = build_simple_hugr(2, |mut f_build| {
227            let mut linear = f_build.as_circuit(f_build.input_wires());
228
229            linear
230                .append(h_gate(), [1])?
231                .append(h_gate(), [0])?
232                .append(cx_gate(), [1, 0])?;
233
234            let outs = linear.finish();
235            f_build.finish_with_outputs(outs)
236        })
237        .unwrap();
238        let hash3 = hugr3.hugr_hash().unwrap();
239
240        assert_ne!(hash1, hash3);
241    }
242}