diff --git a/hugr-core/src/hugr/internal.rs b/hugr-core/src/hugr/internal.rs index d55240b3da..9caa752fa7 100644 --- a/hugr-core/src/hugr/internal.rs +++ b/hugr-core/src/hugr/internal.rs @@ -43,6 +43,7 @@ pub trait HugrInternals { // needed if we want to use petgraph's algorithms on the region graph). // This won't be solvable until we do the big petgraph refactor -.- // In the meantime, just wrap the portgraph in a `FlatRegion` as needed. + #[deprecated(note = "Use scheduling_graph instead", since = "0.27.0")] fn region_portgraph( &self, parent: Self::Node, @@ -396,6 +397,7 @@ impl Hugr { /// Consumes the HUGR and return a flat portgraph view of the region rooted /// at `parent`. #[inline] + #[deprecated(note = "Use scheduling_graph instead", since = "0.27.0")] pub fn into_region_portgraph( self, parent: Node, diff --git a/hugr-core/src/hugr/validate.rs b/hugr-core/src/hugr/validate.rs index ba47d0f725..6648d4fd0c 100644 --- a/hugr-core/src/hugr/validate.rs +++ b/hugr-core/src/hugr/validate.rs @@ -128,10 +128,10 @@ impl<'a, H: HugrView> ValidationContext<'a, H> { &self, parent: H::Node, ) -> (Dominators, H::RegionPortgraphNodes) { - let (region, node_map) = self.hugr.region_portgraph(parent); + let sg = self.hugr.scheduling_graph(parent); let entry_node = self.hugr.children(parent).next().unwrap(); - let doms = dominators::simple_fast(®ion, node_map.to_portgraph(entry_node)); - (doms, node_map) + let doms = dominators::simple_fast(sg.petgraph(), sg.node_to_pg(entry_node)); + (doms, sg.into_node_map()) } /// Check the constraints on a single node. @@ -422,11 +422,11 @@ impl<'a, H: HugrView> ValidationContext<'a, H> { return Ok(()); } - let (region, node_map) = self.hugr.region_portgraph(parent); - let postorder = Topo::new(®ion); + let sg = self.hugr.scheduling_graph(parent); + let postorder = Topo::new(sg.petgraph()); let nodes_visited = postorder - .iter(®ion) - .filter(|n| *n != node_map.to_portgraph(parent)) + .iter(sg.petgraph()) + .filter(|n| *n != sg.node_to_pg(parent)) .count(); let node_count = self.hugr.children(parent).count(); if nodes_visited != node_count { diff --git a/hugr-core/src/hugr/views.rs b/hugr-core/src/hugr/views.rs index 4e0211b6e6..35f62c7993 100644 --- a/hugr-core/src/hugr/views.rs +++ b/hugr-core/src/hugr/views.rs @@ -7,10 +7,12 @@ pub mod render; mod rerooted; mod root_checked; pub mod sibling_subgraph; +mod syn_edge; #[cfg(test)] mod tests; +use ::petgraph::visit as pv; use serde::de::Deserialize; use std::borrow::Cow; use std::collections::HashMap; @@ -24,23 +26,22 @@ pub use rerooted::Rerooted; pub use root_checked::{InvalidSignature, RootChecked, check_tag}; pub use sibling_subgraph::SiblingSubgraph; -use itertools::Itertools; +use itertools::{Either, Itertools}; use portgraph::render::{DotFormat, MermaidFormat}; use portgraph::{LinkView, PortView}; -use super::internal::{HugrInternals, HugrMutInternals}; -use super::validate::ValidationContext; -use super::{Hugr, HugrMut, Node, ValidationError}; use crate::core::HugrNode; use crate::extension::ExtensionRegistry; +use crate::hugr::internal::PortgraphNodeMap; +use crate::hugr::views::syn_edge::SynEdgeWrapper; use crate::metadata::{Metadata, RawMetadataValue}; -use crate::ops::handle::NodeHandle; -use crate::ops::{OpParent, OpTag, OpTrait, OpType}; - +use crate::ops::{OpParent, OpTag, OpTrait, OpType, handle::NodeHandle}; use crate::types::{EdgeKind, PolyFuncType, Signature, Type}; use crate::{Direction, IncomingPort, OutgoingPort, Port}; -use itertools::Either; +use super::internal::{HugrInternals, HugrMutInternals}; +use super::validate::ValidationContext; +use super::{Hugr, HugrMut, Node, ValidationError}; /// A trait for inspecting HUGRs. /// For end users we intend this to be superseded by region-specific APIs. @@ -391,10 +392,7 @@ pub trait HugrView: HugrInternals { /// Return a wrapper over the view that can be used in petgraph algorithms. #[inline] - #[deprecated( - since = "0.26.0", - note = "Use hugr_core::internal::HugrInternals::region_portgraph instead." - )] + #[deprecated(since = "0.26.0", note = "Use HugrView::scheduling_graph instead.")] #[expect(deprecated)] // Remove at same time as PetgraphWrapper fn as_petgraph(&self) -> PetgraphWrapper<'_, Self> where @@ -403,6 +401,22 @@ pub trait HugrView: HugrInternals { PetgraphWrapper { hugr: self } } + /// A view of a flat region, including ordering constraints from nonlocal edges, + /// suitable for use with petgraph algorithms. + fn scheduling_graph(&self, parent: Self::Node) -> SchedulingGraph<'_, Self> { + #[expect(deprecated)] // Inline region_portgraph here when removing + let (region_view, region_nodes) = self.region_portgraph(parent); + let graph = SynEdgeWrapper { + region_view, + syn_edges: Vec::new(), + }; + SchedulingGraph { + graph, + node_map: region_nodes, + region_parent: parent, + } + } + /// Return the mermaid representation of the underlying hierarchical graph. /// /// The hierarchy is represented using subgraphs. Edges are labelled with @@ -552,6 +566,71 @@ impl ExtractionResult for HashMap { } } +/// A graph of a flat region of a Hugr, including ordering constraints from nonlocal edges +pub struct SchedulingGraph<'a, V: HugrView + ?Sized + 'a> { + graph: SynEdgeWrapper>>, + node_map: V::RegionPortgraphNodes, + region_parent: V::Node, +} + +impl<'a, V: HugrView + 'a> SchedulingGraph<'a, V> { + /// Get the parent node of the region represented by this scheduling graph + pub fn region_parent(&self) -> V::Node { + self.region_parent + } + + /// Converts a `V::Node` index in the original Hugr into + /// an index in [Self::petgraph] + /// + /// # Panics + /// + /// If `n` is not a child of [Self::region_parent] + pub fn node_to_pg(&self, n: V::Node) -> portgraph::NodeIndex { + self.node_map.to_portgraph(n) + } + + /// Converts the index of a node in [Self::petgraph] to the corresponding + /// `V::Node` of the original Hugr. + /// + /// # Panics + /// + /// If `n` is not a node in `Self::petgraph` + pub fn pg_to_node(&self, n: portgraph::NodeIndex) -> V::Node { + self.node_map.from_portgraph(n) + } + + /// Extracts the map between `V::Node` and the [NodeIndex] used in [Self::petgraph], + /// discarding the rest of `self`. + /// + /// [NodeIndex]: portgraph::NodeIndex + pub fn into_node_map(self) -> V::RegionPortgraphNodes { + self.node_map + } + + fn portgraph_no_syn_edges( + self, + ) -> ( + portgraph::view::FlatRegion<'a, V::RegionPortgraph<'a>>, + V::RegionPortgraphNodes, + ) { + // This may need to change when the SynEdgeWrapper actually has edges in it... + // or maybe we should keep the assert to prevent this being used any time it does. + assert!(self.graph.syn_edges.is_empty()); + (self.graph.region_view, self.node_map) + } + + /// Access to the graph, sufficient to allow [pv::Topo] + pub fn petgraph( + &self, + ) -> impl pv::NodeCount + + pv::IntoNodeIdentifiers + + pv::IntoEdgeReferences + + pv::IntoNeighborsDirected + + pv::Visitable { + &self.graph + } +} + impl HugrView for Hugr { #[inline] fn entrypoint(&self) -> Self::Node { diff --git a/hugr-core/src/hugr/views/impls.rs b/hugr-core/src/hugr/views/impls.rs index 34ee97c016..45b6e2a262 100644 --- a/hugr-core/src/hugr/views/impls.rs +++ b/hugr-core/src/hugr/views/impls.rs @@ -11,6 +11,7 @@ macro_rules! hugr_internal_methods { ($arg:ident, $e:expr) => { delegate::delegate! { to ({let $arg=self; $e}) { + #[expect(deprecated)] // Remove delegate along with region_portgraph fn region_portgraph(&self, parent: Self::Node) -> (portgraph::view::FlatRegion<'_, Self::RegionPortgraph<'_>>, Self::RegionPortgraphNodes); fn node_metadata_map(&self, node: Self::Node) -> &crate::hugr::NodeMetadataMap; } diff --git a/hugr-core/src/hugr/views/sibling_subgraph.rs b/hugr-core/src/hugr/views/sibling_subgraph.rs index 09121fc8c7..704e5a2d5a 100644 --- a/hugr-core/src/hugr/views/sibling_subgraph.rs +++ b/hugr-core/src/hugr/views/sibling_subgraph.rs @@ -3,16 +3,13 @@ //! Views into convex subgraphs of HUGRs within a single level of the //! hierarchy, i.e. within a sibling graph. Convex subgraph are always //! induced subgraphs, i.e. they are defined by a subset of the sibling nodes. - -use std::collections::HashSet; +use std::collections::{BTreeSet, HashSet}; use std::mem; use itertools::Itertools; -use portgraph::LinkView; -use portgraph::PortView; -use portgraph::algorithms::CreateConvexChecker; +use petgraph::visit::IntoNodeIdentifiers; use portgraph::algorithms::convex::{LineIndex, LineIntervals, Position}; -use portgraph::boundary::Boundary; +use portgraph::{PortView, algorithms::CreateConvexChecker, boundary::Boundary}; use rustc_hash::FxHashSet; use thiserror::Error; @@ -26,7 +23,9 @@ use crate::ops::{NamedOp, OpTag, OpTrait, OpType}; use crate::types::{Signature, Type}; use crate::{Hugr, IncomingPort, Node, OutgoingPort, Port, SimpleReplacement}; -use super::RootChecked; +use super::{RootChecked, SchedulingGraph, SynEdgeWrapper}; + +mod convex; /// Checks convexity of potential sibling subgraphs within a Hugr region. pub trait HugrConvexChecker { @@ -53,7 +52,7 @@ pub trait HugrConvexChecker { ) -> Result, InvalidSubgraph>; } -impl<'a, H, CC> HugrConvexChecker for ConvexChecker<'a, H, CC> +impl<'a, H, CC> HugrConvexChecker for PortgraphCheckerWithNodes<'a, H, CC> where H: HugrView, CC: CreateConvexChecker, NodeIndexBase = u32, PortIndexBase = u32>, @@ -258,7 +257,7 @@ impl SiblingSubgraph { let parent = hugr .get_parent(node) .ok_or(InvalidSubgraph::OrphanNode { orphan: node })?; - let checker = TopoConvexChecker::new(hugr, parent); + let checker = SchedGraphChecker::new(hugr.scheduling_graph(parent)); Self::try_new_with_checker(inputs, outputs, hugr, &checker) } @@ -358,7 +357,7 @@ impl SiblingSubgraph { .get_parent(*node) .ok_or(InvalidSubgraph::OrphanNode { orphan: *node })?; - let checker = TopoConvexChecker::new(hugr, parent); + let checker = SchedGraphChecker::new(hugr.scheduling_graph(parent)); Self::try_from_nodes_with_checker(nodes, hugr, &checker) } @@ -520,48 +519,89 @@ impl SiblingSubgraph { /// Check the validity of the subgraph, as described in the docs of /// [`SiblingSubgraph::try_new`]. - /// - /// The `mode` parameter controls the convexity check: - /// - [`ValidationMode::CheckConvexity`] will create a new convexity - /// checker for the subgraph and use it to check convexity of the - /// subgraph. - /// - [`ValidationMode::WithChecker`] will use the given convexity checker - /// to check convexity of the subgraph. - /// - [`ValidationMode::SkipConvexity`] will skip the convexity check. + #[deprecated( + note = "Use `validate_with_checker`, `validate_default` or `validate_skip_convexity`", + since = "0.27.1" + )] + #[expect(deprecated)] // Remove with ValidationMode pub fn validate<'h, H: HugrView>( &self, hugr: &'h H, mode: ValidationMode<'_, 'h, H>, + ) -> Result<(), InvalidSubgraph> { + match mode { + ValidationMode::WithChecker(checker) => self.validate_with_checker(hugr, Some(checker)), + ValidationMode::CheckConvexity => self.validate_default(hugr), + ValidationMode::SkipConvexity => self.validate_skip_convexity(hugr), + } + } + + /// Check the validity of the subgraph, as described in the docs of + /// [`SiblingSubgraph::try_new`], using a new [`SchedGraphChecker`] for the convexity check. + pub fn validate_default( + &self, + hugr: &impl HugrView, + ) -> Result<(), InvalidSubgraph> { + let parent = check_parent(hugr, &self.inputs, &self.outputs)?; + self.validate_with_checker( + hugr, + Some(&SchedGraphChecker::new(hugr.scheduling_graph(parent))), + ) + } + + /// Check the validity of the subgraph, as described in the docs of + /// [`SiblingSubgraph::try_new`], but do not check convexity. + pub fn validate_skip_convexity( + &self, + hugr: &impl HugrView, + ) -> Result<(), InvalidSubgraph> { + enum NoChecker {} + impl HugrConvexChecker for NoChecker { + fn region_parent(&self) -> N { + match *self {} + } + + fn nodes_if_convex( + &self, + _hugr: &impl HugrView, + _inputs: &IncomingPorts, + _outputs: &OutgoingPorts, + _function_calls: &IncomingPorts, + ) -> Result, InvalidSubgraph> { + match *self {} + } + } + let no_checker: Option<&NoChecker> = None; + self.validate_with_checker(hugr, no_checker) + } + + /// Check the validity of the subgraph, as described in the docs of + /// [`SiblingSubgraph::try_new`], with a given convexity checker + /// (or `None` to skip convexity checks). + /// + /// See also convenience methods [Self::validate_default] and [Self::validate_skip_convexity]. + pub fn validate_with_checker>( + &self, + hugr: &H, + checker: Option<&impl HugrConvexChecker>, ) -> Result<(), InvalidSubgraph> { let subgraph_parent = check_parent(hugr, &self.inputs, &self.outputs)?; - let checker; - let checker_ref = match mode { - ValidationMode::WithChecker(c) => { + + let mut exp_nodes = match checker { + Some(c) => { if c.region_parent() != subgraph_parent { return Err(InvalidSubgraph::MismatchedCheckerParent { checker_parent: c.region_parent(), subgraph_parent, }); } - Some(c) + c.nodes_if_convex(hugr, &self.inputs, &self.outputs, &self.function_calls)? } - ValidationMode::CheckConvexity => { - checker = TopoConvexChecker::new(hugr, subgraph_parent); - Some(&checker) - } - ValidationMode::SkipConvexity => None, - }; - - let mut exp_nodes = match checker_ref { - Some(checker_ref) => checker_ref.nodes_if_convex( - hugr, - &self.inputs, - &self.outputs, - &self.function_calls, - )?, // Note we used to check exp_nodes == nodes *before* the convexity check None => { - let (region, node_map) = hugr.region_portgraph(subgraph_parent); + let (region, node_map) = hugr + .scheduling_graph(subgraph_parent) + .portgraph_no_syn_edges(); make_pg_subgraph::(region, &self.inputs, &self.outputs, &node_map) .nodes_iter() .map(|n| node_map.from_portgraph(n)) @@ -790,6 +830,11 @@ impl SiblingSubgraph { } /// Specify the checks to perform for [`SiblingSubgraph::validate`]. +#[allow(deprecated)] // Remove enum along with TopoConvexChecker +#[deprecated( + note = "Call validate_with_checker or validate_default instead", + since = "0.27.1" +)] #[derive(Default)] pub enum ValidationMode<'t, 'h, H: HugrView> { /// Check convexity with the given checker. @@ -809,7 +854,15 @@ fn make_pg_subgraph<'h, H: HugrView>( ) -> portgraph::view::Subgraph> { // Ordering of the edges here is preserved and becomes ordering of the // signature. - let boundary = make_boundary::(®ion, node_map, inputs, outputs); + let to_pg_index = |n: H::Node, p: Port| { + region + .port_index(node_map.to_portgraph(n), p.pg_offset()) + .unwrap() + }; + let boundary = Boundary::new( + iter_incoming(inputs).map(|(n, p)| to_pg_index(n, p.into())), + iter_outgoing(outputs).map(|(n, p)| to_pg_index(n, p.into())), + ); portgraph::view::Subgraph::new_subgraph(region, boundary) } @@ -992,23 +1045,7 @@ fn check_parent<'a, N: HugrNode>( Ok(first_parent) } -fn make_boundary<'a, H: HugrView>( - region: &impl LinkView, - node_map: &H::RegionPortgraphNodes, - inputs: &'a IncomingPorts, - outputs: &'a OutgoingPorts, -) -> Boundary { - let to_pg_index = |n: H::Node, p: Port| { - region - .port_index(node_map.to_portgraph(n), p.pg_offset()) - .unwrap() - }; - Boundary::new( - iter_incoming(inputs).map(|(n, p)| to_pg_index(n, p.into())), - iter_outgoing(outputs).map(|(n, p)| to_pg_index(n, p.into())), - ) -} - +// I'd deprecate this if it were `pub` but it isn't, so, fine type CheckerRegion<'g, Base> = portgraph::view::FlatRegion<'g, ::RegionPortgraph<'g>>; @@ -1018,8 +1055,15 @@ type CheckerRegion<'g, Base> = /// convexity checking. /// /// This a good default choice for most convexity checking use cases. -pub type TopoConvexChecker<'g, Base> = - ConvexChecker<'g, Base, portgraph::algorithms::TopoConvexChecker>>; +#[deprecated( + note = "Use SchedGraphChecker or LineConvexChecker instead", + since = "0.27.1" +)] +pub type TopoConvexChecker<'g, Base> = PortgraphCheckerWithNodes< + 'g, + Base, + portgraph::algorithms::TopoConvexChecker>, +>; /// Precompute convexity information for a HUGR. /// @@ -1028,19 +1072,22 @@ pub type TopoConvexChecker<'g, Base> = /// /// This is a good choice for checking convexity of circuit-like graphs, /// particularly when many checks must be performed. -pub type LineConvexChecker<'g, Base> = - ConvexChecker<'g, Base, portgraph::algorithms::LineConvexChecker>>; +pub type LineConvexChecker<'g, Base> = PortgraphCheckerWithNodes< + 'g, + Base, + portgraph::algorithms::LineConvexChecker>, +>; -/// Precompute convexity information for a HUGR. +/// Precompute convexity information for a Portgraph view of a Hugr. /// /// This can be used when constructing multiple sibling subgraphs to speed up /// convexity checking. /// /// This type is generic over the convexity checker used. If checking convexity -/// for circuit-like graphs, use [`LineConvexChecker`], otherwise use +/// for circuit-like graphs, use [`LineConvexChecker`]. Alternatively, use [SchedGraphChecker]. /// [`TopoConvexChecker`]. #[derive(Clone)] -pub struct ConvexChecker<'g, Base: HugrView, Checker> { +pub struct PortgraphCheckerWithNodes<'g, Base: HugrView, Checker> { /// The base HUGR to check convexity on. base: &'g Base, /// The parent of the region where we are checking convexity. @@ -1051,14 +1098,20 @@ pub struct ConvexChecker<'g, Base: HugrView, Checker> { node_map: Base::RegionPortgraphNodes, } -impl<'g, Base, Checker> ConvexChecker<'g, Base, Checker> +#[deprecated(note = "Use PortgraphCheckerWithNodes instead", since = "0.27.1")] +/// Use [PortgraphCheckerWithNodes] +pub type ConvexChecker<'g, Base, Checker> = PortgraphCheckerWithNodes<'g, Base, Checker>; + +impl<'g, Base, Checker> PortgraphCheckerWithNodes<'g, Base, Checker> where Base: HugrView, Checker: CreateConvexChecker>, { /// Create a new convexity checker. pub fn new(base: &'g Base, region_parent: Base::Node) -> Self { - let (region, node_map) = base.region_portgraph(region_parent); + let (region, node_map) = base + .scheduling_graph(region_parent) + .portgraph_no_syn_edges(); let checker = Checker::new_convex_checker(region); Self { base, @@ -1081,7 +1134,8 @@ where } } -impl<'g, Base, Checker> portgraph::algorithms::ConvexChecker for ConvexChecker<'g, Base, Checker> +impl<'g, Base, Checker> portgraph::algorithms::ConvexChecker + for PortgraphCheckerWithNodes<'g, Base, Checker> where Base: HugrView, Checker: CreateConvexChecker, NodeIndexBase = u32, PortIndexBase = u32>, @@ -1526,797 +1580,97 @@ fn has_unique_linear_ports(host: &H, ports: &OutgoingPorts linear_ports.len() == unique_ports.len() } -#[cfg(test)] -mod tests { - use std::collections::BTreeSet; - - use cool_asserts::assert_matches; - use rstest::{fixture, rstest}; - - use crate::builder::{endo_sig, inout_sig}; - use crate::extension::prelude::{MakeTuple, UnpackTuple}; - use crate::hugr::Patch; - use crate::hugr::internal::HugrMutInternals; - use crate::ops::Const; - use crate::ops::handle::DataflowParentID; - use crate::std_extensions::arithmetic::float_types::ConstF64; - use crate::std_extensions::logic::LogicOp; - use crate::type_row; - use crate::utils::test_quantum_extension::{cx_gate, rz_f64}; - use crate::{ - builder::{ - BuildError, DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder, - ModuleBuilder, - }, - extension::prelude::{bool_t, qb_t}, - ops::handle::{DfgID, FuncID, NodeHandle}, - std_extensions::logic::test::and_op, - }; +/// A [HugrConvexChecker] that works on a [SchedulingGraph] +pub struct SchedGraphChecker<'h, H: HugrView + 'h> { + node_map: H::RegionPortgraphNodes, + region_parent: H::Node, + checker: convex::TopoConvexChecker< + SynEdgeWrapper>>, + >, +} - use super::*; - - impl SiblingSubgraph { - /// Create a sibling subgraph containing every node in a HUGR region. - /// - /// This will return an [`InvalidSubgraph::EmptySubgraph`] error if the - /// subgraph is empty. - fn from_sibling_graph( - hugr: &impl HugrView, - parent: N, - ) -> Result> { - let nodes = hugr.children(parent).collect_vec(); - if nodes.is_empty() { - Err(InvalidSubgraph::EmptySubgraph) - } else { - Ok(Self { - nodes, - inputs: Vec::new(), - outputs: Vec::new(), - function_calls: Vec::new(), - }) - } +impl<'h, H: HugrView> SchedGraphChecker<'h, H> { + /// Creates a new instance from a [SchedulingGraph]. This performs some precomputation, + /// so it is more efficient to reuse the same instance for multiple checks on the same graph. + pub fn new(graph: SchedulingGraph<'h, H>) -> Self { + let SchedulingGraph { + graph, + node_map, + region_parent, + } = graph; + let checker = convex::TopoConvexChecker::new(graph); + Self { + node_map, + region_parent, + checker, } } +} - /// A Module with a single function from three qubits to three qubits. - /// The function applies a CX gate to the first two qubits and a Rz gate - /// (with a constant angle) to the last qubit. - fn build_hugr() -> Result<(Hugr, Node), BuildError> { - let mut mod_builder = ModuleBuilder::new(); - let func = - mod_builder.declare("test", Signature::new_endo([qb_t(), qb_t(), qb_t()]).into())?; - let func_id = { - let mut dfg = mod_builder.define_declaration(&func)?; - let [w0, w1, w2] = dfg.input_wires_arr(); - let [w0, w1] = dfg.add_dataflow_op(cx_gate(), [w0, w1])?.outputs_arr(); - let c = dfg.add_load_const(Const::new(ConstF64::new(0.5).into())); - let [w2] = dfg.add_dataflow_op(rz_f64(), [w2, c])?.outputs_arr(); - dfg.finish_with_outputs([w0, w1, w2])? - }; - let hugr = mod_builder - .finish_hugr() - .map_err(|e| -> BuildError { e.into() })?; - Ok((hugr, func_id.node())) - } - - /// A bool to bool hugr with three subsequent NOT gates. - fn build_3not_hugr() -> Result<(Hugr, Node), BuildError> { - let mut mod_builder = ModuleBuilder::new(); - let func = mod_builder.declare("test", Signature::new_endo([bool_t()]).into())?; - let func_id = { - let mut dfg = mod_builder.define_declaration(&func)?; - let outs1 = dfg.add_dataflow_op(LogicOp::Not, dfg.input_wires())?; - let outs2 = dfg.add_dataflow_op(LogicOp::Not, outs1.outputs())?; - let outs3 = dfg.add_dataflow_op(LogicOp::Not, outs2.outputs())?; - dfg.finish_with_outputs(outs3.outputs())? - }; - let hugr = mod_builder - .finish_hugr() - .map_err(|e| -> BuildError { e.into() })?; - Ok((hugr, func_id.node())) - } - - /// A bool to (bool, bool) with multiports. - fn build_multiport_hugr() -> Result<(Hugr, Node), BuildError> { - let mut mod_builder = ModuleBuilder::new(); - let func = mod_builder.declare( - "test", - Signature::new([bool_t()], vec![bool_t(), bool_t()]).into(), - )?; - let func_id = { - let mut dfg = mod_builder.define_declaration(&func)?; - let [b0] = dfg.input_wires_arr(); - let [b1] = dfg.add_dataflow_op(LogicOp::Not, [b0])?.outputs_arr(); - let [b2] = dfg.add_dataflow_op(LogicOp::Not, [b1])?.outputs_arr(); - dfg.finish_with_outputs([b1, b2])? - }; - let hugr = mod_builder - .finish_hugr() - .map_err(|e| -> BuildError { e.into() })?; - Ok((hugr, func_id.node())) - } - - /// A HUGR with a copy - fn build_hugr_classical() -> Result<(Hugr, Node), BuildError> { - let mut mod_builder = ModuleBuilder::new(); - let func = mod_builder.declare("test", Signature::new_endo([bool_t()]).into())?; - let func_id = { - let mut dfg = mod_builder.define_declaration(&func)?; - let in_wire = dfg.input_wires().exactly_one().unwrap(); - let outs = dfg.add_dataflow_op(and_op(), [in_wire, in_wire])?; - dfg.finish_with_outputs(outs.outputs())? - }; - let hugr = mod_builder - .finish_hugr() - .map_err(|e| -> BuildError { e.into() })?; - Ok((hugr, func_id.node())) - } - - #[test] - fn construct_simple_replacement() -> Result<(), InvalidSubgraph> { - let (mut hugr, func_root) = build_hugr().unwrap(); - let func = hugr.with_entrypoint(func_root); - let sub = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( - RootChecked::try_new(&func).expect("Root should be FuncDefn."), - )?; - assert!(sub.validate(&func, Default::default()).is_ok()); - - let empty_dfg = { - let builder = DFGBuilder::new(Signature::new_endo([qb_t(), qb_t(), qb_t()])).unwrap(); - let inputs = builder.input_wires(); - builder.finish_hugr_with_outputs(inputs).unwrap() - }; - - let rep = sub.create_simple_replacement(&func, empty_dfg).unwrap(); - - assert_eq!(rep.subgraph().nodes().len(), 4); - - assert_eq!(hugr.num_nodes(), 8); // Module + Def + In + CX + Rz + Const + LoadConst + Out - hugr.apply_patch(rep).unwrap(); - assert_eq!(hugr.num_nodes(), 4); // Module + Def + In + Out - - Ok(()) - } - - /// Make a sibling subgraph from a constant and a LoadConst node. - #[test] - fn construct_load_const_subgraph() -> Result<(), InvalidSubgraph> { - let (hugr, func_root) = build_hugr().unwrap(); - - let const_node = hugr - .children(func_root) - .find(|&n| hugr.get_optype(n).is_const()) - .unwrap(); - let load_const_node = hugr - .children(func_root) - .find(|&n| hugr.get_optype(n).is_load_constant()) - .unwrap(); - let nodes: BTreeSet<_> = BTreeSet::from_iter([const_node, load_const_node]); - - let sub = SiblingSubgraph::try_from_nodes(vec![const_node, load_const_node], &hugr)?; - - let subgraph_nodes: BTreeSet<_> = sub.nodes().iter().copied().collect(); - assert_eq!(subgraph_nodes, nodes); - - Ok(()) - } - - #[test] - fn test_signature() -> Result<(), InvalidSubgraph> { - let (hugr, dfg) = build_hugr().unwrap(); - let func = hugr.with_entrypoint(dfg); - let sub = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( - RootChecked::try_new(&func).expect("Root should be FuncDefn."), - )?; - assert!(sub.validate(&func, Default::default()).is_ok()); - assert_eq!( - sub.signature(&func), - Signature::new_endo([qb_t(), qb_t(), qb_t()]) - ); - Ok(()) - } - - #[test] - fn construct_simple_replacement_invalid_signature() -> Result<(), InvalidSubgraph> { - let (hugr, dfg) = build_hugr().unwrap(); - let func = hugr.with_entrypoint(dfg); - let sub = SiblingSubgraph::from_sibling_graph(&hugr, dfg)?; - - let empty_dfg = { - let builder = DFGBuilder::new(Signature::new_endo([qb_t()])).unwrap(); - let inputs = builder.input_wires(); - builder.finish_hugr_with_outputs(inputs).unwrap() - }; - - assert_matches!( - sub.create_simple_replacement(&func, empty_dfg).unwrap_err(), - InvalidReplacement::InvalidSignature { .. } - ); - Ok(()) - } - - #[test] - fn convex_subgraph() { - let (hugr, func_root) = build_hugr().unwrap(); - let func = hugr.with_entrypoint(func_root); - assert_eq!( - SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( - RootChecked::try_new(&func).expect("Root should be FuncDefn.") - ) - .unwrap() - .nodes() - .len(), - 4 - ); - } - - #[test] - fn with_checker() { - let (mut hugr, func_root) = build_hugr().unwrap(); - hugr.set_entrypoint(func_root); - let mut hugr2 = hugr.clone(); - match hugr2.optype_mut(func_root) { - OpType::FuncDefn(fd) => *fd.func_name_mut() = "test2".into(), - _ => panic!(), - }; - let func2 = hugr - .insert_hugr(hugr.module_root(), hugr2) - .inserted_entrypoint; - hugr.validate().unwrap(); - - let checker1 = TopoConvexChecker::new(&hugr, func_root); - let checker2 = TopoConvexChecker::new(&hugr, func2); - let sub1 = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( - RootChecked::try_new(&hugr).expect("Root should be FuncDefn."), - ) - .unwrap(); - sub1.validate(&hugr, ValidationMode::WithChecker(&checker1)) - .unwrap(); - let e = sub1.validate(&hugr, ValidationMode::WithChecker(&checker2)); - assert_eq!( - e, - Err(InvalidSubgraph::MismatchedCheckerParent { - checker_parent: func2, - subgraph_parent: func_root - }) - ); - - SiblingSubgraph::try_new_with_checker( - sub1.inputs.clone(), - sub1.outputs.clone(), - &hugr, - &checker1, - ) - .unwrap(); - let e = SiblingSubgraph::try_new_with_checker( - sub1.inputs.clone(), - sub1.outputs.clone(), - &hugr, - &checker2, - ); - assert_eq!( - e, - Err(InvalidSubgraph::MismatchedCheckerParent { - checker_parent: func2, - subgraph_parent: func_root - }) - ); - - SiblingSubgraph::try_from_nodes_with_checker(sub1.nodes.clone(), &hugr, &checker1).unwrap(); - let e = SiblingSubgraph::try_from_nodes_with_checker(sub1.nodes.clone(), &hugr, &checker2); - assert_eq!( - e, - Err(InvalidSubgraph::MismatchedCheckerParent { - checker_parent: func2, - subgraph_parent: func_root - }) - ); - } - - #[test] - fn convex_subgraph_2() { - let (hugr, func_root) = build_hugr().unwrap(); - let [inp, out] = hugr.get_io(func_root).unwrap(); - let func = hugr.with_entrypoint(func_root); - // All graph except input/output nodes - SiblingSubgraph::try_new( - hugr.node_outputs(inp) - .take(2) - .map(|p| hugr.linked_inputs(inp, p).collect_vec()) - .filter(|ps| !ps.is_empty()) - .collect(), - hugr.node_inputs(out) - .take(2) - .filter_map(|p| hugr.single_linked_output(out, p)) - .collect(), - &func, - ) - .unwrap(); - } - - #[test] - fn degen_boundary() { - let (hugr, func_root) = build_hugr().unwrap(); - let func = hugr.with_entrypoint(func_root); - let [inp, _] = hugr.get_io(func_root).unwrap(); - let first_cx_edge = hugr.node_outputs(inp).next().unwrap(); - // All graph but one edge - assert_matches!( - SiblingSubgraph::try_new( - vec![ - hugr.linked_ports(inp, first_cx_edge) - .map(|(n, p)| (n, p.as_incoming().unwrap())) - .collect() - ], - vec![(inp, first_cx_edge)], - &func, - ), - Err(InvalidSubgraph::InvalidBoundary( - InvalidSubgraphBoundary::DisconnectedBoundaryPort(_, _) - )) - ); - } - - #[test] - fn non_convex_subgraph() { - let (hugr, func_root) = build_3not_hugr().unwrap(); - let func = hugr.with_entrypoint(func_root); - let [inp, _out] = hugr.get_io(func_root).unwrap(); - let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); - let not2 = hugr.output_neighbours(not1).exactly_one().ok().unwrap(); - let not3 = hugr.output_neighbours(not2).exactly_one().ok().unwrap(); - let not1_inp = hugr.node_inputs(not1).next().unwrap(); - let not1_out = hugr.node_outputs(not1).next().unwrap(); - let not3_inp = hugr.node_inputs(not3).next().unwrap(); - let not3_out = hugr.node_outputs(not3).next().unwrap(); - assert_matches!( - SiblingSubgraph::try_new( - vec![vec![(not1, not1_inp)], vec![(not3, not3_inp)]], - vec![(not1, not1_out), (not3, not3_out)], - &func - ), - Err(InvalidSubgraph::NotConvex) - ); - } - - /// A subgraphs mixed with multiports caused a `NonConvex` error. - /// - #[test] - fn convex_multiports() { - let (hugr, func_root) = build_multiport_hugr().unwrap(); - let [inp, out] = hugr.get_io(func_root).unwrap(); - let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); - let not2 = hugr - .output_neighbours(not1) - .filter(|&n| n != out) - .exactly_one() - .ok() - .unwrap(); - - let subgraph = SiblingSubgraph::try_from_nodes([not1, not2], &hugr).unwrap(); - assert_eq!(subgraph.nodes(), [not1, not2]); - } - - #[test] - fn invalid_boundary() { - let (hugr, func_root) = build_hugr().unwrap(); - let func = hugr.with_entrypoint(func_root); - let [inp, out] = hugr.get_io(func_root).unwrap(); - let cx_edges_in = hugr.node_outputs(inp); - let cx_edges_out = hugr.node_inputs(out); - // All graph but the CX - assert_matches!( - SiblingSubgraph::try_new( - cx_edges_out.map(|p| vec![(out, p)]).collect(), - cx_edges_in.map(|p| (inp, p)).collect(), - &func, - ), - Err(InvalidSubgraph::InvalidBoundary( - InvalidSubgraphBoundary::DisconnectedBoundaryPort(_, _) - )) - ); - } - - #[test] - fn preserve_signature() { - let (hugr, func_root) = build_hugr_classical().unwrap(); - let func_graph = hugr.with_entrypoint(func_root); - let func = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( - RootChecked::try_new(&func_graph).expect("Root should be FuncDefn."), - ) - .unwrap(); - let func_defn = hugr.get_optype(func_root).as_func_defn().unwrap(); - assert_eq!(func_defn.signature(), &func.signature(&func_graph).into()); - } - - #[test] - fn extract_subgraph() { - let (hugr, func_root) = build_hugr().unwrap(); - let func_graph = hugr.with_entrypoint(func_root); - let subgraph = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( - RootChecked::try_new(&func_graph).expect("Root should be FuncDefn."), - ) - .unwrap(); - let extracted = subgraph.extract_subgraph(&hugr, "region"); - - extracted.validate().unwrap(); +impl HugrConvexChecker for SchedGraphChecker<'_, H> { + fn region_parent(&self) -> H::Node { + self.region_parent } - - #[test] - fn edge_both_output_and_copy() { - // https://github.com/CQCL/hugr/issues/518 - let one_bit = vec![bool_t()]; - let two_bit = vec![bool_t(), bool_t()]; - - let mut builder = DFGBuilder::new(inout_sig(one_bit.clone(), two_bit.clone())).unwrap(); - let inw = builder.input_wires().exactly_one().unwrap(); - let outw1 = builder - .add_dataflow_op(LogicOp::Not, [inw]) - .unwrap() - .out_wire(0); - let outw2 = builder - .add_dataflow_op(and_op(), [inw, outw1]) - .unwrap() - .outputs(); - let outw = [outw1].into_iter().chain(outw2); - let h = builder.finish_hugr_with_outputs(outw).unwrap(); - let subg = SiblingSubgraph::try_new_dataflow_subgraph::<_, DfgID>( - RootChecked::try_new(&h).expect("Root should be DFG."), + fn nodes_if_convex( + &self, + hugr: &impl HugrView, + inputs: &IncomingPorts, + outputs: &OutgoingPorts, + function_calls: &IncomingPorts, + ) -> Result, InvalidSubgraph> { + // Compute the nodes inside the boundary ignoring synthetic edges - + // there is no way to specify a synthetic edge as part of the boundary, + // and if there are any nonlocal edges not part of the boundary (the condition + // that would lead to needing those synthetic edges) then the subgraph is invalid + // (nodes would not share the same parent)! + let node_indices = make_pg_subgraph::( + self.checker.graph().region_view.clone(), + inputs, + outputs, + &self.node_map, ) - .unwrap(); - assert_eq!(subg.nodes().len(), 2); - } - - #[test] - fn test_unconnected() { - // test a replacement on a subgraph with a discarded output - let mut b = DFGBuilder::new(Signature::new([bool_t()], type_row![])).unwrap(); - let inw = b.input_wires().exactly_one().unwrap(); - let not_n = b.add_dataflow_op(LogicOp::Not, [inw]).unwrap(); - // Unconnected output, discarded - let mut h = b.finish_hugr_with_outputs([]).unwrap(); - - let subg = SiblingSubgraph::from_node(not_n.node(), &h); - - assert_eq!(subg.nodes().len(), 1); - // TODO create a valid replacement - let replacement = { - let mut rep_b = DFGBuilder::new(Signature::new_endo([bool_t()])).unwrap(); - let inw = rep_b.input_wires().exactly_one().unwrap(); - - let not_n = rep_b.add_dataflow_op(LogicOp::Not, [inw]).unwrap(); - - rep_b.finish_hugr_with_outputs(not_n.outputs()).unwrap() - }; - let rep = subg.create_simple_replacement(&h, replacement).unwrap(); - rep.apply(&mut h).unwrap(); - } - - /// Test the behaviour of the sibling subgraph when built from a single - /// node. - #[test] - fn single_node_subgraph() { - // A hugr with a single NOT operation, with disconnected output. - let mut b = DFGBuilder::new(Signature::new([bool_t()], type_row![])).unwrap(); - let inw = b.input_wires().exactly_one().unwrap(); - let not_n = b.add_dataflow_op(LogicOp::Not, [inw]).unwrap(); - // Unconnected output, discarded - let h = b.finish_hugr_with_outputs([]).unwrap(); - - // When built with `from_node`, the subgraph's signature is the same as the - // node's. (bool input, bool output) - let subg = SiblingSubgraph::from_node(not_n.node(), &h); - assert_eq!(subg.nodes().len(), 1); - assert_eq!( - subg.signature(&h).io(), - Signature::new(vec![bool_t()], vec![bool_t()]).io() - ); - - // `from_nodes` is different, is it only uses incoming and outgoing edges to - // compute the signature. In this case, the output is disconnected, so - // it is not part of the subgraph signature. - let subg = SiblingSubgraph::try_from_nodes([not_n.node()], &h).unwrap(); - assert_eq!(subg.nodes().len(), 1); - assert_eq!( - subg.signature(&h).io(), - Signature::new(vec![bool_t()], vec![]).io() - ); - } - - /// Test the behaviour of the sibling subgraph when built from a single - /// node with no inputs or outputs. - #[test] - fn singleton_disconnected_subgraph() { - // A hugr with some empty MakeTuple operations. - let op = MakeTuple::new(type_row![]); - - let mut b = DFGBuilder::new(Signature::new_endo(type_row![])).unwrap(); - let _mk_tuple_1 = b.add_dataflow_op(op.clone(), []).unwrap(); - let mk_tuple_2 = b.add_dataflow_op(op.clone(), []).unwrap(); - let _mk_tuple_3 = b.add_dataflow_op(op, []).unwrap(); - // Unconnected output, discarded - let h = b.finish_hugr_with_outputs([]).unwrap(); - - // When built with `try_from_nodes`, the subgraph's signature is the same as the - // node's. (empty input, tuple output) - let subg = SiblingSubgraph::from_node(mk_tuple_2.node(), &h); - assert_eq!(subg.nodes().len(), 1); - assert_eq!( - subg.signature(&h).io(), - Signature::new(type_row![], vec![Type::new_tuple(type_row![])]).io() - ); - - // `from_nodes` is different, is it only uses incoming and outgoing edges to - // compute the signature. In this case, the output is disconnected, so - // it is not part of the subgraph signature. - let subg = SiblingSubgraph::try_from_nodes([mk_tuple_2.node()], &h).unwrap(); - assert_eq!(subg.nodes().len(), 1); - assert_eq!( - subg.signature(&h).io(), - Signature::new_endo(type_row![]).io() - ); - } - - /// Run `try_from_nodes` including some complete graph components. - #[test] - fn partially_connected_subgraph() { - // A hugr with some empty MakeTuple operations. - let tuple_op = MakeTuple::new(type_row![]); - let untuple_op = UnpackTuple::new(type_row![]); - let tuple_t = Type::new_tuple(type_row![]); - - let mut b = DFGBuilder::new(Signature::new(type_row![], vec![tuple_t.clone()])).unwrap(); - let mk_tuple_1 = b.add_dataflow_op(tuple_op.clone(), []).unwrap(); - let untuple_1 = b - .add_dataflow_op(untuple_op.clone(), [mk_tuple_1.out_wire(0)]) - .unwrap(); - let mk_tuple_2 = b.add_dataflow_op(tuple_op.clone(), []).unwrap(); - let _mk_tuple_3 = b.add_dataflow_op(tuple_op, []).unwrap(); - // Output the 2nd tuple output - let h = b - .finish_hugr_with_outputs([mk_tuple_2.out_wire(0)]) - .unwrap(); - - let subgraph_nodes = [mk_tuple_1.node(), mk_tuple_2.node(), untuple_1.node()]; - - // `try_from_nodes` uses incoming and outgoing edges to compute the signature. - let subg = SiblingSubgraph::try_from_nodes(subgraph_nodes, &h).unwrap(); - assert_eq!(subg.nodes().len(), 3); - assert_eq!( - subg.signature(&h).io(), - Signature::new(type_row![], vec![tuple_t]).io() - ); - } - - #[test] - fn test_set_outgoing_ports() { - let (hugr, func_root) = build_3not_hugr().unwrap(); - let [inp, out] = hugr.get_io(func_root).unwrap(); - let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); - let not1_out = hugr.node_outputs(not1).next().unwrap(); - - // Create a subgraph with just the NOT gate - let mut subgraph = SiblingSubgraph::from_node(not1, &hugr); - - // Initially should have one output - assert_eq!(subgraph.outgoing_ports().len(), 1); - - // Try to set two outputs by copying the existing one - let new_outputs = vec![(not1, not1_out), (not1, not1_out)]; - assert!(subgraph.set_outgoing_ports(new_outputs, &hugr).is_ok()); - - // Should now have two outputs - assert_eq!(subgraph.outgoing_ports().len(), 2); - - // Try to set an invalid output (from a different node) - let invalid_outputs = vec![(not1, not1_out), (out, 2.into())]; - assert!(matches!( - subgraph.set_outgoing_ports(invalid_outputs, &hugr), - Err(InvalidOutputPorts::UnknownOutput { .. }) - )); - - // Should still have two outputs from before - assert_eq!(subgraph.outgoing_ports().len(), 2); - } - - #[test] - fn test_set_outgoing_ports_linear() { - let (hugr, func_root) = build_hugr().unwrap(); - let [inp, _out] = hugr.get_io(func_root).unwrap(); - let rz = hugr.output_neighbours(inp).nth(2).unwrap(); - let rz_out = hugr.node_outputs(rz).next().unwrap(); - - // Create a subgraph with just the CX gate - let mut subgraph = SiblingSubgraph::from_node(rz, &hugr); - - // Initially should have one output - assert_eq!(subgraph.outgoing_ports().len(), 1); - - // Try to set two outputs by copying the existing one (should fail for linear - // ports) - let new_outputs = vec![(rz, rz_out), (rz, rz_out)]; - assert!(matches!( - subgraph.set_outgoing_ports(new_outputs, &hugr), - Err(InvalidOutputPorts::NonUniqueLinear) - )); - - // Should still have one output - assert_eq!(subgraph.outgoing_ports().len(), 1); - } - - #[test] - fn test_try_from_nodes_with_intervals() { - let (hugr, func_root) = build_3not_hugr().unwrap(); - let line_checker = LineConvexChecker::new(&hugr, func_root); - let [inp, _out] = hugr.get_io(func_root).unwrap(); - let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); - let not2 = hugr.output_neighbours(not1).exactly_one().ok().unwrap(); - - let intervals = line_checker.get_intervals_from_nodes([not1, not2]).unwrap(); - let subgraph = - SiblingSubgraph::try_from_nodes_with_intervals([not1, not2], &intervals, &line_checker) - .unwrap(); - let exp_subgraph = SiblingSubgraph::try_from_nodes([not1, not2], &hugr).unwrap(); - - assert_eq!(subgraph, exp_subgraph); - assert_eq!( - line_checker.nodes_in_intervals(&intervals).collect_vec(), - [not1, not2] - ); + .node_identifiers() + .collect_vec(); - let intervals2 = line_checker - .get_intervals_from_boundary_ports([ - (not1, IncomingPort::from(0).into()), - (not2, OutgoingPort::from(0).into()), - ]) - .unwrap(); - let subgraph2 = SiblingSubgraph::try_from_nodes_with_intervals( - [not1, not2], - &intervals2, - &line_checker, - ) - .unwrap(); - assert_eq!(subgraph2, exp_subgraph); - } + let nodes = node_indices + .iter() + .map(|&pg_node| self.node_map.from_portgraph(pg_node)) + .collect_vec(); + validate_boundary(hugr, &nodes, inputs, outputs, function_calls)?; - #[test] - fn test_validate() { - let (hugr, func_root) = build_3not_hugr().unwrap(); - let func = hugr.with_entrypoint(func_root); - let checker = TopoConvexChecker::new(&func, func_root); - let [inp, _out] = hugr.get_io(func_root).unwrap(); - let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); - let not2 = hugr.output_neighbours(not1).exactly_one().ok().unwrap(); - let not3 = hugr.output_neighbours(not2).exactly_one().ok().unwrap(); - - // A valid boundary, and convex - let sub = SiblingSubgraph::new_unchecked( - vec![vec![(not1, 0.into())]], - vec![(not2, 0.into())], - vec![], - vec![not1, not2], - ); - assert_eq!(sub.validate(&func, ValidationMode::SkipConvexity), Ok(())); - assert_eq!(sub.validate(&func, ValidationMode::CheckConvexity), Ok(())); - assert_eq!( - sub.validate(&func, ValidationMode::WithChecker(&checker)), - Ok(()) - ); + if nodes.len() <= 1 { + return Ok(nodes); + } + let post_outputs: BTreeSet<_> = outputs + .iter() + .flat_map(|(n, p)| hugr.linked_inputs(*n, *p)) + .collect(); + if inputs.iter().flatten().any(|p| post_outputs.contains(p)) { + return Err(InvalidSubgraph::NotConvex); + } - // A valid boundary, but not convex - let sub = SiblingSubgraph::new_unchecked( - vec![vec![(not1, 0.into())], vec![(not3, 0.into())]], - vec![(not1, 0.into()), (not3, 0.into())], - vec![], - vec![not1, not3], - ); - assert_eq!(sub.validate(&func, ValidationMode::SkipConvexity), Ok(())); - assert_eq!( - sub.validate(&func, ValidationMode::CheckConvexity), - Err(InvalidSubgraph::NotConvex) - ); - assert_eq!( - sub.validate(&func, ValidationMode::WithChecker(&checker)), + if self.checker.is_node_convex(node_indices) { + Ok(nodes) + } else { Err(InvalidSubgraph::NotConvex) - ); - - // An invalid boundary (missing an input) - let sub = SiblingSubgraph::new_unchecked( - vec![vec![(not1, 0.into())]], - vec![(not1, 0.into()), (not3, 0.into())], - vec![], - vec![not1, not3], - ); - assert_eq!( - sub.validate(&func, ValidationMode::SkipConvexity), - Err(InvalidSubgraph::InvalidNodeSet) - ); - } - - #[fixture] - pub(crate) fn hugr_call_subgraph() -> Hugr { - let mut builder = ModuleBuilder::new(); - let decl_node = builder - .declare("test", endo_sig([bool_t()]).into()) - .unwrap(); - let mut main = builder - .define_function("main", endo_sig([bool_t()])) - .unwrap(); - let [bool] = main.input_wires_arr(); - - let [bool] = main - .add_dataflow_op(LogicOp::Not, [bool]) - .unwrap() - .outputs_arr(); - - // Chain two calls to the same function - let [bool] = main.call(&decl_node, &[], [bool]).unwrap().outputs_arr(); - let [bool] = main.call(&decl_node, &[], [bool]).unwrap().outputs_arr(); - - let main_def = main.finish_with_outputs([bool]).unwrap(); - - let mut hugr = builder.finish_hugr().unwrap(); - hugr.set_entrypoint(main_def.node()); - hugr - } - - #[rstest] - fn test_call_subgraph_from_dfg(hugr_call_subgraph: Hugr) { - let subg = SiblingSubgraph::try_new_dataflow_subgraph::<_, DataflowParentID>( - RootChecked::try_new(&hugr_call_subgraph).expect("Root should be DFG container."), - ) - .unwrap(); - - assert_eq!(subg.function_calls.len(), 1); - assert_eq!(subg.function_calls[0].len(), 2); + } } +} - #[rstest] - fn test_call_subgraph_from_nodes(hugr_call_subgraph: Hugr) { - let call_nodes = hugr_call_subgraph - .children(hugr_call_subgraph.entrypoint()) - .filter(|&n| hugr_call_subgraph.get_optype(n).is_call()) - .collect_vec(); - - let subg = - SiblingSubgraph::try_from_nodes(call_nodes.clone(), &hugr_call_subgraph).unwrap(); - assert_eq!(subg.function_calls.len(), 1); - assert_eq!(subg.function_calls[0].len(), 2); - - let subg = - SiblingSubgraph::try_from_nodes(call_nodes[0..1].to_owned(), &hugr_call_subgraph) - .unwrap(); - assert_eq!(subg.function_calls.len(), 1); - assert_eq!(subg.function_calls[0].len(), 1); - } +#[cfg(test)] +mod test_traits_impld { + use crate::{Hugr, HugrView, builder::test::simple_dfg_hugr}; + use portgraph::NodeIndex; + use rstest::rstest; #[rstest] - fn test_call_subgraph_from_boundary(hugr_call_subgraph: Hugr) { - let call_nodes = hugr_call_subgraph - .children(hugr_call_subgraph.entrypoint()) - .filter(|&n| hugr_call_subgraph.get_optype(n).is_call()) - .collect_vec(); - let not_node = hugr_call_subgraph - .children(hugr_call_subgraph.entrypoint()) - .filter(|&n| hugr_call_subgraph.get_optype(n) == &LogicOp::Not.into()) - .exactly_one() - .ok() - .unwrap(); - - let subg = SiblingSubgraph::try_new( - vec![ - vec![(not_node, IncomingPort::from(0))], - call_nodes - .iter() - .map(|&n| (n, IncomingPort::from(1))) - .collect_vec(), - ], - vec![(call_nodes[1], OutgoingPort::from(0))], - &hugr_call_subgraph, - ) - .unwrap(); - - assert_eq!(subg.function_calls.len(), 1); - assert_eq!(subg.function_calls[0].len(), 2); + fn test(simple_dfg_hugr: Hugr) { + let sg = simple_dfg_hugr.scheduling_graph(simple_dfg_hugr.module_root()); + // Just to check that this compiles, never mind the actual result. + super::convex::TopoConvexChecker::new(sg.petgraph()) + .is_node_convex([NodeIndex::new(0), NodeIndex::new(2)]); } } + +#[cfg(test)] +mod tests; diff --git a/hugr-core/src/hugr/views/sibling_subgraph/convex.rs b/hugr-core/src/hugr/views/sibling_subgraph/convex.rs new file mode 100644 index 0000000000..dd18bd05af --- /dev/null +++ b/hugr-core/src/hugr/views/sibling_subgraph/convex.rs @@ -0,0 +1,117 @@ +use petgraph::visit::{ + GraphBase, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount, Topo, Visitable, Walker, +}; +use std::collections::BTreeSet; + +use portgraph::{SecondaryMap, UnmanagedDenseMap}; + +/// Convexity checking using a pre-computed topological node order. +pub(super) struct TopoConvexChecker { + graph: G, + // The nodes in topological order + topsort_nodes: Vec, + // The index of a node in the topological order (the inverse of topsort_nodes) + topsort_ind: UnmanagedDenseMap, +} + +impl TopoConvexChecker +where + for<'a> &'a G: IntoNeighborsDirected + IntoNodeIdentifiers + Visitable, + G::NodeId: Into + TryFrom + Copy, +{ + /// Create a new ConvexChecker. + pub fn new(graph: G) -> Self { + let topsort_nodes: Vec<_> = Topo::new(&graph).iter(&graph).collect(); + let mut topsort_ind = UnmanagedDenseMap::with_capacity(graph.node_count()); + for (i, &n) in topsort_nodes.iter().enumerate() { + topsort_ind.set(n, i); + } + Self { + graph, + topsort_nodes, + topsort_ind, + } + } + + /// The graph on which convexity queries can be made. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Whether the subgraph induced by the node set is convex. + /// + /// An induced subgraph is convex if there is no node that is both in the + /// past and in the future of some nodes in the subgraph. + /// + /// ## Arguments + /// + /// - `nodes`: The nodes inducing a subgraph of `self.graph()`. + /// + /// ## Algorithm + /// + /// Each node in the "vicinity" of the subgraph will be assigned a causal + /// property, either of being in the past or in the future of the subgraph. + /// It can then be checked whether there is a node in the past that is also + /// in the future, violating convexity. + /// + /// Currently, the "vicinity" of a subgraph is defined as the set of nodes + /// that are in the interval between the first and last node of the subgraph + /// in some topological order. In the worst case this will traverse every + /// node in the graph and can be improved on in the future. + pub fn is_node_convex(&self, nodes: impl IntoIterator) -> bool { + // The nodes in the subgraph, in topological order. + let nodes: BTreeSet<_> = nodes.into_iter().map(|n| self.topsort_ind[n]).collect(); + if nodes.len() <= 1 { + return true; + } + + // The range of considered nodes, as positions in the toposorted vector. + // Since the nodes are ordered, any node outside of this range will + // necessarily be outside the convex hull. + let min_ind = *nodes.first().unwrap(); + let max_ind = *nodes.last().unwrap(); + let node_range = min_ind..=max_ind; + + let mut node_iter = nodes.iter().copied().peekable(); + + // Nodes in the causal future of `nodes` (inside `node_range`). + let mut other_nodes = BTreeSet::new(); + + loop { + if node_iter.peek().is_none() { + break; + } + if other_nodes.is_empty() || node_iter.peek() < other_nodes.first() { + let current = node_iter.next().unwrap(); + let current_node = self.topsort_nodes[current]; + for neighbour in self + .graph + .neighbors_directed(current_node, petgraph::Direction::Outgoing) + .map(|n| self.topsort_ind[n]) + .filter(|ind| node_range.contains(ind)) + { + if !nodes.contains(&neighbour) { + other_nodes.insert(neighbour); + } + } + } else { + let current = other_nodes.pop_first().unwrap(); + let current_node = self.topsort_nodes[current]; + for neighbour in self + .graph + .neighbors_directed(current_node, petgraph::Direction::Outgoing) + .map(|n| self.topsort_ind[n]) + .filter(|ind| node_range.contains(ind)) + { + if nodes.contains(&neighbour) { + // A non-subgraph node in the causal future of the subgraph has an output neighbour in the subgraph. + return false; + } else { + other_nodes.insert(neighbour); + } + } + } + } + true + } +} diff --git a/hugr-core/src/hugr/views/sibling_subgraph/tests.rs b/hugr-core/src/hugr/views/sibling_subgraph/tests.rs new file mode 100644 index 0000000000..4a033f6d26 --- /dev/null +++ b/hugr-core/src/hugr/views/sibling_subgraph/tests.rs @@ -0,0 +1,864 @@ +use std::collections::BTreeSet; + +use cool_asserts::assert_matches; +use rstest::{fixture, rstest}; + +use crate::builder::{endo_sig, inout_sig}; +use crate::extension::prelude::{MakeTuple, UnpackTuple}; +use crate::hugr::Patch; +use crate::hugr::internal::HugrMutInternals; +use crate::ops::Const; +use crate::ops::handle::DataflowParentID; +use crate::std_extensions::arithmetic::float_types::ConstF64; +use crate::std_extensions::logic::LogicOp; +use crate::type_row; +use crate::utils::test_quantum_extension::{cx_gate, rz_f64}; +use crate::{ + builder::{ + BuildError, DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, HugrBuilder, + ModuleBuilder, + }, + extension::prelude::{bool_t, qb_t}, + ops::handle::{DfgID, FuncID, NodeHandle}, + std_extensions::logic::test::and_op, +}; + +use super::*; + +impl SiblingSubgraph { + /// Create a sibling subgraph containing every node in a HUGR region. + /// + /// This will return an [`InvalidSubgraph::EmptySubgraph`] error if the + /// subgraph is empty. + fn from_sibling_graph( + hugr: &impl HugrView, + parent: N, + ) -> Result> { + let nodes = hugr.children(parent).collect_vec(); + if nodes.is_empty() { + Err(InvalidSubgraph::EmptySubgraph) + } else { + Ok(Self { + nodes, + inputs: Vec::new(), + outputs: Vec::new(), + function_calls: Vec::new(), + }) + } + } +} + +/// A Module with a single function from three qubits to three qubits. +/// The function applies a CX gate to the first two qubits and a Rz gate +/// (with a constant angle) to the last qubit. +fn build_hugr() -> Result<(Hugr, Node), BuildError> { + let mut mod_builder = ModuleBuilder::new(); + let func = mod_builder.declare("test", Signature::new_endo([qb_t(), qb_t(), qb_t()]).into())?; + let func_id = { + let mut dfg = mod_builder.define_declaration(&func)?; + let [w0, w1, w2] = dfg.input_wires_arr(); + let [w0, w1] = dfg.add_dataflow_op(cx_gate(), [w0, w1])?.outputs_arr(); + let c = dfg.add_load_const(Const::new(ConstF64::new(0.5).into())); + let [w2] = dfg.add_dataflow_op(rz_f64(), [w2, c])?.outputs_arr(); + dfg.finish_with_outputs([w0, w1, w2])? + }; + let hugr = mod_builder + .finish_hugr() + .map_err(|e| -> BuildError { e.into() })?; + Ok((hugr, func_id.node())) +} + +/// A bool to bool hugr with three subsequent NOT gates. +fn build_3not_hugr() -> Result<(Hugr, Node), BuildError> { + let mut mod_builder = ModuleBuilder::new(); + let func = mod_builder.declare("test", Signature::new_endo([bool_t()]).into())?; + let func_id = { + let mut dfg = mod_builder.define_declaration(&func)?; + let outs1 = dfg.add_dataflow_op(LogicOp::Not, dfg.input_wires())?; + let outs2 = dfg.add_dataflow_op(LogicOp::Not, outs1.outputs())?; + let outs3 = dfg.add_dataflow_op(LogicOp::Not, outs2.outputs())?; + dfg.finish_with_outputs(outs3.outputs())? + }; + let hugr = mod_builder + .finish_hugr() + .map_err(|e| -> BuildError { e.into() })?; + Ok((hugr, func_id.node())) +} + +/// A bool to (bool, bool) with multiports. +fn build_multiport_hugr() -> Result<(Hugr, Node), BuildError> { + let mut mod_builder = ModuleBuilder::new(); + let func = mod_builder.declare( + "test", + Signature::new([bool_t()], vec![bool_t(), bool_t()]).into(), + )?; + let func_id = { + let mut dfg = mod_builder.define_declaration(&func)?; + let [b0] = dfg.input_wires_arr(); + let [b1] = dfg.add_dataflow_op(LogicOp::Not, [b0])?.outputs_arr(); + let [b2] = dfg.add_dataflow_op(LogicOp::Not, [b1])?.outputs_arr(); + dfg.finish_with_outputs([b1, b2])? + }; + let hugr = mod_builder + .finish_hugr() + .map_err(|e| -> BuildError { e.into() })?; + Ok((hugr, func_id.node())) +} + +/// A HUGR with a copy +fn build_hugr_classical() -> Result<(Hugr, Node), BuildError> { + let mut mod_builder = ModuleBuilder::new(); + let func = mod_builder.declare("test", Signature::new_endo([bool_t()]).into())?; + let func_id = { + let mut dfg = mod_builder.define_declaration(&func)?; + let in_wire = dfg.input_wires().exactly_one().unwrap(); + let outs = dfg.add_dataflow_op(and_op(), [in_wire, in_wire])?; + dfg.finish_with_outputs(outs.outputs())? + }; + let hugr = mod_builder + .finish_hugr() + .map_err(|e| -> BuildError { e.into() })?; + Ok((hugr, func_id.node())) +} + +#[test] +fn construct_simple_replacement() -> Result<(), InvalidSubgraph> { + let (mut hugr, func_root) = build_hugr().unwrap(); + let func = hugr.with_entrypoint(func_root); + let sub = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( + RootChecked::try_new(&func).expect("Root should be FuncDefn."), + )?; + assert!(sub.validate_default(&func).is_ok()); + + let empty_dfg = { + let builder = DFGBuilder::new(Signature::new_endo([qb_t(), qb_t(), qb_t()])).unwrap(); + let inputs = builder.input_wires(); + builder.finish_hugr_with_outputs(inputs).unwrap() + }; + + let rep = sub.create_simple_replacement(&func, empty_dfg).unwrap(); + + assert_eq!(rep.subgraph().nodes().len(), 4); + + assert_eq!(hugr.num_nodes(), 8); // Module + Def + In + CX + Rz + Const + LoadConst + Out + hugr.apply_patch(rep).unwrap(); + assert_eq!(hugr.num_nodes(), 4); // Module + Def + In + Out + + Ok(()) +} + +/// Make a sibling subgraph from a constant and a LoadConst node. +#[test] +fn construct_load_const_subgraph() -> Result<(), InvalidSubgraph> { + let (hugr, func_root) = build_hugr().unwrap(); + + let const_node = hugr + .children(func_root) + .find(|&n| hugr.get_optype(n).is_const()) + .unwrap(); + let load_const_node = hugr + .children(func_root) + .find(|&n| hugr.get_optype(n).is_load_constant()) + .unwrap(); + let nodes: BTreeSet<_> = BTreeSet::from_iter([const_node, load_const_node]); + + let sub = SiblingSubgraph::try_from_nodes(vec![const_node, load_const_node], &hugr)?; + + let subgraph_nodes: BTreeSet<_> = sub.nodes().iter().copied().collect(); + assert_eq!(subgraph_nodes, nodes); + + Ok(()) +} + +#[test] +fn test_signature() -> Result<(), InvalidSubgraph> { + let (hugr, dfg) = build_hugr().unwrap(); + let func = hugr.with_entrypoint(dfg); + let sub = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( + RootChecked::try_new(&func).expect("Root should be FuncDefn."), + )?; + assert!(sub.validate_default(&func).is_ok()); + assert_eq!( + sub.signature(&func), + Signature::new_endo([qb_t(), qb_t(), qb_t()]) + ); + Ok(()) +} + +#[test] +fn construct_simple_replacement_invalid_signature() -> Result<(), InvalidSubgraph> { + let (hugr, dfg) = build_hugr().unwrap(); + let func = hugr.with_entrypoint(dfg); + let sub = SiblingSubgraph::from_sibling_graph(&hugr, dfg)?; + + let empty_dfg = { + let builder = DFGBuilder::new(Signature::new_endo([qb_t()])).unwrap(); + let inputs = builder.input_wires(); + builder.finish_hugr_with_outputs(inputs).unwrap() + }; + + assert_matches!( + sub.create_simple_replacement(&func, empty_dfg).unwrap_err(), + InvalidReplacement::InvalidSignature { .. } + ); + Ok(()) +} + +#[test] +fn convex_subgraph() { + let (hugr, func_root) = build_hugr().unwrap(); + let func = hugr.with_entrypoint(func_root); + assert_eq!( + SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( + RootChecked::try_new(&func).expect("Root should be FuncDefn.") + ) + .unwrap() + .nodes() + .len(), + 4 + ); +} + +#[test] +fn with_checker() { + let (mut hugr, func_root) = build_hugr().unwrap(); + hugr.set_entrypoint(func_root); + let mut hugr2 = hugr.clone(); + match hugr2.optype_mut(func_root) { + OpType::FuncDefn(fd) => *fd.func_name_mut() = "test2".into(), + _ => panic!(), + }; + let func2 = hugr + .insert_hugr(hugr.module_root(), hugr2) + .inserted_entrypoint; + hugr.validate().unwrap(); + + let checker1 = SchedGraphChecker::new(hugr.scheduling_graph(func_root)); + let checker2 = SchedGraphChecker::new(hugr.scheduling_graph(func2)); + let sub1 = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( + RootChecked::try_new(&hugr).expect("Root should be FuncDefn."), + ) + .unwrap(); + sub1.validate_with_checker(&hugr, Some(&checker1)).unwrap(); + let e = sub1.validate_with_checker(&hugr, Some(&checker2)); + assert_eq!( + e, + Err(InvalidSubgraph::MismatchedCheckerParent { + checker_parent: func2, + subgraph_parent: func_root + }) + ); + + SiblingSubgraph::try_new_with_checker( + sub1.inputs.clone(), + sub1.outputs.clone(), + &hugr, + &checker1, + ) + .unwrap(); + let e = SiblingSubgraph::try_new_with_checker( + sub1.inputs.clone(), + sub1.outputs.clone(), + &hugr, + &checker2, + ); + assert_eq!( + e, + Err(InvalidSubgraph::MismatchedCheckerParent { + checker_parent: func2, + subgraph_parent: func_root + }) + ); + + SiblingSubgraph::try_from_nodes_with_checker(sub1.nodes.clone(), &hugr, &checker1).unwrap(); + let e = SiblingSubgraph::try_from_nodes_with_checker(sub1.nodes.clone(), &hugr, &checker2); + assert_eq!( + e, + Err(InvalidSubgraph::MismatchedCheckerParent { + checker_parent: func2, + subgraph_parent: func_root + }) + ); +} + +#[test] +fn convex_subgraph_2() { + let (hugr, func_root) = build_hugr().unwrap(); + let [inp, out] = hugr.get_io(func_root).unwrap(); + let func = hugr.with_entrypoint(func_root); + // All graph except input/output nodes + SiblingSubgraph::try_new( + hugr.node_outputs(inp) + .take(2) + .map(|p| hugr.linked_inputs(inp, p).collect_vec()) + .filter(|ps| !ps.is_empty()) + .collect(), + hugr.node_inputs(out) + .take(2) + .filter_map(|p| hugr.single_linked_output(out, p)) + .collect(), + &func, + ) + .unwrap(); +} + +#[test] +fn degen_boundary() { + let (hugr, func_root) = build_hugr().unwrap(); + let func = hugr.with_entrypoint(func_root); + let [inp, _] = hugr.get_io(func_root).unwrap(); + let first_cx_edge = hugr.node_outputs(inp).next().unwrap(); + // All graph but one edge + assert_matches!( + SiblingSubgraph::try_new( + vec![ + hugr.linked_ports(inp, first_cx_edge) + .map(|(n, p)| (n, p.as_incoming().unwrap())) + .collect() + ], + vec![(inp, first_cx_edge)], + &func, + ), + Err(InvalidSubgraph::InvalidBoundary( + InvalidSubgraphBoundary::DisconnectedBoundaryPort(_, _) + )) + ); +} + +#[test] +fn non_convex_subgraph() { + let (hugr, func_root) = build_3not_hugr().unwrap(); + let func = hugr.with_entrypoint(func_root); + let [inp, _out] = hugr.get_io(func_root).unwrap(); + let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); + let not2 = hugr.output_neighbours(not1).exactly_one().ok().unwrap(); + let not3 = hugr.output_neighbours(not2).exactly_one().ok().unwrap(); + let not1_inp = hugr.node_inputs(not1).next().unwrap(); + let not1_out = hugr.node_outputs(not1).next().unwrap(); + let not3_inp = hugr.node_inputs(not3).next().unwrap(); + let not3_out = hugr.node_outputs(not3).next().unwrap(); + assert_matches!( + SiblingSubgraph::try_new( + vec![vec![(not1, not1_inp)], vec![(not3, not3_inp)]], + vec![(not1, not1_out), (not3, not3_out)], + &func + ), + Err(InvalidSubgraph::NotConvex) + ); +} + +/// A subgraphs mixed with multiports caused a `NonConvex` error. +/// +#[test] +fn convex_multiports() { + let (hugr, func_root) = build_multiport_hugr().unwrap(); + let [inp, out] = hugr.get_io(func_root).unwrap(); + let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); + let not2 = hugr + .output_neighbours(not1) + .filter(|&n| n != out) + .exactly_one() + .ok() + .unwrap(); + + let subgraph = SiblingSubgraph::try_from_nodes([not1, not2], &hugr).unwrap(); + assert_eq!(subgraph.nodes(), [not1, not2]); +} + +#[test] +fn invalid_boundary() { + let (hugr, func_root) = build_hugr().unwrap(); + let func = hugr.with_entrypoint(func_root); + let [inp, out] = hugr.get_io(func_root).unwrap(); + let cx_edges_in = hugr.node_outputs(inp); + let cx_edges_out = hugr.node_inputs(out); + // All graph but the CX + assert_matches!( + SiblingSubgraph::try_new( + cx_edges_out.map(|p| vec![(out, p)]).collect(), + cx_edges_in.map(|p| (inp, p)).collect(), + &func, + ), + Err(InvalidSubgraph::InvalidBoundary( + InvalidSubgraphBoundary::DisconnectedBoundaryPort(_, _) + )) + ); +} + +#[test] +fn preserve_signature() { + let (hugr, func_root) = build_hugr_classical().unwrap(); + let func_graph = hugr.with_entrypoint(func_root); + let func = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( + RootChecked::try_new(&func_graph).expect("Root should be FuncDefn."), + ) + .unwrap(); + let func_defn = hugr.get_optype(func_root).as_func_defn().unwrap(); + assert_eq!(func_defn.signature(), &func.signature(&func_graph).into()); +} + +#[test] +fn extract_subgraph() { + let (hugr, func_root) = build_hugr().unwrap(); + let func_graph = hugr.with_entrypoint(func_root); + let subgraph = SiblingSubgraph::try_new_dataflow_subgraph::<_, FuncID>( + RootChecked::try_new(&func_graph).expect("Root should be FuncDefn."), + ) + .unwrap(); + let extracted = subgraph.extract_subgraph(&hugr, "region"); + + extracted.validate().unwrap(); +} + +#[test] +fn edge_both_output_and_copy() { + // https://github.com/CQCL/hugr/issues/518 + let one_bit = vec![bool_t()]; + let two_bit = vec![bool_t(), bool_t()]; + + let mut builder = DFGBuilder::new(inout_sig(one_bit.clone(), two_bit.clone())).unwrap(); + let inw = builder.input_wires().exactly_one().unwrap(); + let outw1 = builder + .add_dataflow_op(LogicOp::Not, [inw]) + .unwrap() + .out_wire(0); + let outw2 = builder + .add_dataflow_op(and_op(), [inw, outw1]) + .unwrap() + .outputs(); + let outw = [outw1].into_iter().chain(outw2); + let h = builder.finish_hugr_with_outputs(outw).unwrap(); + let subg = SiblingSubgraph::try_new_dataflow_subgraph::<_, DfgID>( + RootChecked::try_new(&h).expect("Root should be DFG."), + ) + .unwrap(); + assert_eq!(subg.nodes().len(), 2); +} + +#[test] +fn test_unconnected() { + // test a replacement on a subgraph with a discarded output + let mut b = DFGBuilder::new(Signature::new([bool_t()], type_row![])).unwrap(); + let inw = b.input_wires().exactly_one().unwrap(); + let not_n = b.add_dataflow_op(LogicOp::Not, [inw]).unwrap(); + // Unconnected output, discarded + let mut h = b.finish_hugr_with_outputs([]).unwrap(); + + let subg = SiblingSubgraph::from_node(not_n.node(), &h); + + assert_eq!(subg.nodes().len(), 1); + // TODO create a valid replacement + let replacement = { + let mut rep_b = DFGBuilder::new(Signature::new_endo([bool_t()])).unwrap(); + let inw = rep_b.input_wires().exactly_one().unwrap(); + + let not_n = rep_b.add_dataflow_op(LogicOp::Not, [inw]).unwrap(); + + rep_b.finish_hugr_with_outputs(not_n.outputs()).unwrap() + }; + let rep = subg.create_simple_replacement(&h, replacement).unwrap(); + rep.apply(&mut h).unwrap(); +} + +/// Test the behaviour of the sibling subgraph when built from a single +/// node. +#[test] +fn single_node_subgraph() { + // A hugr with a single NOT operation, with disconnected output. + let mut b = DFGBuilder::new(Signature::new([bool_t()], type_row![])).unwrap(); + let inw = b.input_wires().exactly_one().unwrap(); + let not_n = b.add_dataflow_op(LogicOp::Not, [inw]).unwrap(); + // Unconnected output, discarded + let h = b.finish_hugr_with_outputs([]).unwrap(); + + // When built with `from_node`, the subgraph's signature is the same as the + // node's. (bool input, bool output) + let subg = SiblingSubgraph::from_node(not_n.node(), &h); + assert_eq!(subg.nodes().len(), 1); + assert_eq!( + subg.signature(&h).io(), + Signature::new(vec![bool_t()], vec![bool_t()]).io() + ); + + // `from_nodes` is different, is it only uses incoming and outgoing edges to + // compute the signature. In this case, the output is disconnected, so + // it is not part of the subgraph signature. + let subg = SiblingSubgraph::try_from_nodes([not_n.node()], &h).unwrap(); + assert_eq!(subg.nodes().len(), 1); + assert_eq!( + subg.signature(&h).io(), + Signature::new(vec![bool_t()], vec![]).io() + ); +} + +/// Test the behaviour of the sibling subgraph when built from a single +/// node with no inputs or outputs. +#[test] +fn singleton_disconnected_subgraph() { + // A hugr with some empty MakeTuple operations. + let op = MakeTuple::new(type_row![]); + + let mut b = DFGBuilder::new(Signature::new_endo(type_row![])).unwrap(); + let _mk_tuple_1 = b.add_dataflow_op(op.clone(), []).unwrap(); + let mk_tuple_2 = b.add_dataflow_op(op.clone(), []).unwrap(); + let _mk_tuple_3 = b.add_dataflow_op(op, []).unwrap(); + // Unconnected output, discarded + let h = b.finish_hugr_with_outputs([]).unwrap(); + + // When built with `try_from_nodes`, the subgraph's signature is the same as the + // node's. (empty input, tuple output) + let subg = SiblingSubgraph::from_node(mk_tuple_2.node(), &h); + assert_eq!(subg.nodes().len(), 1); + assert_eq!( + subg.signature(&h).io(), + Signature::new(type_row![], vec![Type::new_tuple(type_row![])]).io() + ); + + // `from_nodes` is different, is it only uses incoming and outgoing edges to + // compute the signature. In this case, the output is disconnected, so + // it is not part of the subgraph signature. + let subg = SiblingSubgraph::try_from_nodes([mk_tuple_2.node()], &h).unwrap(); + assert_eq!(subg.nodes().len(), 1); + assert_eq!( + subg.signature(&h).io(), + Signature::new_endo(type_row![]).io() + ); +} + +/// Run `try_from_nodes` including some complete graph components. +#[test] +fn partially_connected_subgraph() { + // A hugr with some empty MakeTuple operations. + let tuple_op = MakeTuple::new(type_row![]); + let untuple_op = UnpackTuple::new(type_row![]); + let tuple_t = Type::new_tuple(type_row![]); + + let mut b = DFGBuilder::new(Signature::new(type_row![], vec![tuple_t.clone()])).unwrap(); + let mk_tuple_1 = b.add_dataflow_op(tuple_op.clone(), []).unwrap(); + let untuple_1 = b + .add_dataflow_op(untuple_op.clone(), [mk_tuple_1.out_wire(0)]) + .unwrap(); + let mk_tuple_2 = b.add_dataflow_op(tuple_op.clone(), []).unwrap(); + let _mk_tuple_3 = b.add_dataflow_op(tuple_op, []).unwrap(); + // Output the 2nd tuple output + let h = b + .finish_hugr_with_outputs([mk_tuple_2.out_wire(0)]) + .unwrap(); + + let subgraph_nodes = [mk_tuple_1.node(), mk_tuple_2.node(), untuple_1.node()]; + + // `try_from_nodes` uses incoming and outgoing edges to compute the signature. + let subg = SiblingSubgraph::try_from_nodes(subgraph_nodes, &h).unwrap(); + assert_eq!(subg.nodes().len(), 3); + assert_eq!( + subg.signature(&h).io(), + Signature::new(type_row![], vec![tuple_t]).io() + ); +} + +#[test] +fn test_set_outgoing_ports() { + let (hugr, func_root) = build_3not_hugr().unwrap(); + let [inp, out] = hugr.get_io(func_root).unwrap(); + let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); + let not1_out = hugr.node_outputs(not1).next().unwrap(); + + // Create a subgraph with just the NOT gate + let mut subgraph = SiblingSubgraph::from_node(not1, &hugr); + + // Initially should have one output + assert_eq!(subgraph.outgoing_ports().len(), 1); + + // Try to set two outputs by copying the existing one + let new_outputs = vec![(not1, not1_out), (not1, not1_out)]; + assert!(subgraph.set_outgoing_ports(new_outputs, &hugr).is_ok()); + + // Should now have two outputs + assert_eq!(subgraph.outgoing_ports().len(), 2); + + // Try to set an invalid output (from a different node) + let invalid_outputs = vec![(not1, not1_out), (out, 2.into())]; + assert!(matches!( + subgraph.set_outgoing_ports(invalid_outputs, &hugr), + Err(InvalidOutputPorts::UnknownOutput { .. }) + )); + + // Should still have two outputs from before + assert_eq!(subgraph.outgoing_ports().len(), 2); +} + +#[test] +fn test_set_outgoing_ports_linear() { + let (hugr, func_root) = build_hugr().unwrap(); + let [inp, _out] = hugr.get_io(func_root).unwrap(); + let rz = hugr.output_neighbours(inp).nth(2).unwrap(); + let rz_out = hugr.node_outputs(rz).next().unwrap(); + + // Create a subgraph with just the CX gate + let mut subgraph = SiblingSubgraph::from_node(rz, &hugr); + + // Initially should have one output + assert_eq!(subgraph.outgoing_ports().len(), 1); + + // Try to set two outputs by copying the existing one (should fail for linear + // ports) + let new_outputs = vec![(rz, rz_out), (rz, rz_out)]; + assert!(matches!( + subgraph.set_outgoing_ports(new_outputs, &hugr), + Err(InvalidOutputPorts::NonUniqueLinear) + )); + + // Should still have one output + assert_eq!(subgraph.outgoing_ports().len(), 1); +} + +#[test] +fn test_try_from_nodes_with_intervals() { + let (hugr, func_root) = build_3not_hugr().unwrap(); + let line_checker = LineConvexChecker::new(&hugr, func_root); + let [inp, _out] = hugr.get_io(func_root).unwrap(); + let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); + let not2 = hugr.output_neighbours(not1).exactly_one().ok().unwrap(); + + let intervals = line_checker.get_intervals_from_nodes([not1, not2]).unwrap(); + let subgraph = + SiblingSubgraph::try_from_nodes_with_intervals([not1, not2], &intervals, &line_checker) + .unwrap(); + let exp_subgraph = SiblingSubgraph::try_from_nodes([not1, not2], &hugr).unwrap(); + + assert_eq!(subgraph, exp_subgraph); + assert_eq!( + line_checker.nodes_in_intervals(&intervals).collect_vec(), + [not1, not2] + ); + + let intervals2 = line_checker + .get_intervals_from_boundary_ports([ + (not1, IncomingPort::from(0).into()), + (not2, OutgoingPort::from(0).into()), + ]) + .unwrap(); + let subgraph2 = + SiblingSubgraph::try_from_nodes_with_intervals([not1, not2], &intervals2, &line_checker) + .unwrap(); + assert_eq!(subgraph2, exp_subgraph); +} + +#[test] +fn test_validate() { + let (hugr, func_root) = build_3not_hugr().unwrap(); + let func = hugr.with_entrypoint(func_root); + let checker = SchedGraphChecker::new(func.scheduling_graph(func_root)); + let [inp, _out] = hugr.get_io(func_root).unwrap(); + let not1 = hugr.output_neighbours(inp).exactly_one().ok().unwrap(); + let not2 = hugr.output_neighbours(not1).exactly_one().ok().unwrap(); + let not3 = hugr.output_neighbours(not2).exactly_one().ok().unwrap(); + + // A valid boundary, and convex + let sub = SiblingSubgraph::new_unchecked( + vec![vec![(not1, 0.into())]], + vec![(not2, 0.into())], + vec![], + vec![not1, not2], + ); + assert_eq!(sub.validate_skip_convexity(&func), Ok(())); + assert_eq!(sub.validate_skip_convexity(&func), Ok(())); + assert_eq!(sub.validate_with_checker(&func, Some(&checker)), Ok(())); + + // A valid boundary, but not convex + let sub = SiblingSubgraph::new_unchecked( + vec![vec![(not1, 0.into())], vec![(not3, 0.into())]], + vec![(not1, 0.into()), (not3, 0.into())], + vec![], + vec![not1, not3], + ); + assert_eq!(sub.validate_skip_convexity(&func), Ok(())); + assert_eq!(sub.validate_default(&func), Err(InvalidSubgraph::NotConvex)); + assert_eq!( + sub.validate_with_checker(&func, Some(&checker)), + Err(InvalidSubgraph::NotConvex) + ); + + // An invalid boundary (missing an input) + let sub = SiblingSubgraph::new_unchecked( + vec![vec![(not1, 0.into())]], + vec![(not1, 0.into()), (not3, 0.into())], + vec![], + vec![not1, not3], + ); + assert_eq!( + sub.validate_skip_convexity(&func), + Err(InvalidSubgraph::InvalidNodeSet) + ); +} + +#[fixture] +pub(crate) fn hugr_call_subgraph() -> Hugr { + let mut builder = ModuleBuilder::new(); + let decl_node = builder + .declare("test", endo_sig([bool_t()]).into()) + .unwrap(); + let mut main = builder + .define_function("main", endo_sig([bool_t()])) + .unwrap(); + let [bool] = main.input_wires_arr(); + + let [bool] = main + .add_dataflow_op(LogicOp::Not, [bool]) + .unwrap() + .outputs_arr(); + + // Chain two calls to the same function + let [bool] = main.call(&decl_node, &[], [bool]).unwrap().outputs_arr(); + let [bool] = main.call(&decl_node, &[], [bool]).unwrap().outputs_arr(); + + let main_def = main.finish_with_outputs([bool]).unwrap(); + + let mut hugr = builder.finish_hugr().unwrap(); + hugr.set_entrypoint(main_def.node()); + hugr +} + +#[rstest] +fn test_call_subgraph_from_dfg(hugr_call_subgraph: Hugr) { + let subg = SiblingSubgraph::try_new_dataflow_subgraph::<_, DataflowParentID>( + RootChecked::try_new(&hugr_call_subgraph).expect("Root should be DFG container."), + ) + .unwrap(); + + assert_eq!(subg.function_calls.len(), 1); + assert_eq!(subg.function_calls[0].len(), 2); +} + +#[rstest] +fn test_call_subgraph_from_nodes(hugr_call_subgraph: Hugr) { + let call_nodes = hugr_call_subgraph + .children(hugr_call_subgraph.entrypoint()) + .filter(|&n| hugr_call_subgraph.get_optype(n).is_call()) + .collect_vec(); + + let subg = SiblingSubgraph::try_from_nodes(call_nodes.clone(), &hugr_call_subgraph).unwrap(); + assert_eq!(subg.function_calls.len(), 1); + assert_eq!(subg.function_calls[0].len(), 2); + + let subg = + SiblingSubgraph::try_from_nodes(call_nodes[0..1].to_owned(), &hugr_call_subgraph).unwrap(); + assert_eq!(subg.function_calls.len(), 1); + assert_eq!(subg.function_calls[0].len(), 1); +} + +#[rstest] +fn test_call_subgraph_from_boundary(hugr_call_subgraph: Hugr) { + let call_nodes = hugr_call_subgraph + .children(hugr_call_subgraph.entrypoint()) + .filter(|&n| hugr_call_subgraph.get_optype(n).is_call()) + .collect_vec(); + let not_node = hugr_call_subgraph + .children(hugr_call_subgraph.entrypoint()) + .filter(|&n| hugr_call_subgraph.get_optype(n) == &LogicOp::Not.into()) + .exactly_one() + .ok() + .unwrap(); + + let subg = SiblingSubgraph::try_new( + vec![ + vec![(not_node, IncomingPort::from(0))], + call_nodes + .iter() + .map(|&n| (n, IncomingPort::from(1))) + .collect_vec(), + ], + vec![(call_nodes[1], OutgoingPort::from(0))], + &hugr_call_subgraph, + ) + .unwrap(); + + assert_eq!(subg.function_calls.len(), 1); + assert_eq!(subg.function_calls[0].len(), 2); +} + +#[test] +fn test_nonlocal_edge_excluding_target() { + let mut outer = DFGBuilder::new(endo_sig([bool_t()])).unwrap(); + let [inp] = outer.input_wires_arr(); + let not_op = outer.add_dataflow_op(LogicOp::Not, [inp]).unwrap(); + let mut dfb = outer.dfg_builder(inout_sig([], [bool_t()]), []).unwrap(); + let [nested_not] = dfb + .add_dataflow_op(LogicOp::Not, not_op.outputs()) + .unwrap() + .outputs_arr(); + let [dfg] = dfb.finish_with_outputs([nested_not]).unwrap().outputs_arr(); + let h = outer.finish_hugr_with_outputs([dfg]).unwrap(); + //eprintln!("{}", h.mermaid_string()); + + // Sanity check - simple SSG without the nonlocal edge + assert_eq!( + h.output_neighbours(not_op.node()).collect_vec(), + vec![nested_not.node(), dfg.node()] + ); + let outer_not_inputs = vec![vec![(not_op.node(), IncomingPort::from(0))]]; + let ss = SiblingSubgraph::try_new( + outer_not_inputs.clone(), + vec![(not_op.node(), 0.into())], + &h, + ) + .unwrap(); + // Nodes include the DFG (by following Order edge) and Output (edge from DFG) + assert_eq!( + ss.nodes(), + &[ + h.get_io(h.entrypoint()).unwrap()[1], + not_op.node(), + dfg.node() + ] + ); + ss.validate_default(&h).unwrap(); + + // We can't "not" follow the Order edge.... + let ss2 = SiblingSubgraph::try_new( + outer_not_inputs.clone(), + vec![ + (not_op.node(), 0.into()), + ( + not_op.node(), + h.get_optype(not_op.node()).other_output_port().unwrap(), + ), + ], + &h, + ); + assert_matches!(ss2, Err(InvalidSubgraph::UnsupportedEdgeKind(_, _))); + + // Now try to make an SSG with the outer Not and the DFG...this should not be possible ATM + // (it would contain an edge to the inner Not, thus contain the inner Not, thus is not a sibling subgraph). + // TODO in future we could consider allowing this i.e. discounting the nonlocal edge and its target + // as being part of the graph as it's "internal"? + let nested_output = vec![(dfg.node(), dfg.source())]; + let bad_ss = SiblingSubgraph::try_new(outer_not_inputs.clone(), nested_output.clone(), &h); + assert_eq!(bad_ss, Err(InvalidSubgraph::NotConvex)); + + let dfg_and_outer_not_outputs = vec![(not_op.node(), 0.into()), (dfg.node(), 0.into())]; + let ss1 = SiblingSubgraph::try_new( + outer_not_inputs.clone(), + dfg_and_outer_not_outputs.clone(), + &h, + ) + .unwrap(); + assert_eq!(ss1.nodes(), &[not_op.node(), dfg.node()]); + ss1.validate_default(&h).unwrap(); + + let ss2 = SiblingSubgraph::try_from_nodes([not_op.node(), dfg.node()], &h).unwrap(); + assert_eq!(ss2.incoming_ports(), &outer_not_inputs); + assert_eq!(ss2.outgoing_ports(), &dfg_and_outer_not_outputs); + ss2.validate_default(&h).unwrap(); + + let ss3 = SiblingSubgraph::new_unchecked( + outer_not_inputs, + nested_output, + vec![], + vec![not_op.node(), dfg.node()], + ); + assert_eq!(ss3.validate_default(&h), Err(InvalidSubgraph::NotConvex)); + // Without a convexity checker, and since we ignore the nonlocal edges themselves, + // all is "ok".... + ss3.validate_skip_convexity(&h).unwrap(); +} diff --git a/hugr-core/src/hugr/views/syn_edge.rs b/hugr-core/src/hugr/views/syn_edge.rs new file mode 100644 index 0000000000..f1bebe5883 --- /dev/null +++ b/hugr-core/src/hugr/views/syn_edge.rs @@ -0,0 +1,204 @@ +//! Contains a wrapper allowing to add extra edges to a Hugr Region view. +use petgraph::visit as pv; +use portgraph::{LinkView, NodeIndex as NIdx, PortOffset}; + +/// Wraps some property of an edge that may either be an original edge in the Hugr +/// or a synthetic edge - currently only those added for ordering constraints, +/// but this could be extended in the future. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MaybeSynEdge { + /// The original data for an original edge in the Hugr + Original(T), + /// Edge is synthetic and represents the ordering constraint that the source + /// node must be ordered before the target because of a nonlocal [EdgeKind::Value] + /// edge in the original Hugr between the source and a strict descendant of the target. + NonLocalOrderingConstraint, +} + +/// Wrapper for a [LinkView] that implements petgraph traits and adds some extra edges. +#[derive(Clone, Debug)] +pub(super) struct SynEdgeWrapper { + pub(super) region_view: T, + #[allow(clippy::type_complexity)] + pub(super) syn_edges: Vec<(NIdx, NIdx)>, +} + +impl pv::GraphBase for SynEdgeWrapper { + type NodeId = NIdx; + type EdgeId = ( + NIdx, + NIdx, + MaybeSynEdge<(PortOffset, PortOffset)>, + ); +} + +impl pv::GraphProp for SynEdgeWrapper { + type EdgeType = petgraph::Directed; +} + +impl pv::NodeCount for SynEdgeWrapper { + fn node_count(&self) -> usize { + self.region_view.node_count() + } +} + +impl pv::NodeIndexable for SynEdgeWrapper { + fn node_bound(&self) -> usize { + self.region_view.node_capacity() + } + + fn to_index(&self, ix: Self::NodeId) -> usize { + ix.index() + } + + fn from_index(&self, ix: usize) -> Self::NodeId { + NIdx::new(ix) + } +} + +impl pv::EdgeCount for SynEdgeWrapper { + fn edge_count(&self) -> usize { + self.region_view.link_count() + self.syn_edges.len() + } +} + +impl pv::Data for SynEdgeWrapper { + /// Turns out the underlying [FlatRegion] has unit node weights, we may want to fix that. + /// + /// [FlatRegion]: portgraph::view::FlatRegion + type NodeWeight = (); + + /// Turns out the underlying [FlatRegion] has unit edge weights; we may want to fix that, + /// but at least this distinguishes synthetic edges from original edges. + /// + /// [FlatRegion]: portgraph::view::FlatRegion + type EdgeWeight = MaybeSynEdge<(PortOffset, PortOffset)>; +} + +impl<'a, T: LinkView> pv::IntoNodeReferences for &'a SynEdgeWrapper { + type NodeRef = NIdx; + type NodeReferences = Box> + 'a>; + + fn node_references(self) -> Self::NodeReferences { + Box::new(self.region_view.nodes_iter()) + } +} + +impl<'a, T: LinkView> pv::IntoNodeIdentifiers for &'a SynEdgeWrapper { + type NodeIdentifiers = Box + 'a>; + + fn node_identifiers(self) -> Self::NodeIdentifiers { + pv::IntoNodeReferences::node_references(self) + } +} + +impl<'a, T: LinkView> pv::IntoNeighbors for &'a SynEdgeWrapper { + type Neighbors = Box> + 'a>; + + fn neighbors(self, n: Self::NodeId) -> Self::Neighbors { + Box::new( + self.region_view.output_neighbours(n).chain( + self.syn_edges + .iter() + .filter_map(move |(src, dst)| (*src == n).then_some(*dst)), + ), + ) + } +} + +/// An edge in the derived graph - either an original edge, or a synthetic edge. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct EdgeRef { + src: NId, + tgt: NId, + weight: MaybeSynEdge<(PId, PId)>, +} + +impl pv::EdgeRef for EdgeRef { + type NodeId = NId; + type EdgeId = (NId, NId, MaybeSynEdge<(PId, PId)>); + type Weight = MaybeSynEdge<(PId, PId)>; + + fn source(&self) -> Self::NodeId { + self.src + } + + fn target(&self) -> Self::NodeId { + self.tgt + } + + fn weight(&self) -> &Self::Weight { + &self.weight + } + + fn id(&self) -> Self::EdgeId { + (self.src, self.tgt, self.weight) + } +} + +impl<'a, T: LinkView> pv::IntoEdgeReferences for &'a SynEdgeWrapper { + type EdgeRef = EdgeRef, PortOffset>; + type EdgeReferences = Box + 'a>; + + fn edge_references(self) -> Self::EdgeReferences { + Box::new( + self.region_view + .nodes_iter() + .flat_map(|n| { + self.region_view + .links(n, portgraph::Direction::Outgoing) + .map(|(src, tgt)| { + let er: Self::EdgeRef = EdgeRef { + src: self.region_view.port_node(src).unwrap(), + tgt: self.region_view.port_node(tgt).unwrap(), + weight: MaybeSynEdge::Original(( + self.region_view.port_offset(src).unwrap(), + self.region_view.port_offset(tgt).unwrap(), + )), + }; + er + }) + }) + .chain(self.syn_edges.iter().map(|(src, tgt)| EdgeRef { + src: *src, + tgt: *tgt, + weight: MaybeSynEdge::NonLocalOrderingConstraint, + })), + ) + } +} + +impl<'a, T: LinkView> pv::IntoNeighborsDirected for &'a SynEdgeWrapper { + type NeighborsDirected = Box> + 'a>; + + fn neighbors_directed( + self, + n: Self::NodeId, + d: petgraph::Direction, + ) -> Self::NeighborsDirected { + Box::new( + self.region_view + .neighbours(n, d.into()) + .chain(self.syn_edges.iter().filter_map(move |(src, dst)| match d { + petgraph::Direction::Outgoing if *src == n => Some(*dst), + petgraph::Direction::Incoming if *dst == n => Some(*src), + _ => None, + })), + ) + } +} + +impl>>> pv::Visitable + for SynEdgeWrapper +{ + type Map = T::Map; + + fn visit_map(&self) -> Self::Map { + self.region_view.visit_map() + } + + fn reset_map(&self, map: &mut Self::Map) { + self.region_view.reset_map(map); + } +} diff --git a/hugr-core/src/hugr/views/tests.rs b/hugr-core/src/hugr/views/tests.rs index 056cdfa561..1df56377e8 100644 --- a/hugr-core/src/hugr/views/tests.rs +++ b/hugr-core/src/hugr/views/tests.rs @@ -1,22 +1,17 @@ +use itertools::Itertools; +use petgraph::visit::IntoNeighbors as _; use portgraph::PortOffset; use rstest::{fixture, rstest}; -use crate::{ - Hugr, HugrView, - builder::{ - BuildError, BuildHandle, Container, DFGBuilder, Dataflow, DataflowHugr, HugrBuilder, - endo_sig, inout_sig, - }, - extension::prelude::qb_t, - ops::{ - Value, - handle::{DataflowOpID, NodeHandle}, - }, - std_extensions::logic::LogicOp, - type_row, - types::Signature, - utils::test_quantum_extension::cx_gate, +use crate::builder::{ + BuildError, BuildHandle, Container, DFGBuilder, Dataflow, DataflowHugr, DataflowSubContainer, + HugrBuilder, endo_sig, inout_sig, }; +use crate::extension::prelude::{bool_t, qb_t}; +use crate::ops::Value; +use crate::ops::handle::{DataflowOpID, NodeHandle}; +use crate::std_extensions::logic::LogicOp; +use crate::{Hugr, HugrView, type_row, types::Signature, utils::test_quantum_extension::cx_gate}; /// A Dataflow graph from two qubits to two qubits that applies two CX operations on them. /// @@ -215,3 +210,28 @@ fn test_dataflow_ports_only() { ] ); } + +#[test] +fn test_syn_edge() { + let mut outer = DFGBuilder::new(endo_sig([bool_t()])).unwrap(); + let [inp] = outer.input_wires_arr(); + let mut sub_dfg = outer.dfg_builder(inout_sig([], [bool_t()]), []).unwrap(); + let [not] = sub_dfg + .add_dataflow_op(LogicOp::Not, [inp]) + .unwrap() + .outputs_arr(); + let [sub_dfg] = sub_dfg.finish_with_outputs([not]).unwrap().outputs_arr(); + let h = outer.finish_hugr_with_outputs([sub_dfg]).unwrap(); + + assert_eq!( + h.output_neighbours(inp.node()).collect_vec(), + [not.node(), sub_dfg.node()] + ); + + let sg = h.scheduling_graph(h.entrypoint()); + assert!( + sg.petgraph() + .neighbors(sg.node_to_pg(inp.node())) + .contains(&sg.node_to_pg(sub_dfg.node())) + ); +} diff --git a/hugr-llvm/src/emit/ops.rs b/hugr-llvm/src/emit/ops.rs index 435ab6ff92..3a53acb97f 100644 --- a/hugr-llvm/src/emit/ops.rs +++ b/hugr-llvm/src/emit/ops.rs @@ -1,6 +1,5 @@ use anyhow::{Context, Result, anyhow}; use hugr_core::Node; -use hugr_core::hugr::internal::PortgraphNodeMap; use hugr_core::ops::{ CFG, Call, CallIndirect, Case, Conditional, Const, ExtensionOp, Input, LoadConstant, LoadFunction, OpTag, OpTrait, OpType, Output, Tag, TailLoop, Value, constant::Sum, @@ -70,10 +69,10 @@ where debug_assert!(i.out_value_types().count() == self.inputs.as_ref().unwrap().len()); debug_assert!(o.in_value_types().count() == self.outputs.as_ref().unwrap().len()); - let (region_graph, node_map) = node.hugr().region_portgraph(node.node()); - let topo = Topo::new(®ion_graph); - for n in topo.iter(®ion_graph) { - let node = node.hugr().fat_optype(node_map.from_portgraph(n)); + let sg = node.hugr().scheduling_graph(node.node()); + let topo = Topo::new(sg.petgraph()); + for n in topo.iter(sg.petgraph()) { + let node = node.hugr().fat_optype(sg.pg_to_node(n)); let inputs_rmb = context.node_ins_rmb(node)?; let inputs = inputs_rmb.read(context.builder(), [])?; let outputs = context.node_outs_rmb(node)?.promise(); diff --git a/hugr-persistent/src/trait_impls.rs b/hugr-persistent/src/trait_impls.rs index a82b744900..7593c26f26 100644 --- a/hugr-persistent/src/trait_impls.rs +++ b/hugr-persistent/src/trait_impls.rs @@ -65,7 +65,7 @@ impl HugrInternals for PersistentHugr { // TODO: this is currently not very efficient (see #2248) let (hugr, node_map) = self.apply_all(); let parent = node_map[&parent]; - + #[expect(deprecated)] // Remove region_portgraph at same time (hugr.into_region_portgraph(parent), node_map) } @@ -424,7 +424,7 @@ mod tests { use super::super::tests::test_state_space; use super::*; - use portgraph::PortView; + use petgraph::visit::NodeCount; use rstest::rstest; #[rstest] @@ -570,9 +570,12 @@ mod tests { assert_eq!(hugr.num_nodes(), extracted_hugr.num_nodes()); assert_eq!(hugr.num_edges(), extracted_hugr.num_edges()); - let (pg, _) = hugr.region_portgraph(hugr.entrypoint()); + let sg = hugr.scheduling_graph(hugr.entrypoint()); - assert_eq!(pg.node_count(), hugr.children(hugr.entrypoint()).count()); + assert_eq!( + sg.petgraph().node_count(), + hugr.children(hugr.entrypoint()).count() + ); let (new_hugr, _) = hugr.extract_hugr(hugr.entrypoint());