From f02710ff115e518a206343c9c809ac49bac903f0 Mon Sep 17 00:00:00 2001 From: seyon Date: Wed, 5 Apr 2023 10:55:19 +0100 Subject: [PATCH 1/4] add HUGR (de)serialization --- Cargo.toml | 4 + src/hugr.rs | 4 +- src/hugr/serialize.rs | 199 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/hugr/serialize.rs diff --git a/Cargo.toml b/Cargo.toml index a08061ad3..1a065e5db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,10 @@ pyo3 = ["dep:pyo3"] [dev-dependencies] criterion = { version = "0.4.0", features = ["html_reports"] } rstest = "0.16.0" +portgraph = { git = "http://github.com/CQCL/portgraph", features = ["proptest"] } +proptest = "1.1.0" +rmp-serde = "1.1.1" + [[bench]] name = "bench_main" diff --git a/src/hugr.rs b/src/hugr.rs index 8535988c9..8bb43dc66 100644 --- a/src/hugr.rs +++ b/src/hugr.rs @@ -9,8 +9,10 @@ use thiserror::Error; use crate::ops::{ModuleOp, Op, OpType}; use crate::rewrite::{Rewrite, RewriteError}; +pub mod serialize; + /// The Hugr data structure. -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq)] pub struct Hugr { /// The graph encoding the adjacency structure of the HUGR. pub(crate) graph: PortGraph, diff --git a/src/hugr/serialize.rs b/src/hugr/serialize.rs new file mode 100644 index 000000000..7aa9be4e5 --- /dev/null +++ b/src/hugr/serialize.rs @@ -0,0 +1,199 @@ +//! Serialization definition for [`Hugr`] +//! [`Hugr`]: crate::hugr::Hugr + +use std::collections::HashMap; + +use crate::{hugr::Hugr, ops::OpType}; +use portgraph::{Direction, Hierarchy, NodeIndex, PortGraph, PortIndex, SecondaryMap}; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Serialize, Deserialize)] +struct SerHugr { + nodes: Vec<(NodeIndex, usize, usize)>, + edges: Vec<[(NodeIndex, usize); 2]>, + root: NodeIndex, + op_types: HashMap, +} + +impl From<&Hugr> for SerHugr { + fn from( + Hugr { + graph, + hierarchy, + root, + op_types, + }: &Hugr, + ) -> Self { + let mut op_types_hsh = HashMap::new(); + let nodes: Vec<_> = graph + .nodes_iter() + .enumerate() + .map(|(i, n)| { + assert_eq!(i, n.index(), "can't serialize a non-compact graph"); + let parent = hierarchy.parent(n).unwrap_or_else(|| { + assert_eq!(*root, n); + n + }); + let opt = &op_types[n]; + if opt != &OpType::default() { + op_types_hsh.insert(n, opt.clone()); + } + (parent, graph.num_inputs(n), graph.num_outputs(n)) + }) + .collect(); + + let find_offset = |p: PortIndex| { + ( + graph.port_node(p).unwrap(), + graph.port_offset(p).unwrap().index(), + ) + }; + + let edges: Vec<_> = graph + .ports_iter() + .filter_map(|p| { + if graph.port_direction(p) == Some(Direction::Outgoing) { + let tgt = graph.port_link(p)?; + let np = [p, tgt].map(find_offset); + Some(np) + } else { + None + } + }) + .collect(); + + Self { + nodes, + edges, + root: *root, + op_types: op_types_hsh, + } + } +} + +impl From for Hugr { + fn from( + SerHugr { + nodes, + edges, + root, + mut op_types, + }: SerHugr, + ) -> Self { + let mut hierarchy = Hierarchy::new(); + + // if there are any unconnected ports the capacity will be an + // underestimate + let mut graph = PortGraph::with_capacity(nodes.len(), edges.len() * 2); + let mut op_types_sec = SecondaryMap::new(); + for (parent, incoming, outgoing) in nodes { + let ni = graph.add_node(incoming, outgoing); + if parent != ni { + hierarchy + .push_child(ni, parent) + .expect("Unexpected hierarchy error"); // TODO remove unwrap + } + if let Some(typ) = op_types.remove(&ni) { + op_types_sec[ni] = typ; + } + } + + for [(srcn, from_offset), (tgtn, to_offset)] in edges { + graph + .link_nodes(srcn, from_offset, tgtn, to_offset) + .expect("Unexpected link error"); + } + + Self { + graph, + hierarchy, + root, + op_types: op_types_sec, + } + } +} + +impl Serialize for Hugr { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let shg: SerHugr = self.into(); + shg.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Hugr { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let shg = SerHugr::deserialize(deserializer)?; + Ok(shg.into()) + } +} + +#[cfg(test)] +pub mod test { + + use super::*; + use portgraph::proptest::gen_portgraph; + use proptest::prelude::*; + proptest! { + #[test] + fn prop_serialization(mut graph in gen_portgraph(100, 50, 1000)) { + let root = graph.add_node(0, 0); + let mut hierarchy = Hierarchy::new(); + for n in graph.nodes_iter() { + if n != root { + hierarchy.push_child(n, root).unwrap(); + } + } + + let hgraph = Hugr { graph, hierarchy, root, ..Default::default()}; + + prop_assert_eq!(ser_roundtrip(&hgraph), hgraph); + } + } + + #[test] + fn empty_hugr_serialize() { + let hg = Hugr::default(); + assert_eq!(ser_roundtrip(&hg), hg); + } + pub fn ser_roundtrip(g: &T) -> T { + let v = rmp_serde::to_vec_named(g).unwrap(); + rmp_serde::from_slice(&v[..]).unwrap() + } + + #[test] + fn simpleser() { + let mut g = PortGraph::new(); + let a = g.add_node(1, 1); + let b = g.add_node(3, 2); + let c = g.add_node(1, 1); + let root = g.add_node(0, 0); + + g.link_nodes(a, 0, b, 0).unwrap(); + g.link_nodes(b, 0, b, 1).unwrap(); + g.link_nodes(b, 1, c, 0).unwrap(); + g.link_nodes(c, 0, a, 0).unwrap(); + + let mut h = Hierarchy::new(); + + for n in [a, b, c] { + h.push_child(n, root).unwrap(); + } + let hg = Hugr { + graph: g, + hierarchy: h, + root, + op_types: SecondaryMap::new(), + }; + + let v = rmp_serde::to_vec_named(&hg).unwrap(); + + let newhg = rmp_serde::from_slice(&v[..]).unwrap(); + assert_eq!(hg, newhg); + } +} From 11d82e55783b07bdf61f3783c0d72a34fa69b6b2 Mon Sep 17 00:00:00 2001 From: seyon Date: Wed, 12 Apr 2023 13:07:18 +0100 Subject: [PATCH 2/4] add comment about secondary map defaults --- src/hugr/serialize.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hugr/serialize.rs b/src/hugr/serialize.rs index 7aa9be4e5..c03909fc3 100644 --- a/src/hugr/serialize.rs +++ b/src/hugr/serialize.rs @@ -35,6 +35,9 @@ impl From<&Hugr> for SerHugr { n }); let opt = &op_types[n]; + // secondary map holds default values for empty positions + // whether or not the default value is present or not - the + // serializaion roundtrip will be correct if opt != &OpType::default() { op_types_hsh.insert(n, opt.clone()); } From 4551ca05ec027071ad14665e602b22d8c163fe34 Mon Sep 17 00:00:00 2001 From: seyon Date: Thu, 13 Apr 2023 10:29:24 +0100 Subject: [PATCH 3/4] implement try_from for serialize --- src/hugr/serialize.rs | 65 ++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/hugr/serialize.rs b/src/hugr/serialize.rs index c03909fc3..24c27d9cb 100644 --- a/src/hugr/serialize.rs +++ b/src/hugr/serialize.rs @@ -2,9 +2,13 @@ //! [`Hugr`]: crate::hugr::Hugr use std::collections::HashMap; +use thiserror::Error; use crate::{hugr::Hugr, ops::OpType}; -use portgraph::{Direction, Hierarchy, NodeIndex, PortGraph, PortIndex, SecondaryMap}; +use portgraph::{ + hierarchy::AttachError, Direction, Hierarchy, LinkError, NodeIndex, PortGraph, PortIndex, + SecondaryMap, +}; use serde::{Deserialize, Deserializer, Serialize}; #[derive(Serialize, Deserialize)] @@ -15,21 +19,39 @@ struct SerHugr { op_types: HashMap, } -impl From<&Hugr> for SerHugr { - fn from( +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum HUGRSerializationError { + /// Cannot serialize a non-compact graph. + #[error("Cannot serialize a non-compact graph (node indices must be contiguous).")] + NonCompactGraph, + /// Unexpected hierarchy error. + #[error("Failed to attach child to parent: {0:?}.")] + AttachError(#[from] AttachError), + /// Failed to add edge. + #[error("Failed to build edge when deserializing: {0:?}.")] + LinkError(#[from] LinkError), +} + +impl TryFrom<&Hugr> for SerHugr { + type Error = HUGRSerializationError; + + fn try_from( Hugr { graph, hierarchy, root, op_types, }: &Hugr, - ) -> Self { + ) -> Result { let mut op_types_hsh = HashMap::new(); - let nodes: Vec<_> = graph + let nodes: Result, HUGRSerializationError> = graph .nodes_iter() .enumerate() .map(|(i, n)| { - assert_eq!(i, n.index(), "can't serialize a non-compact graph"); + if i != n.index() { + return Err(HUGRSerializationError::NonCompactGraph); + } + let parent = hierarchy.parent(n).unwrap_or_else(|| { assert_eq!(*root, n); n @@ -41,9 +63,10 @@ impl From<&Hugr> for SerHugr { if opt != &OpType::default() { op_types_hsh.insert(n, opt.clone()); } - (parent, graph.num_inputs(n), graph.num_outputs(n)) + Ok((parent, graph.num_inputs(n), graph.num_outputs(n))) }) .collect(); + let nodes = nodes?; let find_offset = |p: PortIndex| { ( @@ -65,24 +88,24 @@ impl From<&Hugr> for SerHugr { }) .collect(); - Self { + Ok(Self { nodes, edges, root: *root, op_types: op_types_hsh, - } + }) } } - -impl From for Hugr { - fn from( +impl TryFrom for Hugr { + type Error = HUGRSerializationError; + fn try_from( SerHugr { nodes, edges, root, mut op_types, }: SerHugr, - ) -> Self { + ) -> Result { let mut hierarchy = Hierarchy::new(); // if there are any unconnected ports the capacity will be an @@ -92,9 +115,7 @@ impl From for Hugr { for (parent, incoming, outgoing) in nodes { let ni = graph.add_node(incoming, outgoing); if parent != ni { - hierarchy - .push_child(ni, parent) - .expect("Unexpected hierarchy error"); // TODO remove unwrap + hierarchy.push_child(ni, parent)?; // TODO remove unwrap } if let Some(typ) = op_types.remove(&ni) { op_types_sec[ni] = typ; @@ -102,17 +123,15 @@ impl From for Hugr { } for [(srcn, from_offset), (tgtn, to_offset)] in edges { - graph - .link_nodes(srcn, from_offset, tgtn, to_offset) - .expect("Unexpected link error"); + graph.link_nodes(srcn, from_offset, tgtn, to_offset)?; } - Self { + Ok(Self { graph, hierarchy, root, op_types: op_types_sec, - } + }) } } @@ -121,7 +140,7 @@ impl Serialize for Hugr { where S: serde::Serializer, { - let shg: SerHugr = self.into(); + let shg: SerHugr = self.try_into().map_err(serde::ser::Error::custom)?; shg.serialize(serializer) } } @@ -132,7 +151,7 @@ impl<'de> Deserialize<'de> for Hugr { D: Deserializer<'de>, { let shg = SerHugr::deserialize(deserializer)?; - Ok(shg.into()) + shg.try_into().map_err(serde::de::Error::custom) } } From 23bae83d365e410d0e3ab0e63c18b95c7897a1a8 Mon Sep 17 00:00:00 2001 From: seyon Date: Thu, 13 Apr 2023 10:52:51 +0100 Subject: [PATCH 4/4] miri ignore proptest --- src/hugr/serialize.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hugr/serialize.rs b/src/hugr/serialize.rs index 24c27d9cb..344b6bf91 100644 --- a/src/hugr/serialize.rs +++ b/src/hugr/serialize.rs @@ -96,6 +96,7 @@ impl TryFrom<&Hugr> for SerHugr { }) } } + impl TryFrom for Hugr { type Error = HUGRSerializationError; fn try_from( @@ -163,6 +164,8 @@ pub mod test { use proptest::prelude::*; proptest! { #[test] + // miri fails due to proptest filesystem access + #[cfg_attr(miri, ignore)] fn prop_serialization(mut graph in gen_portgraph(100, 50, 1000)) { let root = graph.add_node(0, 0); let mut hierarchy = Hierarchy::new();