From ea66225a9f86d7590402c3cb3b9a7e87b3ce409a Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Wed, 20 Aug 2025 17:21:35 +0200 Subject: [PATCH 1/3] feat: Add ResourceScope::is_convex --- tket/src/resource.rs | 1 + tket/src/resource/convex_checker.rs | 119 ++++++++++++++++++++++++++++ tket/src/resource/interval.rs | 8 ++ tket/src/resource/scope.rs | 5 ++ tket/src/rewrite/strategy.rs | 3 - tket/src/subcircuit.rs | 9 ++- 6 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 tket/src/resource/convex_checker.rs diff --git a/tket/src/resource.rs b/tket/src/resource.rs index f6ac4db71..3692e9883 100644 --- a/tket/src/resource.rs +++ b/tket/src/resource.rs @@ -50,6 +50,7 @@ pub use scope::{ResourceScope, ResourceScopeConfig}; pub use types::{CircuitUnit, Position, ResourceAllocator, ResourceId}; // Internal modules +mod convex_checker; mod flow; mod interval; mod scope; diff --git a/tket/src/resource/convex_checker.rs b/tket/src/resource/convex_checker.rs new file mode 100644 index 000000000..8c162fc56 --- /dev/null +++ b/tket/src/resource/convex_checker.rs @@ -0,0 +1,119 @@ +//! Use [`ResourceScope`] to check whether a subcircuit is convex. + +use std::collections::{BTreeSet, VecDeque}; + +use hugr::{Direction, HugrView}; + +use crate::Subcircuit; + +use super::ResourceScope; + +impl ResourceScope { + /// Check if the given subcircuit is convex. + /// + /// A subcircuit is convex if there is no path from a circuit output to a + /// circuit input. + pub fn is_convex(&self, subcircuit: Subcircuit) -> bool { + let Some(max_start_pos) = subcircuit + .intervals_iter() + .map(|interval| interval.start_pos()) + .max() + else { + // An empty subcircuit is convex + return true; + }; + + let mut future_nodes = + VecDeque::from_iter(subcircuit.intervals_iter().filter_map(|interval| { + let last_node = interval.end_node(); + self.resource_path_iter(interval.resource_id(), last_node, Direction::Outgoing) + .nth(1) + })); + let mut visited = BTreeSet::new(); + + // We must prove that all nodes in `future_nodes` are not in the past + // of any node at the beginning of a line interval. + while let Some(node) = future_nodes.pop_front() { + let pos = self.get_position(node).expect("known node"); + if pos > max_start_pos { + // we cannot be in the past of any node at the beginning of a + // line interval, so we can stop searching + continue; + } + if !visited.insert(node) { + continue; // been here before + } + for resource_id in self.get_all_resources(node) { + if let Some(interval) = subcircuit.get_interval(resource_id) { + debug_assert!( + pos < interval.start_pos() || pos > interval.end_pos(), + "node cannot be in interval [min, max]" + ); + if pos < interval.start_pos() { + // we are in the past of min, so there is a path from + // an output to an input! -> not convex + return false; + } + } + } + + future_nodes.extend( + self.hugr() + .output_neighbours(node) + .filter(|&nei| self.contains_node(nei)), + ); + } + + true + } +} + +#[cfg(test)] +mod tests { + use crate::{utils::build_simple_circuit, Circuit, TketOp}; + + use super::*; + + use rstest::rstest; + + // A circuit made of two CX ladders (v-shape) + // - first ladder is (0, 1), (1, 2), etc + // - second ladder is (n_qubits - 1, n_qubits - 2), (n_qubits - 2, n_qubits - 3), etc + fn cx_ladder(n_qubits: usize) -> Circuit { + build_simple_circuit(n_qubits, |circ| { + for i in 0..n_qubits - 1 { + circ.append(TketOp::CX, [i, i + 1]).unwrap(); + } + for i in (1..n_qubits).rev() { + circ.append(TketOp::CX, [i, i - 1]).unwrap(); + } + Ok(()) + }) + .unwrap() + } + + // Any sequence of non-contiguous node indices will be non-convex. + // Note that for a lot of non-convex cases, subcircuit construction will + // fail. We do not include these cases here. + #[rstest] + #[case(vec![0, 1], true)] + #[case(vec![0, 1, 2, 3, 4], true)] + #[case(vec![4, 5, 6], true)] + #[case(vec![3, 4], true)] + #[case(vec![3, 4, 5, 6, 7], true)] + #[case(vec![0, 2], false)] + #[case(vec![0, 1, 3, 4], false)] + #[case(vec![0, 1, 4], false)] + #[case(vec![3, 6, 7], false)] + fn test_is_convex(#[case] selected_nodes: Vec, #[case] is_convex: bool) { + let circ = cx_ladder(5); + let subgraph = circ.subgraph(); + let cx_nodes = subgraph.nodes(); + let circ = ResourceScope::from(circ); + let selected_nodes = selected_nodes.into_iter().map(|i| cx_nodes[i]); + + let subcirc = Subcircuit::try_from_nodes(selected_nodes, &circ).unwrap(); + + assert_eq!(circ.is_convex(subcirc), is_convex); + } +} diff --git a/tket/src/resource/interval.rs b/tket/src/resource/interval.rs index a263ebdd2..95634ebe7 100644 --- a/tket/src/resource/interval.rs +++ b/tket/src/resource/interval.rs @@ -93,6 +93,14 @@ impl Interval { self.nodes[0] } + pub(crate) fn start_pos(&self) -> Position { + self.positions[0] + } + + pub(crate) fn end_pos(&self) -> Position { + self.positions[1] + } + /// Get the end node of the interval. pub fn end_node(&self) -> N { self.nodes[1] diff --git a/tket/src/resource/scope.rs b/tket/src/resource/scope.rs index 3c1602fcb..07d8c2504 100644 --- a/tket/src/resource/scope.rs +++ b/tket/src/resource/scope.rs @@ -267,6 +267,11 @@ impl ResourceScope { self.nodes().contains(&next_node).then_some(next_node) }) } + + /// Check if the given node is in the subgraph. + pub fn contains_node(&self, node: H::Node) -> bool { + self.subgraph.nodes().contains(&node) + } } impl> ResourceScope { diff --git a/tket/src/rewrite/strategy.rs b/tket/src/rewrite/strategy.rs index 99a3ee30d..32613221b 100644 --- a/tket/src/rewrite/strategy.rs +++ b/tket/src/rewrite/strategy.rs @@ -516,7 +516,6 @@ mod tests { } #[test] - #[ignore = "reason: subcircuit to subgraph conversion is not implemented"] fn test_greedy_strategy() { let mut circ = n_cx(10); let cx_gates = circ.commands().map(|cmd| cmd.node()).collect_vec(); @@ -546,7 +545,6 @@ mod tests { } #[test] - #[ignore = "reason: subcircuit to subgraph conversion is not implemented"] fn test_exhaustive_default_strategy() { let mut circ = n_cx(10); let cx_gates = circ.commands().map(|cmd| cmd.node()).collect_vec(); @@ -584,7 +582,6 @@ mod tests { } #[test] - #[ignore = "reason: subcircuit to subgraph conversion is not implemented"] fn test_exhaustive_gamma_strategy() { let circ = n_cx(10); let cx_gates = circ.commands().map(|cmd| cmd.node()).collect_vec(); diff --git a/tket/src/subcircuit.rs b/tket/src/subcircuit.rs index 3d6345cae..22cebc56e 100644 --- a/tket/src/subcircuit.rs +++ b/tket/src/subcircuit.rs @@ -231,9 +231,14 @@ impl Subcircuit { /// Convert the subcircuit to a [`SiblingSubgraph`]. pub fn try_to_subgraph( &self, - _circuit: &ResourceScope>, + circuit: &ResourceScope>, ) -> Result, InvalidSubgraph> { - todo!() + if !circuit.is_convex(self.clone()) { + return Err(InvalidSubgraph::NotConvex); + } + + // TODO(performance): this checks convexity again and is very inefficient + SiblingSubgraph::try_from_nodes(self.nodes(circuit).collect_vec(), circuit.hugr()) } /// Create a rewrite rule to replace the subcircuit with a new circuit. From 11dc4b36924c64637b407eaea1ac82b80ddb0f7b Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Thu, 21 Aug 2025 12:46:33 +0200 Subject: [PATCH 2/3] Use new SiblingSubgraph::new_unchecked --- tket/src/resource/convex_checker.rs | 6 +- tket/src/resource/scope.rs | 44 +++++-- tket/src/resource/types.rs | 14 +- tket/src/subcircuit.rs | 198 +++++++++++++++++++++++++++- 4 files changed, 238 insertions(+), 24 deletions(-) diff --git a/tket/src/resource/convex_checker.rs b/tket/src/resource/convex_checker.rs index 8c162fc56..cb65831ab 100644 --- a/tket/src/resource/convex_checker.rs +++ b/tket/src/resource/convex_checker.rs @@ -13,7 +13,7 @@ impl ResourceScope { /// /// A subcircuit is convex if there is no path from a circuit output to a /// circuit input. - pub fn is_convex(&self, subcircuit: Subcircuit) -> bool { + pub fn is_convex(&self, subcircuit: &Subcircuit) -> bool { let Some(max_start_pos) = subcircuit .intervals_iter() .map(|interval| interval.start_pos()) @@ -107,13 +107,13 @@ mod tests { #[case(vec![3, 6, 7], false)] fn test_is_convex(#[case] selected_nodes: Vec, #[case] is_convex: bool) { let circ = cx_ladder(5); - let subgraph = circ.subgraph(); + let subgraph = circ.subgraph().unwrap(); let cx_nodes = subgraph.nodes(); let circ = ResourceScope::from(circ); let selected_nodes = selected_nodes.into_iter().map(|i| cx_nodes[i]); let subcirc = Subcircuit::try_from_nodes(selected_nodes, &circ).unwrap(); - assert_eq!(circ.is_convex(subcirc), is_convex); + assert_eq!(circ.is_convex(&subcirc), is_convex); } } diff --git a/tket/src/resource/scope.rs b/tket/src/resource/scope.rs index 07d8c2504..a3645380e 100644 --- a/tket/src/resource/scope.rs +++ b/tket/src/resource/scope.rs @@ -171,15 +171,35 @@ impl ResourceScope { Some(port_map.get_slice(direction)) } - /// Get the port of node on the given resource path. + /// Get the ports of node with the given opvalue in the given direction. /// /// The returned port will have the direction `dir`. - pub fn get_port(&self, node: H::Node, resource_id: ResourceId, dir: Direction) -> Option { - let units = self.get_circuit_units_slice(node, dir)?; - let offset = units - .iter() - .position(|unit| unit.as_resource() == Some(resource_id))?; - Some(Port::new(dir, offset)) + pub fn get_ports( + &self, + node: H::Node, + unit: impl Into>, + dir: Direction, + ) -> impl Iterator + '_ { + let exp_unit = unit.into(); + let units = self.get_circuit_units_slice(node, dir); + let offsets = units + .into_iter() + .flatten() + .positions(move |unit| unit == &exp_unit); + offsets.map(move |offset| Port::new(dir, offset)) + } + + /// Get the port of node with the given resource in the given direction. + pub fn get_resource_port( + &self, + node: H::Node, + resource_id: ResourceId, + dir: Direction, + ) -> Option { + self.get_ports(node, resource_id, dir) + .at_most_one() + .ok() + .expect("linear resource") } /// Get the position of the given node. @@ -221,10 +241,10 @@ impl ResourceScope { /// Whether the given node is the first node on the path of the given /// resource. pub fn is_resource_start(&self, node: H::Node, resource_id: ResourceId) -> bool { - self.get_port(node, resource_id, Direction::Outgoing) + self.get_resource_port(node, resource_id, Direction::Outgoing) .is_some() && self - .get_port(node, resource_id, Direction::Incoming) + .get_resource_port(node, resource_id, Direction::Incoming) .is_none() } @@ -259,7 +279,7 @@ impl ResourceScope { direction: Direction, ) -> impl Iterator + '_ { iter::successors(Some(start_node), move |&curr_node| { - let port = self.get_port(curr_node, resource_id, direction)?; + let port = self.get_resource_port(curr_node, resource_id, direction)?; let (next_node, _) = self .hugr() .single_linked_port(curr_node, port) @@ -270,7 +290,9 @@ impl ResourceScope { /// Check if the given node is in the subgraph. pub fn contains_node(&self, node: H::Node) -> bool { - self.subgraph.nodes().contains(&node) + self.subgraph + .as_ref() + .map_or(false, |subgraph| subgraph.nodes().contains(&node)) } } diff --git a/tket/src/resource/types.rs b/tket/src/resource/types.rs index 5b9b9d726..1c88d3f5b 100644 --- a/tket/src/resource/types.rs +++ b/tket/src/resource/types.rs @@ -4,6 +4,7 @@ //! copyable values throughout a HUGR circuit, including resource identifiers, //! positions, and the mapping structures that associate them with operations. +use derive_more::derive::From; use hugr::{ core::HugrNode, types::Signature, Direction, IncomingPort, OutgoingPort, Port, PortIndex, Wire, }; @@ -70,12 +71,19 @@ impl Position { /// A value associated with a dataflow port, identified either by a resource ID /// (for linear values) or by its wire (for copyable values). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// +/// This can currently be converted to and from [`hugr::CircuitUnit`], but +/// linear wires are assigned to resources with typed resource IDs instead of +/// integers. +/// +/// Equivalence with [`hugr::CircuitUnit`] is not guaranteed in the future: we +/// may expand expressivity, e.g. identifying copyable units by their ASTs. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From)] pub enum CircuitUnit { /// A linear resource. - Resource(ResourceId), + Resource(#[from] ResourceId), /// A copyable value. - Copyable(Wire), + Copyable(#[from] Wire), } impl CircuitUnit { diff --git a/tket/src/subcircuit.rs b/tket/src/subcircuit.rs index 22cebc56e..8fba8b7f6 100644 --- a/tket/src/subcircuit.rs +++ b/tket/src/subcircuit.rs @@ -8,9 +8,11 @@ use std::collections::BTreeMap; use derive_more::derive::{Display, Error}; use hugr::core::HugrNode; -use hugr::hugr::views::sibling_subgraph::{InvalidReplacement, InvalidSubgraph}; +use hugr::hugr::views::sibling_subgraph::{ + IncomingPorts, InvalidReplacement, InvalidSubgraph, OutgoingPorts, +}; use hugr::hugr::views::SiblingSubgraph; -use hugr::{Direction, HugrView, Wire}; +use hugr::{Direction, HugrView, IncomingPort, Port, Wire}; use itertools::Itertools; use crate::circuit::Circuit; @@ -228,17 +230,62 @@ impl Subcircuit { self.intervals.len() } + /// Get the input ports of the subcircuit. + /// + /// The linear ports will come first, followed by all copyable values used + /// in the subcircuit. Within each group, the ports are ordered in the order + /// in which they were added to the subcircuit. + pub fn input_ports( + &self, + circuit: &ResourceScope>, + ) -> IncomingPorts { + let resource_ports = self + .boundary_resource_ports(circuit, Direction::Incoming) + .map(|(node, port)| { + let port = port.as_incoming().expect("boundary_resource_ports dir"); + vec![(node, port)] + }); + resource_ports + .chain(self.boundary_copyable_input_ports(circuit)) + .collect_vec() + } + + /// Get the output ports of the subcircuit. + /// + /// This will only contain linear ports (copyable outputs are not supported + /// at the moment). The ports are ordered in the order in which they were + /// added to the subcircuit. + pub fn output_ports( + &self, + circuit: &ResourceScope>, + ) -> OutgoingPorts { + self.boundary_resource_ports(circuit, Direction::Outgoing) + .map(|(node, port)| { + let port = port.as_outgoing().expect("boundary_resource_ports dir"); + (node, port) + }) + .collect_vec() + } + /// Convert the subcircuit to a [`SiblingSubgraph`]. pub fn try_to_subgraph( &self, circuit: &ResourceScope>, ) -> Result, InvalidSubgraph> { - if !circuit.is_convex(self.clone()) { + if !circuit.is_convex(self) { return Err(InvalidSubgraph::NotConvex); } - // TODO(performance): this checks convexity again and is very inefficient - SiblingSubgraph::try_from_nodes(self.nodes(circuit).collect_vec(), circuit.hugr()) + if self.is_empty() { + return Err(InvalidSubgraph::EmptySubgraph); + } + + Ok(SiblingSubgraph::new_unchecked( + self.input_ports(circuit), + self.output_ports(circuit), + vec![], + self.nodes(circuit).collect_vec(), + )) } /// Create a rewrite rule to replace the subcircuit with a new circuit. @@ -378,7 +425,47 @@ impl Subcircuit { Direction::Incoming => interval.start_node(), Direction::Outgoing => interval.end_node(), }; - circuit.get_port(node, resource_id, dir).is_some() + circuit.get_resource_port(node, resource_id, dir).is_some() + } + + /// Get the linear input or output ports of the subcircuit. + fn boundary_resource_ports<'a>( + &'a self, + circuit: &'a ResourceScope>, + dir: Direction, + ) -> impl Iterator + 'a { + let boundary_resources = match dir { + Direction::Incoming => &self.input_resources, + Direction::Outgoing => &self.output_resources, + }; + boundary_resources.iter().map(move |&res| { + let interval = self.get_interval(res).expect("resource is in subcircuit"); + let node = match dir { + Direction::Incoming => interval.start_node(), + Direction::Outgoing => interval.end_node(), + }; + let port = circuit + .get_resource_port(node, res, dir) + .expect("subcircuit input has incoming port"); + (node, port) + }) + } + + /// Get the copyable input ports of the subcircuit. + fn boundary_copyable_input_ports<'a>( + &'a self, + circuit: &'a ResourceScope>, + ) -> impl Iterator> + 'a { + self.input_copyable_values.iter().map(move |&val| { + self.nodes(circuit) + .flat_map(move |node| { + circuit + .get_ports(node, val, Direction::Incoming) + .map(|p| p.as_incoming().expect("port dir matches get_port arg")) + .map(move |port| (node, port)) + }) + .collect_vec() + }) } fn update_input( @@ -414,6 +501,7 @@ impl Subcircuit { mod tests { use super::*; use crate::{ + extension::rotation::rotation_type, resource::{ tests::{cx_circuit, cx_rz_circuit}, ResourceAllocator, @@ -421,7 +509,7 @@ mod tests { utils::build_simple_circuit, TketOp, }; - use hugr::{CircuitUnit, Hugr, Node}; + use hugr::{extension::prelude::qb_t, types::Signature, CircuitUnit, Hugr, Node, OutgoingPort}; use rstest::{fixture, rstest}; #[rstest] @@ -619,4 +707,100 @@ mod tests { assert_eq!(subcircuit.output_resources, [resources[0]]); assert_eq!(subcircuit.input_copyable_values, vec![]); } + + #[test] + fn test_to_subgraph() { + let circ = cx_rz_circuit(2, true, false); + let subgraph = Circuit::from(&circ).subgraph().unwrap(); + let circ = ResourceScope::new(circ, subgraph); + + let mut subcircuit = Subcircuit::new_empty(); + + let node = |i: usize| Node::from(portgraph::NodeIndex::new(i)); + + // Add first a H gate + subcircuit.try_extend(node(7), &circ).unwrap(); + assert_eq!( + subcircuit.input_ports(&circ), + vec![vec![(node(7), IncomingPort::from(0))]] + ); + assert_eq!( + subcircuit.output_ports(&circ), + vec![(node(7), OutgoingPort::from(0))] + ); + + // Now add a two-qubit CX gate + subcircuit.try_extend(node(9), &circ).unwrap(); + assert_eq!( + subcircuit.input_ports(&circ), + vec![ + vec![(node(7), IncomingPort::from(0))], + vec![(node(9), IncomingPort::from(1))] + ] + ); + assert_eq!( + subcircuit.output_ports(&circ), + vec![ + (node(9), OutgoingPort::from(0)), + (node(9), OutgoingPort::from(1)) + ] + ); + + // Now add two contiguous rotation + subcircuit.try_extend(node(10), &circ).unwrap(); + subcircuit.try_extend(node(11), &circ).unwrap(); + assert_eq!( + subcircuit.input_ports(&circ), + vec![ + vec![(node(7), IncomingPort::from(0))], + vec![(node(9), IncomingPort::from(1))], + vec![ + (node(10), IncomingPort::from(1)), + (node(11), IncomingPort::from(1)) + ], + ] + ); + assert_eq!( + subcircuit.output_ports(&circ), + vec![ + (node(10), OutgoingPort::from(0)), + (node(11), OutgoingPort::from(0)), + ] + ); + + let subgraph = subcircuit.try_to_subgraph(&circ).unwrap(); + assert!(subgraph.validate(circ.hugr(), Default::default()).is_ok()); + let mut nodes = subgraph.nodes().to_owned(); + nodes.sort_unstable(); + assert_eq!(nodes, vec![node(7), node(9), node(10), node(11)]); + assert_eq!( + subgraph.signature(circ.hugr()), + Signature::new(vec![qb_t(), qb_t(), rotation_type()], vec![qb_t(), qb_t()],) + ); + } + + #[test] + fn test_to_subgraph_invalid() { + let circ = cx_rz_circuit(2, true, false); + let subgraph = Circuit::from(&circ).subgraph().unwrap(); + let circ = ResourceScope::new(circ, subgraph); + + let mut subcircuit = Subcircuit::new_empty(); + + assert_eq!( + subcircuit.try_to_subgraph(&circ), + Err(InvalidSubgraph::EmptySubgraph) + ); + + let node = |i: usize| Node::from(portgraph::NodeIndex::new(i)); + + // Add a H gate and a Rz gate, but omitting the CX gate in-between + subcircuit.try_extend(node(7), &circ).unwrap(); + subcircuit.try_extend(node(11), &circ).unwrap(); + + assert_eq!( + subcircuit.try_to_subgraph(&circ), + Err(InvalidSubgraph::NotConvex) + ); + } } From 05651683176a1a2b4f4094fa2bc30578d4d83b71 Mon Sep 17 00:00:00 2001 From: Luca Mondada Date: Thu, 21 Aug 2025 20:12:34 +0200 Subject: [PATCH 3/3] feat!: Replace CircuitRewrite --- tket-py/src/circuit/tk2circuit.rs | 5 +- tket-py/src/pattern.rs | 8 +- tket-py/src/rewrite.rs | 45 +++++-- tket/src/circuit.rs | 2 +- tket/src/optimiser/badger.rs | 38 +++--- tket/src/optimiser/badger/worker.rs | 9 +- tket/src/passes/chunks.rs | 12 +- tket/src/passes/pytket.rs | 3 +- tket/src/passes/tuple_unpack.rs | 13 +- tket/src/portmatching/matcher.rs | 18 ++- tket/src/resource.rs | 60 +++++++++ tket/src/resource/scope.rs | 81 +++++++++++- tket/src/resource/types.rs | 16 +++ tket/src/rewrite.rs | 197 +++++++++++++++++++++++----- tket/src/rewrite/ecc_rewriter.rs | 16 +++ tket/src/rewrite/strategy.rs | 65 +++++---- tket/src/rewrite/trace.rs | 6 +- tket/src/subcircuit.rs | 65 ++++++--- 18 files changed, 532 insertions(+), 127 deletions(-) diff --git a/tket-py/src/circuit/tk2circuit.rs b/tket-py/src/circuit/tk2circuit.rs index 03b812c6e..9ee413936 100644 --- a/tket-py/src/circuit/tk2circuit.rs +++ b/tket-py/src/circuit/tk2circuit.rs @@ -9,6 +9,7 @@ use hugr::builder::{CircuitBuilder, DFGBuilder, Dataflow, DataflowHugr}; use hugr::envelope::{EnvelopeConfig, EnvelopeFormat, ZstdConfig}; use hugr::extension::prelude::qb_t; use hugr::extension::{ExtensionRegistry, EMPTY_REG}; +use hugr::hugr::Patch; use hugr::ops::handle::NodeHandle; use hugr::ops::{ExtensionOp, OpType}; use hugr::package::Package; @@ -92,7 +93,9 @@ impl Tk2Circuit { /// Apply a rewrite on the circuit. pub fn apply_rewrite(&mut self, rw: PyCircuitRewrite) { - rw.rewrite.apply(&mut self.circ).expect("Apply error."); + rw.rewrite + .apply(self.circ.hugr_mut()) + .expect("Apply error."); } /// Encode the circuit as a HUGR envelope. diff --git a/tket-py/src/pattern.rs b/tket-py/src/pattern.rs index a9a63cda5..be442ecde 100644 --- a/tket-py/src/pattern.rs +++ b/tket-py/src/pattern.rs @@ -6,9 +6,10 @@ use crate::circuit::Tk2Circuit; use crate::rewrite::PyCircuitRewrite; use crate::utils::{create_py_exception, ConvertPyErr}; -use hugr::{HugrView, Node}; +use hugr::{HugrView, Node, SimpleReplacement}; use pyo3::prelude::*; use tket::portmatching::{CircuitPattern, PatternMatch, PatternMatcher}; +use tket::rewrite::CircuitRewrite; use tket::Circuit; /// The module definition @@ -123,6 +124,9 @@ impl RuleMatcher { ) -> PyResult { let r = self.rights.get(pmatch.pattern_id().0).unwrap().clone(); let rw = pmatch.to_rewrite(target, r).convert_pyerrs()?; - Ok(rw.into()) + match rw { + CircuitRewrite::New { .. } => unimplemented!(), + CircuitRewrite::Old(rew) => Ok(SimpleReplacement::from(rew).into()), + } } } diff --git a/tket-py/src/rewrite.rs b/tket-py/src/rewrite.rs index 5839de07b..f9825ff05 100644 --- a/tket-py/src/rewrite.rs +++ b/tket-py/src/rewrite.rs @@ -1,11 +1,12 @@ //! PyO3 wrapper for rewriters. use derive_more::From; -use hugr::{hugr::views::SiblingSubgraph, HugrView, Node}; +use hugr::{hugr::views::SiblingSubgraph, HugrView, Node, SimpleReplacement}; use itertools::Itertools; use pyo3::prelude::*; use std::path::PathBuf; use tket::{ + resource::ResourceScope, rewrite::{CircuitRewrite, ECCRewriter, Rewriter}, Circuit, }; @@ -32,7 +33,7 @@ pub fn module(py: Python<'_>) -> PyResult> { #[repr(transparent)] pub struct PyCircuitRewrite { /// Rust representation of the circuit chunks. - pub rewrite: CircuitRewrite, + pub rewrite: SimpleReplacement, } #[pymethods] @@ -42,12 +43,14 @@ impl PyCircuitRewrite { /// The difference between the new number of nodes minus the old. A positive /// number is an increase in node count, a negative number is a decrease. pub fn node_count_delta(&self) -> isize { - self.rewrite.node_count_delta() + let old_count = self.rewrite.subgraph().node_count() as isize; + let new_count = Circuit::new(self.rewrite.replacement()).num_operations() as isize; + new_count - old_count } /// The replacement subcircuit. pub fn replacement(&self) -> Tk2Circuit { - self.rewrite.replacement().to_owned().into() + Circuit::new(self.rewrite.replacement().to_owned()).into() } #[new] @@ -56,13 +59,14 @@ impl PyCircuitRewrite { source_circ: PyRef, replacement: Tk2Circuit, ) -> PyResult { + let repl = SimpleReplacement::try_new( + source_position.0, + source_circ.circ.hugr(), + replacement.circ.into_hugr(), + ) + .map_err(|e| PyErr::new::(e.to_string()))?; Ok(Self { - rewrite: CircuitRewrite::try_new( - &source_position.0, - source_circ.circ.hugr(), - replacement.circ, - ) - .map_err(|e| PyErr::new::(e.to_string()))?, + rewrite: repl.into(), }) } } @@ -79,8 +83,20 @@ pub enum PyRewriter { Vec(Vec), } -impl> Rewriter> for PyRewriter { - fn get_rewrites(&self, circ: &Circuit) -> Vec { +// impl> Rewriter for PyRewriter { +// fn get_rewrites(&self, circ: &H) -> Vec { +// match self { +// Self::ECC(ecc) => ecc.0.get_rewrites(circ), +// Self::Vec(rewriters) => rewriters +// .iter() +// .flat_map(|r| r.get_rewrites(circ)) +// .collect(), +// } +// } +// } + +impl> Rewriter> for PyRewriter { + fn get_rewrites(&self, circ: &ResourceScope) -> Vec::Node>> { match self { Self::ECC(ecc) => ecc.0.get_rewrites(circ), Self::Vec(rewriters) => rewriters @@ -146,7 +162,10 @@ impl PyECCRewriter { self.0 .get_rewrites(&circ.circ) .into_iter() - .map_into() + .map(|r| match r { + CircuitRewrite::New { .. } => unimplemented!(), + CircuitRewrite::Old(rewrite) => SimpleReplacement::from(rewrite).into(), + }) .collect() } } diff --git a/tket/src/circuit.rs b/tket/src/circuit.rs index 869c329b7..81f60395b 100644 --- a/tket/src/circuit.rs +++ b/tket/src/circuit.rs @@ -11,7 +11,7 @@ use std::collections::HashSet; use std::iter::Sum; pub use command::{Command, CommandIterator}; -pub use hash::CircuitHash; +pub use hash::{CircuitHash, HashError}; use hugr::extension::prelude::{NoopDef, TupleOpDef}; use hugr::extension::simple_op::MakeOpDef; use hugr::hugr::views::sibling_subgraph::InvalidSubgraph; diff --git a/tket/src/optimiser/badger.rs b/tket/src/optimiser/badger.rs index d465bed4d..f7cbca1c8 100644 --- a/tket/src/optimiser/badger.rs +++ b/tket/src/optimiser/badger.rs @@ -32,6 +32,7 @@ use crate::circuit::CircuitHash; use crate::optimiser::badger::worker::BadgerWorker; use crate::optimiser::{pqueue_worker, BacktrackingOptimiser, Optimiser, State, StatePQWorker}; use crate::passes::CircuitChunks; +use crate::resource::ResourceScope; use crate::rewrite::strategy::{RewriteResult, RewriteStrategy}; use crate::rewrite::Rewriter; use crate::Circuit; @@ -118,7 +119,7 @@ impl BadgerOptimiser { Self { rewriter, strategy } } - fn cost(&self, circ: &Circuit>) -> S::Cost + fn cost(&self, circ: &ResourceScope>) -> S::Cost where S: RewriteStrategy, { @@ -130,12 +131,13 @@ impl BadgerOptimiser { #[derive(Clone, Debug)] struct BadgerState { /// The current circuit - circ: Circuit, + circ: ResourceScope, /// The circuit cost cost: C, } -impl State<&BadgerOptimiser> for BadgerState +impl, S: RewriteStrategy> State<&BadgerOptimiser> + for BadgerState where S::Cost: serde::Serialize, { @@ -164,7 +166,7 @@ where impl BadgerOptimiser where - R: Rewriter + Send + Clone + Sync + 'static, + R: Rewriter + Send + Clone + Sync + 'static, S: RewriteStrategy + Send + Sync + Clone + 'static, S::Cost: serde::Serialize + Send + Sync, { @@ -188,7 +190,7 @@ where log_config: BadgerLogger, options: BadgerOptions, ) -> Circuit { - match options.n_threads.get() { + let h = match options.n_threads.get() { 1 => self.badger(circ, log_config, options), _ => { if options.split_circuit { @@ -199,6 +201,8 @@ where } } } + .into_hugr(); + Circuit::new(h) } /// Run the Badger optimiser on a circuit, using a single thread. @@ -208,12 +212,11 @@ where circ: &Circuit>, logger: BadgerLogger, opt: BadgerOptions, - ) -> Circuit { + ) -> ResourceScope { let backtracking = BacktrackingOptimiser::with_badger_options(&opt); - let init_state = BadgerState { - circ: circ.to_owned(), - cost: self.cost(circ), - }; + let circ = ResourceScope::from_circuit(circ.to_owned()); + let cost = self.cost(&circ); + let init_state = BadgerState { circ, cost }; backtracking .optimise_with_options(init_state, self, logger.into()) .expect("optimisation failed") @@ -231,10 +234,10 @@ where circ: &Circuit>, mut logger: BadgerLogger, opt: BadgerOptions, - ) -> Circuit { + ) -> ResourceScope { let start_time = Instant::now(); let n_threads: usize = opt.n_threads.get(); - let circ = circ.to_owned(); + let circ = ResourceScope::from_circuit(circ.to_owned()); // multi-consumer priority channel for queuing circuits to be processed by the // workers @@ -372,17 +375,18 @@ where circ: &Circuit>, mut logger: BadgerLogger, opt: BadgerOptions, - ) -> Result { + ) -> Result { let start_time = Instant::now(); - let circ = circ.to_owned(); + let circ = ResourceScope::from_circuit(circ.to_owned()); let circ_cost = self.cost(&circ); let max_chunk_cost = circ_cost.clone().div_cost(opt.n_threads); logger.log(format!( "Splitting circuit with cost {:?} into chunks of at most {max_chunk_cost:?}.", circ_cost.clone() )); - let mut chunks = - CircuitChunks::split_with_cost(&circ, max_chunk_cost, |op| self.strategy.op_cost(op)); + let mut chunks = CircuitChunks::split_with_cost(&circ.as_circuit(), max_chunk_cost, |op| { + self.strategy.op_cost(op) + }); let num_rewrites = circ.rewrite_trace().map(|rs| rs.count()); logger.log_best(circ_cost.clone(), num_rewrites); @@ -421,7 +425,7 @@ where chunks[i] = res; } - let best_circ = chunks.reassemble()?; + let best_circ = ResourceScope::from_circuit(chunks.reassemble()?); let best_circ_cost = self.cost(&best_circ); if best_circ_cost.clone() < circ_cost { let num_rewrites = best_circ.rewrite_trace().map(|rs| rs.count()); diff --git a/tket/src/optimiser/badger/worker.rs b/tket/src/optimiser/badger/worker.rs index a28fd9c56..4852f3c45 100644 --- a/tket/src/optimiser/badger/worker.rs +++ b/tket/src/optimiser/badger/worker.rs @@ -2,10 +2,11 @@ use std::thread::{self, JoinHandle}; +use crate::circuit::cost::CircuitCost; use crate::circuit::CircuitHash; +use crate::resource::ResourceScope; use crate::rewrite::strategy::RewriteStrategy; use crate::rewrite::Rewriter; -use crate::{circuit::cost::CircuitCost, Circuit}; use super::pqueue_worker::{StatePQueueChannels, Work}; @@ -15,7 +16,7 @@ pub struct BadgerWorker { #[allow(unused)] id: usize, /// The channel to send and receive work from. - priority_channel: StatePQueueChannels, + priority_channel: StatePQueueChannels, /// The rewriter to use. rewriter: R, /// The rewrite strategy to use. @@ -24,7 +25,7 @@ pub struct BadgerWorker { impl BadgerWorker where - R: Rewriter + Send + 'static, + R: Rewriter + Send + 'static, S: RewriteStrategy + Send + 'static, P: CircuitCost + Send + Sync + 'static, { @@ -32,7 +33,7 @@ where #[allow(clippy::too_many_arguments)] pub fn spawn( id: usize, - priority_channel: StatePQueueChannels, + priority_channel: StatePQueueChannels, rewriter: R, strategy: S, ) -> JoinHandle<()> { diff --git a/tket/src/passes/chunks.rs b/tket/src/passes/chunks.rs index 486ffdd02..c4bcfbac4 100644 --- a/tket/src/passes/chunks.rs +++ b/tket/src/passes/chunks.rs @@ -15,8 +15,8 @@ use hugr::hugr::{HugrError, NodeMetadataMap}; use hugr::ops::handle::DataflowParentID; use hugr::ops::OpType; use hugr::types::Signature; -use hugr::{Hugr, HugrView, IncomingPort, Node, OutgoingPort, PortIndex, Wire}; -use hugr_core::hugr::internal::{HugrInternals, HugrMutInternals as _}; +use hugr::{HugrView, IncomingPort, Node, OutgoingPort, PortIndex, Wire}; +use hugr_core::hugr::internal::HugrMutInternals as _; use itertools::Itertools; use rayon::iter::{IntoParallelIterator, IntoParallelRefMutIterator, ParallelIterator}; use rayon::slice::ParallelSliceMut; @@ -49,10 +49,10 @@ impl Chunk { /// Extract a chunk from a circuit. /// /// The chunk is extracted from the input wires to the output wires. - pub(self) fn extract( - circ: &Circuit, + pub(self) fn extract>( + circ: &Circuit, nodes: impl IntoIterator, - checker: &TopoConvexChecker<'_, Hugr>, + checker: &TopoConvexChecker<'_, H>, ) -> Self { let subgraph = SiblingSubgraph::try_from_nodes_with_checker( nodes.into_iter().collect_vec(), @@ -260,7 +260,7 @@ impl CircuitChunks { /// /// The circuit is split into chunks of at most `max_cost`, using the provided cost function. pub fn split_with_cost( - circ: &Circuit, + circ: &Circuit>, max_cost: C, op_cost: impl Fn(&OpType) -> C, ) -> Self { diff --git a/tket/src/passes/pytket.rs b/tket/src/passes/pytket.rs index f68933eb8..c14d5cb12 100644 --- a/tket/src/passes/pytket.rs +++ b/tket/src/passes/pytket.rs @@ -4,6 +4,7 @@ //! This is a best-effort attempt, and may not always succeed. use derive_more::{Display, Error, From}; +use hugr::hugr::Patch; use hugr::{HugrView, Node}; use itertools::Itertools; @@ -24,7 +25,7 @@ pub fn lower_to_pytket>( // typically generated by guppy. let rewrites = find_tuple_unpack_rewrites(&circ).collect_vec(); for rewrite in rewrites { - rewrite.apply(&mut circ).unwrap(); + rewrite.apply(circ.hugr_mut()).unwrap(); } Ok(circ) diff --git a/tket/src/passes/tuple_unpack.rs b/tket/src/passes/tuple_unpack.rs index 9d13e6c48..c22b01961 100644 --- a/tket/src/passes/tuple_unpack.rs +++ b/tket/src/passes/tuple_unpack.rs @@ -8,18 +8,17 @@ use hugr::extension::simple_op::MakeExtensionOp; use hugr::hugr::views::SiblingSubgraph; use hugr::ops::{OpTrait, OpType}; use hugr::types::Type; -use hugr::{HugrView, Node}; +use hugr::{HugrView, Node, SimpleReplacement}; use itertools::Itertools; use crate::circuit::Command; -use crate::rewrite::CircuitRewrite; use crate::Circuit; /// Find tuple pack operations followed by tuple unpack operations /// and generate rewrites to remove them. pub fn find_tuple_unpack_rewrites( circ: &Circuit>, -) -> impl Iterator + '_ { +) -> impl Iterator + '_ { circ.commands().filter_map(|cmd| make_rewrite(circ, cmd)) } @@ -40,7 +39,7 @@ fn is_unpack_tuple(optype: &OpType) -> bool { fn make_rewrite>( circ: &Circuit, cmd: Command, -) -> Option { +) -> Option { let cmd_optype = cmd.optype(); let tuple_node = cmd.node(); if !is_make_tuple(cmd_optype) { @@ -97,7 +96,7 @@ fn remove_pack_unpack>( pack_node: Node, unpack_nodes: Vec, num_other_outputs: usize, -) -> CircuitRewrite { +) -> SimpleReplacement { let num_unpack_outputs = tuple_types.len() * unpack_nodes.len(); let mut nodes = unpack_nodes; @@ -146,7 +145,6 @@ fn remove_pack_unpack>( subgraph .create_simple_replacement(circ.hugr(), replacement) - .map(CircuitRewrite::from) .unwrap_or_else(|e| { panic!("Failed to create rewrite for removing tuple pack/unpack operations. {e}") }) @@ -157,6 +155,7 @@ mod test { use super::*; use hugr::extension::prelude::{bool_t, qb_t, UnpackTuple}; + use hugr::hugr::Patch; use hugr::types::Signature; use rstest::{fixture, rstest}; @@ -244,7 +243,7 @@ mod test { break; }; num_rewrites += 1; - rewrite.apply(&mut circ)?; + rewrite.apply(circ.hugr_mut())?; } assert_eq!(num_rewrites, expected_rewrites); diff --git a/tket/src/portmatching/matcher.rs b/tket/src/portmatching/matcher.rs index 8be0c351a..0a1197590 100644 --- a/tket/src/portmatching/matcher.rs +++ b/tket/src/portmatching/matcher.rs @@ -9,11 +9,14 @@ use std::{ use super::{CircuitPattern, NodeID, PEdge, PNode}; use derive_more::{Display, Error, From}; -use hugr::hugr::views::sibling_subgraph::{ - InvalidReplacement, InvalidSubgraph, InvalidSubgraphBoundary, TopoConvexChecker, -}; use hugr::hugr::views::SiblingSubgraph; use hugr::ops::OpType; +use hugr::{ + hugr::views::sibling_subgraph::{ + InvalidReplacement, InvalidSubgraph, InvalidSubgraphBoundary, TopoConvexChecker, + }, + SimpleReplacement, +}; use hugr::{HugrView, IncomingPort, Node, OutgoingPort, Port, PortIndex}; use itertools::Itertools; use portmatching::{ @@ -215,7 +218,14 @@ impl PatternMatch { source: &Circuit>, target: Circuit, ) -> Result { - CircuitRewrite::try_new(&self.subgraph, source.hugr(), target) + Ok( + SimpleReplacement::try_new( + self.subgraph.to_owned(), + source.hugr(), + target.into_hugr(), + )? + .into(), + ) } } diff --git a/tket/src/resource.rs b/tket/src/resource.rs index 3692e9883..1b23a9969 100644 --- a/tket/src/resource.rs +++ b/tket/src/resource.rs @@ -45,10 +45,17 @@ // Public API exports pub use flow::{DefaultResourceFlow, ResourceFlow, UnsupportedOp}; +use hugr::{hugr::hugrmut::HugrMut, HugrView}; pub use interval::{Interval, InvalidInterval}; +use itertools::Itertools; pub use scope::{ResourceScope, ResourceScopeConfig}; pub use types::{CircuitUnit, Position, ResourceAllocator, ResourceId}; +use crate::{ + circuit::{CircuitHash, HashError}, + rewrite::trace::RewriteTrace, +}; + // Internal modules mod convex_checker; mod flow; @@ -56,6 +63,59 @@ mod interval; mod scope; mod types; +// Below a bunch of methods that delegate to circuit. +// TODO: clean up once we decide what to do with the `Circuit` type. + +impl ResourceScope { + /// Enable rewrite tracing for the circuit. + #[inline] + pub fn enable_rewrite_tracing(&mut self) { + self.as_circuit_mut().enable_rewrite_tracing(); + } + + /// Register a rewrite applied to the circuit. + /// + /// Returns `true` if the rewrite was successfully registered, or `false` if it was ignored. + #[inline] + pub fn add_rewrite_trace(&mut self, rewrite: impl Into) -> bool { + self.as_circuit_mut().add_rewrite_trace(rewrite) + } +} + +impl ResourceScope { + /// Returns the traces of rewrites applied to the circuit. + /// + /// Returns `None` if rewrite tracing is not enabled for this circuit. + #[inline] + pub fn rewrite_trace(&self) -> Option + '_> { + self.as_circuit() + .rewrite_trace() + .map(|rs| rs.collect_vec().into_iter()) + } + + /// The number of operations in the circuit. + /// + /// This includes [`TketOp`]s, pytket ops, and any other custom operations. + /// + /// Nested circuits are traversed to count their operations. + /// + /// [`TketOp`]: crate::TketOp + pub fn num_operations(&self) -> usize { + self.as_circuit().num_operations() + } + + /// Returns the node containing the circuit definition. + pub fn parent(&self) -> H::Node { + self.as_circuit().parent() + } +} + +impl> CircuitHash for ResourceScope { + fn circuit_hash(&self, parent: hugr::Node) -> Result { + self.as_circuit().circuit_hash(parent) + } +} + #[cfg(test)] pub(crate) mod tests { use hugr::{ diff --git a/tket/src/resource/scope.rs b/tket/src/resource/scope.rs index a3645380e..bbbb26f50 100644 --- a/tket/src/resource/scope.rs +++ b/tket/src/resource/scope.rs @@ -12,8 +12,8 @@ use crate::resource::types::{CircuitUnit, PortMap}; use crate::utils::type_is_linear; use crate::Circuit; use hugr::core::HugrNode; -use hugr::hugr::views::sibling_subgraph::InvalidSubgraph; -use hugr::hugr::views::SiblingSubgraph; +use hugr::hugr::views::sibling_subgraph::{IncomingPorts, InvalidSubgraph, OutgoingPorts}; +use hugr::hugr::views::{ExtractionResult, SiblingSubgraph}; use hugr::ops::OpTrait; use hugr::types::Signature; use hugr::{Direction, HugrView, IncomingPort, Port, PortIndex, Wire}; @@ -57,6 +57,17 @@ impl NodeCircuitUnits { position: Position::default(), } } + + fn map_nodes(&self, mut node_map: impl FnMut(N) -> N2) -> NodeCircuitUnits { + let mapped_port_map = self + .port_map + .clone() + .map(|unit| unit.map_node(&mut node_map)); + NodeCircuitUnits { + port_map: mapped_port_map, + position: self.position, + } + } } /// Configuration for a ResourceScope. @@ -140,11 +151,53 @@ impl ResourceScope { .map_or(&[], |subgraph| subgraph.nodes()) } + /// Ensures the ResourceScope contains an owned HUGR. + pub fn to_owned(&self) -> ResourceScope { + let (hugr, map) = self.hugr.extract_hugr(self.hugr.module_root()); + let map_node = |node: H::Node| map.extracted_node(node); + let new_circuit_units = self + .circuit_units + .iter() + .map(|(node, units)| (map.extracted_node(*node), units.map_nodes(map_node))) + .collect(); + let subgraph = self.subgraph.as_ref().map(|subgraph| { + let new_inputs = map_inputs(subgraph.incoming_ports(), map_node); + let new_outputs = map_outputs(subgraph.outgoing_ports(), map_node); + let new_function_calls = map_inputs(subgraph.function_calls(), map_node); + let new_nodes = subgraph.nodes().iter().map(|&n| map_node(n)).collect_vec(); + SiblingSubgraph::new_unchecked(new_inputs, new_outputs, new_function_calls, new_nodes) + }); + + ResourceScope { + hugr, + subgraph, + circuit_units: new_circuit_units, + } + } + /// Get the underlying HUGR. pub fn hugr(&self) -> &H { &self.hugr } + /// Consume the ResourceScope and return the underlying HUGR. + pub fn into_hugr(self) -> H { + self.hugr + } + + pub(crate) fn hugr_mut(&mut self) -> &mut H { + &mut self.hugr + } + + /// Wrap the underlying HUGR in a Circuit as reference. + pub fn as_circuit(&self) -> Circuit<&H> { + Circuit::new(self.hugr()) + } + + pub(crate) fn as_circuit_mut(&mut self) -> Circuit<&mut H> { + Circuit::new(self.hugr_mut()) + } + /// Get the underlying subgraph, or `None` if the circuit is empty. pub fn subgraph(&self) -> Option<&SiblingSubgraph> { self.subgraph.as_ref() @@ -296,6 +349,30 @@ impl ResourceScope { } } +fn map_inputs( + incoming_ports: &IncomingPorts, + mut node_map: impl FnMut(N1) -> N2, +) -> IncomingPorts { + incoming_ports + .iter() + .map(|uses| { + uses.iter() + .map(|&(node, port)| (node_map(node), port)) + .collect_vec() + }) + .collect_vec() +} + +fn map_outputs( + outgoing_ports: &OutgoingPorts, + mut node_map: impl FnMut(N1) -> N2, +) -> OutgoingPorts { + outgoing_ports + .iter() + .map(|&(node, port)| (node_map(node), port)) + .collect_vec() +} + impl> ResourceScope { /// Create a new ResourceScope from a reference to a circuit. /// diff --git a/tket/src/resource/types.rs b/tket/src/resource/types.rs index 1c88d3f5b..f1ffc4c51 100644 --- a/tket/src/resource/types.rs +++ b/tket/src/resource/types.rs @@ -87,6 +87,15 @@ pub enum CircuitUnit { } impl CircuitUnit { + pub(super) fn map_node(self, map_fn: impl FnOnce(N) -> N2) -> CircuitUnit { + match self { + CircuitUnit::Resource(resource_id) => CircuitUnit::Resource(resource_id), + CircuitUnit::Copyable(wire) => { + CircuitUnit::Copyable(Wire::new(map_fn(wire.node()), wire.source())) + } + } + } + /// Returns true if this is a resource value. pub fn is_resource(&self) -> bool { matches!(self, CircuitUnit::Resource(..)) @@ -157,6 +166,13 @@ impl PortMap { } } + pub(super) fn map(self, mut map_fn: impl FnMut(T) -> U) -> PortMap { + PortMap { + vec: self.vec.into_iter().map(|t| map_fn(t)).collect(), + num_inputs: self.num_inputs, + } + } + fn index(&self, port: impl Into) -> usize { let port = port.into(); match port.direction() { diff --git a/tket/src/rewrite.rs b/tket/src/rewrite.rs index 81d9d0bfb..51448d2ec 100644 --- a/tket/src/rewrite.rs +++ b/tket/src/rewrite.rs @@ -5,6 +5,7 @@ pub mod ecc_rewriter; pub mod strategy; pub mod trace; +use derive_more::derive::{Display, Error}; #[cfg(feature = "portmatching")] pub use ecc_rewriter::ECCRewriter; @@ -12,53 +13,125 @@ use derive_more::{From, Into}; use hugr::core::HugrNode; use hugr::hugr::hugrmut::HugrMut; use hugr::hugr::patch::simple_replace; -use hugr::hugr::views::sibling_subgraph::InvalidReplacement; +use hugr::hugr::views::sibling_subgraph::InvalidSubgraph; use hugr::hugr::Patch; +use hugr::types::Signature; use hugr::{ hugr::{views::SiblingSubgraph, SimpleReplacementError}, SimpleReplacement, }; use hugr::{Hugr, HugrView}; +use itertools::Either; use crate::circuit::Circuit; +use crate::resource::ResourceScope; pub use crate::Subcircuit; /// A rewrite rule for circuits. +/// +/// As a temporary solution, it support both old school [`SimpleReplacement`]s +/// as well as the much more civilised approach using [`ResourceScope`] and +/// [`Subcircuit`]. +#[derive(Debug, Clone, From)] +pub enum CircuitRewrite { + /// A rewrite rule expressed as a subcircuit and replacement circuit. + New(NewCircuitRewrite), + /// A rewrite rule expressed as a [`SimpleReplacement`]. + /// + /// Prefer using [`NewCircuitRewrite`] instead. It is much faster (but is + /// not yet supported in portmatching and the Python interface). + Old(#[from] OldCircuitRewrite), +} + +/// A rewrite rule for circuits. +#[derive(Debug, Clone)] +pub struct NewCircuitRewrite { + subcircuit: Subcircuit, + replacement: Circuit, +} + +/// A rewrite rule for circuits, wrapping a HUGR [`SimpleReplacement`]. +/// +/// You should migrate to using [`NewCircuitRewrite`] instead. It is much faster. #[derive(Debug, Clone, From, Into)] -pub struct CircuitRewrite(SimpleReplacement); +pub struct OldCircuitRewrite(SimpleReplacement); impl CircuitRewrite { - /// Create a new rewrite rule. + /// Create a new rewrite that can be applied to `hugr`. pub fn try_new( - subgraph: &SiblingSubgraph, - hugr: &impl HugrView, - replacement: Circuit>, - ) -> Result { - let replacement = replacement - .extract_dfg() - .unwrap_or_else(|e| panic!("{}", e)) - .into_hugr(); - Ok(Self(subgraph.create_simple_replacement(hugr, replacement)?)) + subcircuit: Subcircuit, + circuit: &ResourceScope>, + replacement: Circuit, + ) -> Result { + subcircuit + .validate_subgraph(circuit) + .map_err(|err| InvalidRewrite::try_from(err).unwrap_or_else(|err| panic!("{err}")))?; + + let subcircuit_sig = subcircuit.dataflow_signature(circuit); + let replacement_sig = replacement.circuit_signature(); + if subcircuit_sig != replacement_sig { + return Err(InvalidRewrite::InvalidSignature { + expected: subcircuit_sig, + actual: replacement_sig.into_owned(), + }); + } + + Ok(Self::New(NewCircuitRewrite { + subcircuit, + replacement, + })) } /// Number of nodes added or removed by the rewrite. /// /// The difference between the new number of nodes minus the old. A positive /// number is an increase in node count, a negative number is a decrease. - pub fn node_count_delta(&self) -> isize { - let new_count = self.replacement().num_operations() as isize; - let old_count = self.subgraph().node_count() as isize; - new_count - old_count + pub fn node_count_delta(&self, circuit: &ResourceScope>) -> isize { + match self { + Self::New(rewrite) => { + compute_node_count_delta(&rewrite.subcircuit, rewrite.replacement.hugr(), circuit) + } + Self::Old(OldCircuitRewrite(simple_replacement)) => { + let old_count = simple_replacement.subgraph().node_count() as isize; + let new_count = + Circuit::new(simple_replacement.replacement()).num_operations() as isize; + new_count - old_count + } + } } - /// The subgraph that is replaced. - pub fn subgraph(&self) -> &SiblingSubgraph { - self.0.subgraph() + /// Construct a [`SiblingSubgraph`] that represents the subcircuit being + /// replaced. + pub fn to_subgraph( + &self, + circuit: &ResourceScope>, + ) -> SiblingSubgraph { + match self { + Self::New(rewrite) => rewrite + .subcircuit + .try_to_subgraph(circuit) + .expect("subcircuit is valid subgraph"), + Self::Old(rewrite) => rewrite.0.subgraph().to_owned(), + } } /// The replacement subcircuit. - pub fn replacement(&self) -> Circuit<&Hugr> { - self.0.replacement().into() + pub fn replacement(&self) -> &Hugr { + match self { + Self::New(rewrite) => rewrite.replacement.hugr(), + Self::Old(rewrite) => rewrite.0.replacement(), + } + } + + /// Construct a [`SimpleReplacement`] that executes the rewrite as a HUGR + /// operation. + pub fn to_simple_replacement( + &self, + circuit: &ResourceScope>, + ) -> SimpleReplacement { + self.to_subgraph(circuit) + .create_simple_replacement(circuit.hugr(), self.replacement().to_owned()) + .expect("rewrite is valid simple replacement") } /// Returns a set of nodes referenced by the rewrite. Modifying any these @@ -67,27 +140,39 @@ impl CircuitRewrite { /// Two `CircuitRewrite`s can be composed if their invalidation sets are /// disjoint. #[inline] - pub fn invalidation_set(&self) -> impl Iterator + '_ { - self.0.invalidation_set() + pub fn invalidation_set<'a>( + &'a self, + circuit: &'a ResourceScope>, + ) -> impl Iterator + 'a { + match self { + Self::New(rewrite) => Either::Left(rewrite.subcircuit.nodes(circuit)), + Self::Old(rewrite) => Either::Right(rewrite.0.subgraph().nodes().iter().copied()), + } } /// Apply the rewrite rule to a circuit. #[inline] pub fn apply( self, - circ: &mut Circuit>, + circ: &mut ResourceScope>, ) -> Result, SimpleReplacementError> { - circ.add_rewrite_trace(&self); - self.0.apply(circ.hugr_mut()) + circ.as_circuit_mut().add_rewrite_trace(&self); + self.to_simple_replacement(circ).apply(circ.hugr_mut()) } /// Apply the rewrite rule to a circuit, without registering it in the rewrite trace. #[inline] pub fn apply_notrace( self, - circ: &mut Circuit>, + circ: &mut ResourceScope>, ) -> Result, SimpleReplacementError> { - self.0.apply(circ.hugr_mut()) + self.to_simple_replacement(circ).apply(circ.hugr_mut()) + } +} + +impl From> for CircuitRewrite { + fn from(value: SimpleReplacement) -> Self { + OldCircuitRewrite(value).into() } } @@ -104,16 +189,66 @@ pub trait Rewriter { // A simple trait to get the node type of a circuit. This will allow us to // support circuit-like types (e.g. persistent circuits) in the future. mod hidden { - use hugr::HugrView; + use hugr::{core::HugrNode, HugrView}; - use crate::Circuit; + use crate::{resource::ResourceScope, Circuit}; pub trait CircuitLike { - type Node; + type Node: HugrNode; + } + + impl CircuitLike for H { + type Node = H::Node; } impl CircuitLike for Circuit { type Node = H::Node; } + + impl CircuitLike for ResourceScope { + type Node = H::Node; + } } use hidden::CircuitLike; + +/// An error that can occur when constructing a rewrite rule. +#[derive(Debug, Clone, PartialEq, Display, Error)] +#[non_exhaustive] +pub enum InvalidRewrite { + /// The LHS subcircuit is not convex. + #[display("The LHS subcircuit is not convex.")] + NonConvexSubgraph, + /// The LHS subcircuit is empty. + #[display("The LHS subcircuit is empty.")] + EmptySubgraph, + /// The left and right hand sides have mismatched signatures. + #[display("The left and right hand sides have mismatched signatures: expected {expected:?}, got {actual:?}.")] + InvalidSignature { + /// The expected signature. + expected: Signature, + /// The actual signature. + actual: Signature, + }, +} + +impl TryFrom> for InvalidRewrite { + type Error = &'static str; + + fn try_from(value: InvalidSubgraph) -> Result { + match value { + InvalidSubgraph::NotConvex => Ok(InvalidRewrite::NonConvexSubgraph), + InvalidSubgraph::EmptySubgraph => Ok(InvalidRewrite::EmptySubgraph), + _ => return Err("Unexpected InvalidSubgraph error"), + } + } +} + +fn compute_node_count_delta( + subcircuit: &Subcircuit, + replacement: &Hugr, + circuit: &ResourceScope>, +) -> isize { + let new_count = Circuit::new(replacement).num_operations() as isize; + let old_count = subcircuit.nodes(circuit).count() as isize; + new_count - old_count +} diff --git a/tket/src/rewrite/ecc_rewriter.rs b/tket/src/rewrite/ecc_rewriter.rs index a98132c12..ea29a781e 100644 --- a/tket/src/rewrite/ecc_rewriter.rs +++ b/tket/src/rewrite/ecc_rewriter.rs @@ -27,6 +27,7 @@ use std::{ }; use crate::extension::REGISTRY; +use crate::resource::ResourceScope; use crate::{ circuit::{remove_empty_wire, Circuit}, optimiser::badger::{load_eccs_json_file, EqCircClass}, @@ -192,6 +193,15 @@ impl ECCRewriter { } } +impl> Rewriter> for ECCRewriter { + fn get_rewrites( + &self, + circ: &ResourceScope, + ) -> Vec as super::hidden::CircuitLike>::Node>> { + self.get_rewrites(&circ.as_circuit()) + } +} + impl> Rewriter> for ECCRewriter { fn get_rewrites(&self, circ: &Circuit) -> Vec> { let matches = self.matcher.find_matches(circ); @@ -211,6 +221,12 @@ impl> Rewriter> for ECCRewriter { } } +impl> Rewriter for ECCRewriter { + fn get_rewrites(&self, circ: &H) -> Vec> { + self.get_rewrites(&Circuit::new(circ)) + } +} + /// Errors that can occur when (de)serialising an [`ECCRewriter`]. #[derive(Debug, Display, Error, From)] #[non_exhaustive] diff --git a/tket/src/rewrite/strategy.rs b/tket/src/rewrite/strategy.rs index 32613221b..10191bd54 100644 --- a/tket/src/rewrite/strategy.rs +++ b/tket/src/rewrite/strategy.rs @@ -29,6 +29,7 @@ use hugr::{HugrView, Node}; use itertools::Itertools; use crate::circuit::cost::{is_cx, is_quantum, CircuitCost, CostDelta, LexicographicCost}; +use crate::resource::ResourceScope; use crate::{op_matches, Circuit, TketOp}; use super::trace::RewriteTrace; @@ -51,7 +52,7 @@ pub trait RewriteStrategy { fn apply_rewrites( &self, rewrites: impl IntoIterator, - circ: &Circuit, + circ: &ResourceScope, ) -> impl Iterator>; /// The cost of a single operation for this strategy's cost function. @@ -59,19 +60,22 @@ pub trait RewriteStrategy { /// The cost of a circuit using this strategy's cost function. #[inline] - fn circuit_cost(&self, circ: &Circuit>) -> Self::Cost { - circ.circuit_cost(|op| self.op_cost(op)) + fn circuit_cost(&self, circ: &ResourceScope>) -> Self::Cost { + circ.as_circuit().circuit_cost(|op| self.op_cost(op)) } /// Returns the cost of a rewrite's matched subcircuit before replacing it. #[inline] - fn pre_rewrite_cost(&self, rw: &CircuitRewrite, circ: &Circuit) -> Self::Cost { - circ.nodes_cost(rw.subgraph().nodes().iter().copied(), |op| self.op_cost(op)) + fn pre_rewrite_cost(&self, rw: &CircuitRewrite, circ: &ResourceScope) -> Self::Cost { + circ.as_circuit() + .nodes_cost(rw.to_subgraph(circ).nodes().iter().copied(), |op| { + self.op_cost(op) + }) } /// Returns the expected cost of a rewrite's matched subcircuit after replacing it. fn post_rewrite_cost(&self, rw: &CircuitRewrite) -> Self::Cost { - rw.replacement().circuit_cost(|op| self.op_cost(op)) + Circuit::new(rw.replacement()).circuit_cost(|op| self.op_cost(op)) } } @@ -79,7 +83,7 @@ pub trait RewriteStrategy { #[derive(Debug, Clone)] pub struct RewriteResult { /// The rewritten circuit. - pub circ: Circuit, + pub circ: ResourceScope, /// The cost delta of the rewrite. pub cost_delta: C::CostDelta, } @@ -89,6 +93,18 @@ impl> From<(Circuit, C::CostDelta)> { #[inline] fn from((circ, cost_delta): (Circuit, C::CostDelta)) -> Self { + Self { + circ: ResourceScope::from_circuit(circ.to_owned()), + cost_delta, + } + } +} + +impl> From<(ResourceScope, C::CostDelta)> + for RewriteResult +{ + #[inline] + fn from((circ, cost_delta): (ResourceScope, C::CostDelta)) -> Self { Self { circ: circ.to_owned(), cost_delta, @@ -116,26 +132,26 @@ impl RewriteStrategy for GreedyRewriteStrategy { fn apply_rewrites( &self, rewrites: impl IntoIterator, - circ: &Circuit, + circ: &ResourceScope, ) -> impl Iterator> { let rewrites = rewrites .into_iter() - .sorted_by_key(|rw| rw.node_count_delta()) - .take_while(|rw| rw.node_count_delta() < 0); + .sorted_by_key(|rw| rw.node_count_delta(circ)) + .take_while(|rw| rw.node_count_delta(circ) < 0); let mut changed_nodes = HashSet::new(); let mut cost_delta = 0; let mut circ = circ.clone(); for rewrite in rewrites { if rewrite - .subgraph() + .to_subgraph(&circ) .nodes() .iter() .any(|n| changed_nodes.contains(n)) { continue; } - changed_nodes.extend(rewrite.subgraph().nodes().iter().copied()); - cost_delta += rewrite.node_count_delta(); + changed_nodes.extend(rewrite.to_subgraph(&circ).nodes().iter().copied()); + cost_delta += rewrite.node_count_delta(&circ); rewrite .apply(&mut circ) .expect("Could not perform rewrite in greedy strategy"); @@ -143,8 +159,8 @@ impl RewriteStrategy for GreedyRewriteStrategy { iter::once((circ, cost_delta).into()) } - fn circuit_cost(&self, circ: &Circuit>) -> Self::Cost { - circ.num_operations() + fn circuit_cost(&self, circ: &ResourceScope>) -> Self::Cost { + circ.as_circuit().num_operations() } fn op_cost(&self, _op: &OpType) -> Self::Cost { @@ -187,7 +203,7 @@ impl RewriteStrategy for ExhaustiveGreedyStrategy { fn apply_rewrites( &self, rewrites: impl IntoIterator, - circ: &Circuit, + circ: &ResourceScope, ) -> impl Iterator> { // Check only the rewrites that reduce the size of the circuit. let rewrites = rewrites @@ -211,12 +227,12 @@ impl RewriteStrategy for ExhaustiveGreedyStrategy { for (rewrite, delta) in &rewrites[i..] { if !changed_nodes.is_empty() && rewrite - .invalidation_set() + .invalidation_set(&curr_circ) .any(|n| changed_nodes.contains(&n)) { continue; } - changed_nodes.extend(rewrite.invalidation_set()); + changed_nodes.extend(rewrite.invalidation_set(&curr_circ)); cost_delta += delta.clone(); composed_rewrite_count += 1; @@ -265,7 +281,7 @@ impl RewriteStrategy for ExhaustiveThresholdStrategy { fn apply_rewrites( &self, rewrites: impl IntoIterator, - circ: &Circuit, + circ: &ResourceScope, ) -> impl Iterator> { rewrites.into_iter().filter_map(|rw| { let pattern_cost = self.pre_rewrite_cost(&rw, circ); @@ -534,6 +550,7 @@ mod tests { rw_to_empty(&circ, cx_gates[9..10].to_vec()), ]; + let circ: ResourceScope<_> = circ.into(); let strategy = GreedyRewriteStrategy; let rewritten = strategy.apply_rewrites(rws, &circ).collect_vec(); assert_eq!(rewritten.len(), 1); @@ -557,6 +574,7 @@ mod tests { rw_to_empty(&circ, cx_gates[9..10].to_vec()), ]; + let circ: ResourceScope<_> = circ.into(); let strategy = LexicographicCostFunction::cx_count().into_greedy_strategy(); let rewritten = strategy.apply_rewrites(rws, &circ).collect_vec(); let exp_circ_lens = HashSet::from_iter([3, 7, 9]); @@ -593,6 +611,7 @@ mod tests { rw_to_empty(&circ, cx_gates[9..10].to_vec()), ]; + let circ: ResourceScope = circ.into(); let strategy = GammaStrategyCost::exhaustive_cx_with_gamma(10.); let rewritten = strategy.apply_rewrites(rws, &circ); let exp_circ_lens = HashSet::from_iter([8, 17, 6, 9]); @@ -603,15 +622,17 @@ mod tests { #[test] fn test_exhaustive_default_cx_cost() { let strat = LexicographicCostFunction::cx_count().into_greedy_strategy(); - let circ = n_cx(3); + let circ = ResourceScope::from_circuit(n_cx(3)); assert_eq!(strat.circuit_cost(&circ), (3, 3).into()); - let circ = build_simple_circuit(2, |circ| { + + let circ: ResourceScope = build_simple_circuit(2, |circ| { circ.append(TketOp::CX, [0, 1])?; circ.append(TketOp::X, [0])?; circ.append(TketOp::X, [1])?; Ok(()) }) - .unwrap(); + .unwrap() + .into(); assert_eq!(strat.circuit_cost(&circ), (1, 3).into()); } diff --git a/tket/src/rewrite/trace.rs b/tket/src/rewrite/trace.rs index 73501337f..524e2dec2 100644 --- a/tket/src/rewrite/trace.rs +++ b/tket/src/rewrite/trace.rs @@ -2,8 +2,10 @@ //! //! This is only tracked if the `rewrite-tracing` feature is enabled. +use hugr::core::HugrNode; use hugr::hugr::hugrmut::HugrMut; use hugr::hugr::NodeMetadata; +use hugr::HugrView; use itertools::Itertools; use crate::Circuit; @@ -33,7 +35,7 @@ pub struct RewriteTrace { individual_matches: u16, } -impl From<&CircuitRewrite> for RewriteTrace { +impl From<&CircuitRewrite> for RewriteTrace { #[inline] fn from(_rewrite: &CircuitRewrite) -> Self { // NOTE: We don't currently track any actual information about the rewrite. @@ -108,7 +110,9 @@ impl Circuit { None => false, } } +} +impl Circuit { /// Returns the traces of rewrites applied to the circuit. /// /// Returns `None` if rewrite tracing is not enabled for this circuit. diff --git a/tket/src/subcircuit.rs b/tket/src/subcircuit.rs index 8fba8b7f6..5a8191f61 100644 --- a/tket/src/subcircuit.rs +++ b/tket/src/subcircuit.rs @@ -8,16 +8,16 @@ use std::collections::BTreeMap; use derive_more::derive::{Display, Error}; use hugr::core::HugrNode; -use hugr::hugr::views::sibling_subgraph::{ - IncomingPorts, InvalidReplacement, InvalidSubgraph, OutgoingPorts, -}; +use hugr::hugr::views::sibling_subgraph::{IncomingPorts, InvalidSubgraph, OutgoingPorts}; use hugr::hugr::views::SiblingSubgraph; +use hugr::ops::OpTrait; +use hugr::types::Signature; use hugr::{Direction, HugrView, IncomingPort, Port, Wire}; use itertools::Itertools; use crate::circuit::Circuit; -use crate::resource::{Interval, InvalidInterval, ResourceId, ResourceScope}; -use crate::rewrite::CircuitRewrite; +use crate::resource::{ Interval, InvalidInterval, ResourceId, ResourceScope}; +use crate::rewrite::{CircuitRewrite, InvalidRewrite}; /// A subgraph within a [`ResourceScope`]. /// @@ -267,11 +267,37 @@ impl Subcircuit { .collect_vec() } - /// Convert the subcircuit to a [`SiblingSubgraph`]. - pub fn try_to_subgraph( + /// Get the dataflow signature of the subcircuit. + pub fn dataflow_signature( &self, circuit: &ResourceScope>, - ) -> Result, InvalidSubgraph> { + ) -> Signature { + let port_type = |n: N, p: Port| { + let op = circuit.hugr().get_optype(n); + let signature = op.dataflow_signature().expect("dataflow op"); + signature.port_type(p).expect("valid dfg port").clone() + }; + + let input_types = self.input_ports(circuit).into_iter().map(|all_uses| { + let (n, p) = all_uses.into_iter().next().expect("all inputs are used"); + port_type(n, p.into()) + }); + let output_types = self + .output_ports(circuit) + .into_iter() + .map(|(n, p)| port_type(n, p.into())); + + Signature::new(input_types.collect_vec(), output_types.collect_vec()) + } + + /// Whether the subcircuit is a valid [`SiblingSubgraph`]. + /// + /// Calling this method will succeed if and only if the subcircuit can be + /// converted to a [`SiblingSubgraph`] using [`Self::try_to_subgraph`]. + pub fn validate_subgraph( + &self, + circuit: &ResourceScope>, + ) -> Result<(), InvalidSubgraph> { if !circuit.is_convex(self) { return Err(InvalidSubgraph::NotConvex); } @@ -280,6 +306,19 @@ impl Subcircuit { return Err(InvalidSubgraph::EmptySubgraph); } + Ok(()) + } + + /// Convert the subcircuit to a [`SiblingSubgraph`]. + /// + /// You may use [`Self::validate_subgraph`] to check whether converting the + /// subcircuit to a [`SiblingSubgraph`] will succeed. + pub fn try_to_subgraph( + &self, + circuit: &ResourceScope>, + ) -> Result, InvalidSubgraph> { + self.validate_subgraph(circuit)?; + Ok(SiblingSubgraph::new_unchecked( self.input_ports(circuit), self.output_ports(circuit), @@ -295,14 +334,10 @@ impl Subcircuit { /// * `replacement` - The new circuit to replace the subcircuit with. pub fn create_rewrite( &self, - replacement: Circuit>, + replacement: Circuit, circuit: &ResourceScope>, - ) -> Result, InvalidReplacement> { - let hugr = circuit.hugr(); - let subgraph = self - .try_to_subgraph(circuit) - .map_err(|_| InvalidReplacement::NonConvexSubgraph)?; - CircuitRewrite::try_new(&subgraph, hugr, replacement) + ) -> Result, InvalidRewrite> { + CircuitRewrite::try_new(self.clone(), circuit, replacement) } }