diff --git a/Cargo.lock b/Cargo.lock index 801025ab8..34ad53805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,9 +52,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -82,22 +82,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -163,7 +163,7 @@ dependencies = [ "petgraph 0.6.5", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -215,14 +215,14 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" [[package]] name = "bitvec" @@ -431,7 +431,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -651,7 +651,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -662,7 +662,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -698,7 +698,7 @@ checksum = "6178a82cf56c836a3ba61a7935cdb1c49bfaa6fa4327cd5bf554a503087de26b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -719,18 +719,18 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "derive-where" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510c292c8cf384b1a340b816a9a6cf2599eb8f566a44949024af88418000c50b" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -743,7 +743,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -763,7 +763,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid", ] @@ -791,9 +791,9 @@ checksum = "97af9b5f014e228b33e77d75ee0e6e87960124f0f4b16337b586a6bec91867b1" [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" @@ -822,7 +822,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -901,7 +901,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -963,9 +963,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "half" @@ -996,9 +996,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -1214,7 +1214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "serde", ] @@ -1246,7 +1246,7 @@ checksum = "f365c8de536236cfdebd0ba2130de22acefed18b1fb99c32783b3840aec5fb46" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1367,7 +1367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -1446,12 +1446,42 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1562,7 +1592,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1592,7 +1622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" dependencies = [ "fixedbitset 0.5.7", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "indexmap 2.10.0", "serde", ] @@ -1715,12 +1745,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1745,9 +1775,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -1798,7 +1828,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1811,7 +1841,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1886,9 +1916,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] @@ -1910,7 +1940,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2011,7 +2041,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", "unicode-ident", ] @@ -2051,9 +2081,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2126,7 +2156,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2170,7 +2200,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2253,15 +2283,14 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2277,9 +2306,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -2337,7 +2366,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2348,7 +2377,7 @@ checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2439,8 +2468,10 @@ dependencies = [ "hugr", "hugr-core", "indexmap 2.10.0", + "insta", "itertools 0.14.0", "lazy_static", + "num-rational", "pest", "pest_derive", "petgraph 0.8.2", @@ -2458,6 +2489,7 @@ dependencies = [ "tket-json-rs", "tracing", "typetag", + "uuid", "zstd", ] @@ -2584,7 +2616,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2661,7 +2693,7 @@ checksum = "35f5380909ffc31b4de4f4bdf96b877175a016aa2ca98cee39fcfd8c4d53d952" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2708,9 +2740,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom", "js-sys", @@ -2771,7 +2803,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -2793,7 +2825,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2869,7 +2901,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2880,7 +2912,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2937,7 +2969,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -2958,10 +2990,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3154,7 +3187,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 252d7b953..b0c0f3b9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,8 @@ typetag = "0.2.20" cool_asserts = "2.0.3" zstd = "0.13.3" anyhow = "1.0.99" +num-rational = "0.4" +uuid = { version = "1.0", features = ["v4"] } [profile.release.package.tket-py] # Some configurations to reduce the size of tket wheels diff --git a/tket/Cargo.toml b/tket/Cargo.toml index b34ab7e0e..752d418f1 100644 --- a/tket/Cargo.toml +++ b/tket/Cargo.toml @@ -76,6 +76,8 @@ pest = { workspace = true } pest_derive = { workspace = true } zstd = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } +num-rational = { workspace = true } +uuid = { workspace = true } [dev-dependencies] @@ -86,6 +88,7 @@ cool_asserts = { workspace = true } # Defined here so it can be overridden by the codspeed CI job # using `cargo add`. criterion = { version = "0.7.0", features = ["html_reports"] } +insta = "1.43.1" [[bench]] name = "bench_main" diff --git a/tket/src/circuit.rs b/tket/src/circuit.rs index 9f919e9fc..f1f58cf52 100644 --- a/tket/src/circuit.rs +++ b/tket/src/circuit.rs @@ -14,7 +14,8 @@ pub use command::{Command, CommandIterator}; pub use hash::CircuitHash; use hugr::extension::prelude::{NoopDef, TupleOpDef}; use hugr::extension::simple_op::MakeOpDef; -use hugr::hugr::views::ExtractionResult; +use hugr::hugr::views::{ExtractionResult, SiblingSubgraph}; +use hugr::ops::handle::DataflowParentID; use itertools::Either::{Left, Right}; use derive_more::{Display, Error, From}; @@ -344,6 +345,14 @@ impl> Circuit { Ok(circ) } + /// The subgraph containing the entire circuit. + pub fn subgraph(&self) -> SiblingSubgraph + where + T: Clone, + { + SiblingSubgraph::try_new_dataflow_subgraph::<_, DataflowParentID>(self.hugr()).unwrap() + } + /// Compute the cost of the circuit based on a per-operation cost function. #[inline] pub fn circuit_cost(&self, op_cost: F) -> C diff --git a/tket/src/lib.rs b/tket/src/lib.rs index 3ca9a7b46..4a83124c0 100644 --- a/tket/src/lib.rs +++ b/tket/src/lib.rs @@ -49,6 +49,7 @@ pub mod extension; pub(crate) mod ops; pub mod optimiser; pub mod passes; +pub mod resource; pub mod rewrite; pub mod serialize; diff --git a/tket/src/resource.rs b/tket/src/resource.rs new file mode 100644 index 000000000..9686c5574 --- /dev/null +++ b/tket/src/resource.rs @@ -0,0 +1,160 @@ +//! Resource tracking within HUGR DFG subgraphs. +//! +//! This module implements the resource tracking system. It provides +//! facilities for tracking linear resources (such as qubits) through quantum +//! circuits represented as HUGR subgraphs. +//! +//! # Overview +//! +//! HUGR has a notion of "Value": the data that corresponds to a wire within a +//! dataflow graph. It further has a notion of "linear value" a.k.a non-copyable +//! value: a value that cannot be copied or discarded (implicitly). +//! +//! As far as HUGR is concerned, a linear value (or any value, for that matter) +//! is born at an op's output and dies at the next op's input. TKET introduces +//! the notion of "Resource" to extend the lifetime of a linear value over +//! multiple ops. +//! +//! If a linear value appears both in an op's input and output, we say that it +//! is "resource-preserving". Using [`ResourceFlow`], we can track resources +//! as they "flow" through multiple operations. The chains of +//! resource-preserving ops acting on a same resource form a so-called resource +//! path. +//! +//! # Resources and Copyable Values +//! +//! Resource tracking distinguishes between two types of values: +//! +//! - **Linear resources**: Non-copyable values that form resource paths through +//! the circuit. Each resource has a unique [`ResourceId`] and operations on +//! the same resource are ordered by a [`Position`]. +//! - **Copyable values**: Regular values that can be copied and discarded +//! freely. Each is identified by a unique [`CopyableValueId`]. +//! +//! # Resource Scope +//! +//! Tracking resources is not free: there is a one-off linear cost to compute +//! the resource paths, plus a linear memory cost to store them. +//! +//! Use a [`SiblingSubgraph`] to define a region of a `HUGR`, within which +//! resources should be tracked. You can then construct a resource-tracked scope +//! using [`ResourceScope::new`]. + +// Public API exports +pub use flow::{DefaultResourceFlow, ResourceFlow, UnsupportedOp}; +pub use scope::{ResourceScope, ResourceScopeConfig}; +pub use types::{CopyableValueId, OpValue, Position, ResourceAllocator, ResourceId}; + +// Internal modules +mod flow; +mod scope; +mod types; + +#[cfg(test)] +mod tests { + use hugr::{ + builder::{DFGBuilder, Dataflow, DataflowHugr}, + extension::prelude::qb_t, + hugr::views::SiblingSubgraph, + ops::handle::DataflowParentID, + types::Signature, + CircuitUnit, Hugr, + }; + + use itertools::Itertools; + use rstest::rstest; + + use crate::{ + extension::rotation::{rotation_type, ConstRotation}, + resource::scope::tests::ResourceScopeReport, + TketOp, + }; + + use super::ResourceScope; + + // Gate being commuted has a non-linear input + fn circ(n_qubits: usize, add_rz: bool, add_const_rz: bool) -> Hugr { + let build = || { + let out_qb_row = vec![qb_t(); n_qubits]; + let mut inp_qb_row = out_qb_row.clone(); + if add_rz { + inp_qb_row.push(rotation_type()); + }; + let mut dfg = DFGBuilder::new(Signature::new(inp_qb_row, out_qb_row))?; + + let (qubits, f) = if add_rz { + let mut inputs = dfg.input_wires().collect_vec(); + let f = inputs.pop().unwrap(); + (inputs, Some(f)) + } else { + (dfg.input_wires().collect_vec(), None) + }; + + let mut circ = dfg.as_circuit(qubits); + + for i in 0..n_qubits { + circ.append(TketOp::H, [i])?; + } + for i in (0..n_qubits).step_by(2) { + if i + 1 < n_qubits { + circ.append(TketOp::CX, [i, i + 1])?; + } + } + if let Some(f) = f { + for i in 0..n_qubits { + circ.append_and_consume( + TketOp::Rz, + [CircuitUnit::Linear(i), CircuitUnit::Wire(f)], + )?; + } + } + if add_const_rz { + let const_angle = circ.add_constant(ConstRotation::PI_2); + for i in 0..n_qubits { + circ.append_and_consume( + TketOp::Rz, + [CircuitUnit::Linear(i), CircuitUnit::Wire(const_angle)], + )?; + } + } + let qbs = circ.finish(); + dfg.finish_hugr_with_outputs(qbs) + }; + build().unwrap() + } + + #[rstest] + #[case(2, false, false)] + #[case(2, true, false)] + #[case(2, false, true)] + #[case(2, true, true)] + #[case(4, false, false)] + #[case(4, true, false)] + #[case(4, false, true)] + #[case(4, true, true)] + fn test_resource_scope_creation( + #[case] n_qubits: usize, + #[case] add_rz: bool, + #[case] add_const_rz: bool, + ) { + let circ = circ(n_qubits, add_rz, add_const_rz); + let subgraph = + SiblingSubgraph::try_new_dataflow_subgraph::<_, DataflowParentID>(&circ).unwrap(); + let scope = ResourceScope::new(&circ, subgraph); + let info = ResourceScopeReport::from(&scope); + + let mut name = format!("{n_qubits}_qubits"); + if add_rz { + name.push('_'); + name.push_str("add_rz"); + } + if add_const_rz { + name.push('_'); + name.push_str("add_const_rz"); + } + + assert_eq!(info.resource_paths.len(), n_qubits); + assert_eq!(info.n_copyable, add_const_rz as usize + add_rz as usize); + insta::assert_snapshot!(name, info); + } +} diff --git a/tket/src/resource/flow.rs b/tket/src/resource/flow.rs new file mode 100644 index 000000000..14696ef9c --- /dev/null +++ b/tket/src/resource/flow.rs @@ -0,0 +1,136 @@ +//! Resource flow logic for tracking how resources move through operations. +//! +//! This module defines the [`ResourceFlow`] trait that specifies how resources +//! flow through operations, along with a default implementation. + +use crate::resource::types::ResourceId; +use derive_more::derive::{Display, Error}; +use hugr::ops::{OpTrait, OpType}; +use hugr::types::Type; +use itertools::{EitherOrBoth, Itertools}; + +/// Error type for unsupported operations in ResourceFlow implementations. +#[derive(Debug, Display, Clone, PartialEq, Error)] +#[display("Unsupported operation '{op_type:?}'")] +pub struct UnsupportedOp { + /// The operation type that is unsupported. + pub op_type: OpType, +} + +/// Trait for specifying how resources flow through operations. +/// +/// This trait allows different implementations to define how linear resources +/// are mapped from inputs to outputs through various operation types. +pub trait ResourceFlow { + /// Map resource IDs from operation inputs to outputs. + /// + /// Takes an operation type and the resource IDs of the operation's inputs. + /// The i-th entry is Some(resource_id) if the i-th port is a linear type, + /// None otherwise. Returns the resource IDs of the operation's outputs + /// in port order. Output resource IDs should be one of the input resource + /// IDs for resource-preserving operations, or None for new resources or + /// non-linear types. + /// + /// # Arguments + /// * `op` - The operation type + /// * `inputs` - Resource IDs for each input port (None for non-linear + /// types) + /// + /// # Returns + /// Resource IDs for each output port, or UnsupportedOp if the operation + /// cannot be handled by this implementation. + fn map_resources( + &self, + op: &OpType, + inputs: &[Option], + ) -> Result>, UnsupportedOp>; +} + +/// Default implementation of ResourceFlow. +/// +/// This implementation considers that an operation is resource-preserving if +/// whenever the i-th input or output is linear, then the i-th input type +/// matches the i-th output. The i-th input is then mapped to the i-th output. +/// +/// Otherwise, all input resources are discarded and all outputs will be given +/// fresh resource IDs. +#[derive(Debug, Clone, Default)] +pub struct DefaultResourceFlow; + +impl DefaultResourceFlow { + /// Create a new DefaultResourceFlow instance. + pub fn new() -> Self { + Self + } + + /// Check if a type is linear (non-copyable). + fn is_linear_type(ty: &Type) -> bool { + !ty.copyable() + } + + /// Determine if an operation is resource-preserving based on input/output + /// types. + fn is_resource_preserving(input_types: &[Type], output_types: &[Type]) -> bool { + // An operation is resource-preserving if for each i, if input[i] or + // output[i] is linear, then type(input[i]) == type(output[i]) + + for io_ty in input_types.iter().zip_longest(output_types.iter()) { + let (input_ty, output_ty) = match io_ty { + EitherOrBoth::Both(input_ty, output_ty) => (input_ty, output_ty), + EitherOrBoth::Left(ty) | EitherOrBoth::Right(ty) => { + if Self::is_linear_type(ty) { + // linear type on one side, nothing on the other + return false; + } + continue; + } + }; + + if Self::is_linear_type(input_ty) || Self::is_linear_type(output_ty) { + // If input/output is linear, both must be the same type + if input_ty != output_ty { + return false; + } + } + } + + true + } +} + +impl ResourceFlow for DefaultResourceFlow { + fn map_resources( + &self, + op: &OpType, + inputs: &[Option], + ) -> Result>, UnsupportedOp> { + let signature = op.dataflow_signature().expect("dataflow op"); + let input_types = signature.input_types(); + let output_types = signature.output_types(); + + debug_assert_eq!( + inputs.len(), + input_types.len(), + "Input resource array length must match operation input count" + ); + + if Self::is_resource_preserving(input_types, output_types) { + Ok(retain_linear_types(inputs.to_vec(), output_types)) + } else { + // Resource-producing/consuming: all linear outputs are new resources (None) + Ok(vec![None; output_types.len()]) + } + } +} + +fn retain_linear_types( + mut resources: Vec>, + types: &[Type], +) -> Vec> { + for (ty, resource) in types.iter().zip(resources.iter_mut()) { + if ty.copyable() { + *resource = None; + } + } + resources +} diff --git a/tket/src/resource/interval.rs b/tket/src/resource/interval.rs new file mode 100644 index 000000000..1fa57af40 --- /dev/null +++ b/tket/src/resource/interval.rs @@ -0,0 +1,294 @@ +//! Subgraph representation using intervals on resource paths. + +use std::cmp::Ordering; + +use derive_more::derive::{Display, Error}; +use hugr::{core::HugrNode, Direction, HugrView}; +use itertools::Itertools; + +use super::{Position, ResourceId, ResourceScope}; + +/// A non-empty interval on a resource path. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Interval { + /// Resource ID of the resource path. + resource_id: ResourceId, + /// Start and end positions of the interval (inclusive). + positions: [Position; 2], + /// Start and end nodes of the interval (inclusive). + nodes: [N; 2], +} + +impl Interval { + /// Create an interval for a single node. + pub fn singleton( + resource_id: ResourceId, + node: N, + scope: &ResourceScope>, + ) -> Self { + let pos = scope + .get_position(node) + .expect("node is not on resource path"); + + Self { + resource_id, + positions: [pos, pos], + nodes: [node, node], + } + } + + /// Create an interval for a range of nodes. + pub fn new( + resource_id: ResourceId, + start_node: N, + end_node: N, + scope: &ResourceScope>, + ) -> Self { + let start_pos = scope + .get_position(start_node) + .expect("start node not on resource path"); + let end_pos = scope + .get_position(end_node) + .expect("end node not on resource path"); + + assert!(start_pos <= end_pos); + + Self { + resource_id, + positions: [start_pos, end_pos], + nodes: [start_node, end_node], + } + } + + /// Get the resource ID of the interval. + pub fn resource_id(&self) -> ResourceId { + self.resource_id + } + + /// Get the start node of the interval. + pub fn start_node(&self) -> N { + self.nodes[0] + } + + /// Get the end node of the interval. + pub fn end_node(&self) -> N { + self.nodes[1] + } + + /// Extend the interval to include the given node. + /// + /// Return the direction the interval was extended in, that is: + /// - if `node` was just before the interval, return `Direction::Incoming` + /// - if `node` was just after the interval, return `Direction::Outgoing` + /// - if `node` was already in the interval, return `None` + /// + /// If `node` is not contiguous with the interval, or if `node` is not + /// on the interval's resource path, return an error. + pub fn try_extend( + &mut self, + node: N, + scope: &ResourceScope>, + ) -> Result, InvalidInterval> { + let Some(pos) = scope.get_position(node) else { + return Err(InvalidInterval::NotOnResourcePath(node)); + }; + + match self.position_in_interval(pos) { + // pos is already within the interval + Ordering::Equal => Ok(None), + // pos is before the interval + Ordering::Less => { + let next_node = scope + .resource_path_iter(self.resource_id, node, Direction::Outgoing) + .nth(1) + .expect("same resource ID with larger position exists"); + if next_node == self.nodes[0] { + // Success! extend the interval to the left by one node + self.positions[0] = pos; + self.nodes[0] = node; + Ok(Some(Direction::Incoming)) + } else { + Err(InvalidInterval::NotContiguous(node)) + } + } + // pos is after the interval + Ordering::Greater => { + let prev_node = scope + .resource_path_iter(self.resource_id, node, Direction::Incoming) + .nth(1) + .expect("same resource ID with smaller position exists"); + if prev_node == self.nodes[1] { + // Success! extend the interval to the right by one node + self.positions[1] = pos; + self.nodes[1] = node; + Ok(Some(Direction::Outgoing)) + } else { + Err(InvalidInterval::NotContiguous(node)) + } + } + } + } + + /// Include the given node in the interval. + /// + /// Does not check if the node is contiguous with the interval. + pub(crate) fn include_node(&mut self, node: N, pos: Position) { + if pos < self.positions[0] { + self.positions[0] = pos; + self.nodes[0] = node; + } + if pos > self.positions[1] { + self.positions[1] = pos; + self.nodes[1] = node; + } + } + + /// Whether `pos` is smaller, larger or within the interval. + fn position_in_interval(&self, pos: Position) -> Ordering { + if pos < self.positions[0] { + Ordering::Less + } else if pos > self.positions[1] { + Ordering::Greater + } else { + Ordering::Equal + } + } +} + +/// Errors that can occur when extending an interval. +#[derive(Debug, Clone, PartialEq, Display, Error)] +pub enum InvalidInterval { + /// The node is not contiguous with the interval. + #[display("node {_0:?} is not contiguous with the interval")] + NotContiguous(N), + /// The node is not on the interval's resource path. + #[display("node {_0:?} is not on the interval's resource path")] + NotOnResourcePath(N), +} + +impl ResourceScope { + /// Get the nodes in an interval. + pub fn nodes_in_interval( + &self, + interval: Interval, + ) -> impl Iterator + '_ { + let [start_node, end_node] = interval.nodes; + self.resource_path_iter(interval.resource_id, start_node, Direction::Outgoing) + .take_while_inclusive(move |&node| node != end_node) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ops::RangeInclusive; + + use crate::{ + resource::tests::cx_circuit, + resource::{Interval, Position, ResourceId}, + Circuit, + }; + + use itertools::Itertools; + use rstest::rstest; + + #[test] + fn test_nodes_in_interval() { + let circ = cx_circuit(5); + let subgraph = Circuit::from(&circ).subgraph(); + let cx_nodes = subgraph.nodes().to_owned(); + let scope = super::ResourceScope::new(&circ, subgraph); + + assert_eq!(cx_nodes.len(), 5); + + let pos_interval = 1usize..4; + + for resource_id in [0, 1].map(ResourceId::new) { + let interval = Interval { + resource_id, + positions: [ + Position::new_integer(pos_interval.start as i64), + Position::new_integer((pos_interval.end - 1) as i64), + ], + nodes: [cx_nodes[pos_interval.start], cx_nodes[pos_interval.end - 1]], + }; + + assert_eq!( + Interval::new( + resource_id, + cx_nodes[pos_interval.start], + cx_nodes[pos_interval.end - 1], + &scope + ), + interval + ); + + assert_eq!( + scope.nodes_in_interval(interval).collect_vec(), + cx_nodes[pos_interval.clone()] + ); + } + } + + #[rstest] + #[case::extend_left_success( + 1, + Ok(Some(Direction::Incoming)), + 1..=3, + )] + #[case::extend_right_success( + 4, + Ok(Some(Direction::Outgoing)), + 2..=4, + )] + #[case::node_already_in_interval_start( + 2, + Ok(None), + 2..=3, + )] + #[case::node_already_in_interval_end( + 3, + Ok(None), + 2..=3, + )] + #[case::extend_to_non_contiguous_node( + 0, + Err(InvalidInterval::NotContiguous(0)), + 2..=3, + )] + fn test_try_extend( + #[case] node_to_extend: usize, + #[case] expected_result: Result, InvalidInterval>, + #[case] expected_range: RangeInclusive, + ) { + let circ = cx_circuit(5); + let subgraph = Circuit::from(&circ).subgraph(); + let cx_nodes = subgraph.nodes().to_owned(); + let scope = super::ResourceScope::new(&circ, subgraph); + + let resource_id = ResourceId::new(0); + + // Create an interval from nodes 2 to 3 (middle of circuit) + let mut interval = Interval::new(resource_id, cx_nodes[2], cx_nodes[3], &scope); + + // Apply the test case + let result = interval.try_extend(cx_nodes[node_to_extend], &scope); + + // Check the result + match expected_result { + Ok(expected_direction) => { + assert_eq!(result.unwrap(), expected_direction); + } + Err(InvalidInterval::NotContiguous(node)) => { + assert_eq!( + result.unwrap_err(), + InvalidInterval::NotContiguous(cx_nodes[node]) + ); + } + Err(_) => unimplemented!(), + } + + assert_eq!(interval.start_node(), cx_nodes[*expected_range.start()]); + assert_eq!(interval.end_node(), cx_nodes[*expected_range.end()]); + } +} diff --git a/tket/src/resource/scope.rs b/tket/src/resource/scope.rs new file mode 100644 index 000000000..c6bd86d52 --- /dev/null +++ b/tket/src/resource/scope.rs @@ -0,0 +1,596 @@ +//! Main ResourceScope implementation for tracking resources in HUGR subgraphs. +//! +//! This module provides the ResourceScope struct which manages resource +//! tracking within a specific region of a HUGR, computing resource paths and +//! providing efficient lookup of port values. + +use std::{cmp, iter, mem}; + +use crate::resource::flow::{DefaultResourceFlow, ResourceFlow}; +use crate::resource::types::{CopyableValueId, OpValue, PortMap}; +use crate::utils::type_is_linear; +use crate::Circuit; +use hugr::hugr::views::SiblingSubgraph; +use hugr::ops::OpTrait; +use hugr::types::Signature; +use hugr::{Direction, HugrView, IncomingPort, Port, PortIndex}; +use hugr_core::hugr::internal::PortgraphNodeMap; +use indexmap::IndexMap; +use itertools::Itertools; +use portgraph::algorithms::{toposort, TopoSort}; +use portgraph::view::{FilteredGraph, NodeFilter, NodeFiltered}; + +use super::{Position, ResourceAllocator, ResourceId}; + +/// ResourceScope tracks resources within a HUGR subgraph. +/// +/// This struct computes and caches resource paths through a given subgraph, +/// allowing efficient lookup of OpValues for any port of any operation within +/// the scope. +#[derive(Debug, Clone)] +pub struct ResourceScope { + /// The HUGR containing the operations. + hugr: H, + /// The subgraph within which resources are tracked. + subgraph: SiblingSubgraph, + /// Mapping from nodes and ports to their OpValues. + op_values: IndexMap, +} + +#[derive(Debug, Clone)] +struct NodeOpValues { + /// Mapping from ports to their OpValues. + port_map: PortMap, + /// The position of the node. + position: Position, +} + +impl NodeOpValues { + fn with_default(default: OpValue, signature: &Signature) -> Self { + Self { + port_map: PortMap::with_default(default, signature), + position: Position::default(), + } + } +} + +/// Configuration for a ResourceScope. +pub struct ResourceScopeConfig { + flows: Vec>, +} + +impl Default for ResourceScopeConfig { + fn default() -> Self { + Self { + flows: vec![Box::new(DefaultResourceFlow::new())], + } + } +} + +impl ResourceScope { + /// Create a new ResourceScope from a SiblingSubgraph using the default + /// resource flow. + pub fn new(hugr: H, subgraph: SiblingSubgraph) -> Self { + Self::with_config(hugr, subgraph, &Default::default()) + } + + /// Create a new ResourceScope with a custom resource flow implementation. + pub fn with_config( + hugr: H, + subgraph: SiblingSubgraph, + config: &ResourceScopeConfig, + ) -> Self { + let mut scope = Self { + hugr, + subgraph, + op_values: IndexMap::new(), + }; + scope.compute_op_values(&config.flows); + scope + } + + /// Get the underlying HUGR. + pub fn hugr(&self) -> &H { + &self.hugr + } + + /// Get the underlying subgraph. + pub fn subgraph(&self) -> &SiblingSubgraph { + &self.subgraph + } + + /// Get the opvalue for a given port. + pub fn get_opvalue(&self, node: H::Node, port: impl Into) -> Option { + let port_map = self.port_map(node)?; + Some(*port_map.get(port)) + } + + /// Get all opvalues for either the incoming or outgoing ports of a node. + pub fn get_opvalue_slice(&self, node: H::Node, direction: Direction) -> Option<&[OpValue]> { + let port_map = self.port_map(node)?; + Some(port_map.get_slice(direction)) + } + + /// Get the port of node on the given resource path. + /// + /// The returned port will have the direction `dir`. + pub fn get_port(&self, node: H::Node, resource_id: ResourceId, dir: Direction) -> Option { + let opvals = self.get_opvalue_slice(node, dir)?; + let offset = opvals.iter().position(|opval| match opval { + &OpValue::Resource(res) => res == resource_id, + _ => false, + })?; + Some(Port::new(dir, offset)) + } + + /// Get the position of the given node. + pub fn get_position(&self, node: H::Node) -> Option { + self.op_values + .get(&node) + .map(|node_op_values| node_op_values.position) + } + + /// All resource IDs on the ports of `node` in the given direction. + pub fn get_resources( + &self, + node: H::Node, + dir: Direction, + ) -> impl Iterator + '_ { + let opvals = self.get_opvalue_slice(node, dir); + opvals + .into_iter() + .flatten() + .filter_map(|opval| match opval { + &OpValue::Resource(res) => Some(res), + _ => None, + }) + } + + /// All resource IDs on the ports of `node`, in both directions. + pub fn get_all_resources(&self, node: H::Node) -> Vec { + let in_resources = self.get_resources(node, Direction::Incoming); + let out_resources = self.get_resources(node, Direction::Outgoing); + let mut all_resources = in_resources.chain(out_resources).collect_vec(); + all_resources.sort_unstable(); + all_resources.dedup(); + all_resources.shrink_to_fit(); + all_resources + } + + /// All copyable values on the ports of `node` in the given direction. + pub fn get_copyable_values( + &self, + node: H::Node, + dir: Direction, + ) -> impl Iterator + '_ { + let opvals = self.get_opvalue_slice(node, dir); + opvals + .into_iter() + .flatten() + .filter_map(|opval| match opval { + &OpValue::Copyable(id) => Some(id), + _ => None, + }) + } + + /// Iterate over the nodes on the resource path starting from the given + /// node in the given direction. + pub fn resource_path_iter( + &self, + resource_id: ResourceId, + start_node: H::Node, + direction: Direction, + ) -> impl Iterator + '_ { + let mut curr_node = start_node; + + iter::from_fn(move || { + let port = self.get_port(curr_node, resource_id, direction)?; + let (next_node, _) = self + .hugr() + .single_linked_port(curr_node, port) + .expect("linear resource"); + + Some(mem::replace(&mut curr_node, next_node)) + }) + } +} + +impl ResourceScope { + /// Create a new ResourceScope from a reference to a circuit. + pub fn from_circuit(circuit: Circuit) -> Self + where + H: Clone + HugrView, + { + let subgraph = circuit.subgraph(); + Self::new(circuit.into_hugr(), subgraph) + } +} + +impl<'h, H: HugrView> ResourceScope<&'h H> { + /// Create a new ResourceScope from a reference to a circuit. + pub fn from_circuit_ref(circuit: &'h Circuit) -> Self + where + H: Clone + HugrView, + { + let subgraph = circuit.subgraph(); + Self::new(circuit.hugr(), subgraph) + } +} + +impl + Clone> From> for ResourceScope { + fn from(value: Circuit) -> Self { + Self::from_circuit(value) + } +} + +impl<'h, H: HugrView + Clone> From<&'h Circuit> for ResourceScope<&'h H> { + fn from(value: &'h Circuit) -> Self { + Self::from_circuit_ref(value) + } +} + +// Private methods to construct the op_values map. +impl ResourceScope { + fn port_map(&self, node: H::Node) -> Option<&PortMap> { + Some(&self.op_values.get(&node)?.port_map) + } + + /// Compute op values for all nodes in the subgraph. + fn compute_op_values(&mut self, flows: &[Box]) { + let mut allocator = OpValueAllocator::default(); + + // Sentinel value for uninitialized ports + let sentinel = OpValue::Copyable(CopyableValueId::new()); + + // First, assign op values to the inputs to the subgraph. + self.assign_op_values( + self.subgraph.incoming_ports().to_owned(), + &mut allocator, + sentinel, + ); + + // Proceed to propagating the op values through the subgraph, in topological + // order. + for node in toposort_subgraph(&self.hugr, &self.subgraph, self.find_sources()) { + if self.hugr.get_optype(node).dataflow_signature().is_none() { + // ignore non-dataflow ops + continue; + } + self.assign_missing_op_values(node, &mut allocator, sentinel); + self.propagate_to_outputs(node, flows, &mut allocator); + self.propagate_to_next_inputs(node, sentinel); + } + } + + /// Assign op values to the given port groups. + /// + /// The ports are partitioned into sets of ports connected to a same value. + /// The port sets can be non-singleton only if the value is copyable. + fn assign_op_values>( + &mut self, + port_groups: impl IntoIterator, + allocator: &mut OpValueAllocator, + sentinel: OpValue, + ) { + for all_uses_of_value in port_groups { + let mut all_uses_of_input = all_uses_of_value.into_iter().peekable(); + let Some(&(fst_node, fst_port)) = all_uses_of_input.peek() else { + // this input is not used in the subgraph, we can skip it + continue; + }; + // We allocate one opvalue for all uses of this input + let op_value = allocator.allocate_op_value(fst_node, fst_port, &self.hugr); + for (node, port) in all_uses_of_input { + let node_op_values = self.op_values.entry(node).or_insert_with(|| { + let signature = self + .hugr + .get_optype(node) + .dataflow_signature() + .expect("dataflow op"); + NodeOpValues::with_default(sentinel, &signature) + }); + node_op_values.port_map.set(port, op_value); + } + } + } + + /// Find source nodes (nodes with no predecessors in the subgraph). + fn find_sources(&self) -> impl Iterator + '_ { + let has_pred_in_subgraph = |node: H::Node| { + self.hugr + .all_linked_outputs(node) + .any(|(n, _)| self.subgraph.nodes().contains(&n)) + }; + + self.subgraph + .nodes() + .iter() + .copied() + .filter(move |&n| !has_pred_in_subgraph(n)) + } + + /// Ensure all input ports have assigned op values. + fn assign_missing_op_values( + &mut self, + node: H::Node, + allocator: &mut OpValueAllocator, + sentinel: OpValue, + ) { + let signature = self + .hugr + .get_optype(node) + .dataflow_signature() + .expect("dataflow op"); + let node_op_values = self + .op_values + .entry(node) + .or_insert_with(|| NodeOpValues::with_default(sentinel, &signature)); + for p in signature.input_ports() { + if node_op_values.port_map.get(p) == &sentinel { + let op_value = allocator.allocate_op_value(node, p, &self.hugr); + node_op_values.port_map.set(p, op_value); + } + } + } + + /// Propagate op values at input ports to output ports. + fn propagate_to_outputs( + &mut self, + node: H::Node, + flows: &[Box], + allocator: &mut OpValueAllocator, + ) { + let port_map = &mut self.op_values.get_mut(&node).expect("known node").port_map; + + let inp_resources = port_map + .get_slice(Direction::Incoming) + .iter() + .map(|&op_val| match op_val { + OpValue::Resource(res) => Some(res), + OpValue::Copyable(_) => None, + }) + .collect_vec(); + + let out_resources = flows + .iter() + .find_map(|f| { + f.map_resources(self.hugr.get_optype(node), &inp_resources) + .ok() + }) + .expect("no flow found"); + + let signature = self + .hugr + .get_optype(node) + .dataflow_signature() + .expect("dataflow op"); + // Set out resources to output, create new opvalues where required + for p in signature.output_ports() { + let op_value = match out_resources.get(p.index()).copied().flatten() { + Some(resource_id) => { + let index = inp_resources + .iter() + .position(|&res| res == Some(resource_id)) + .expect("invalid resource ID returned by flow"); + *port_map.get(IncomingPort::from(index)) + } + None => allocator.allocate_op_value(node, p, &self.hugr), + }; + port_map.set(p, op_value); + } + } + + /// Propagate op values at output ports across wires to connected inputs. + fn propagate_to_next_inputs(&mut self, node: H::Node, sentinel: OpValue) { + let signature = self + .hugr + .get_optype(node) + .dataflow_signature() + .expect("dataflow op"); + let pos = self.get_position(node).expect("known node"); + + for p in signature.output_ports() { + let op_value = self.get_opvalue(node, p).expect("known node"); + + for (in_node, in_port) in self.hugr.linked_inputs(node, p) { + if !self.subgraph.nodes().contains(&in_node) { + continue; + } + let op = self.hugr.get_optype(in_node); + let Some(signature) = op.dataflow_signature() else { + continue; + }; + let next_node_op_values = self + .op_values + .entry(in_node) + .or_insert_with(|| NodeOpValues::with_default(sentinel, &signature)); + let next_op_value = op_value; + next_node_op_values.port_map.set(in_port, next_op_value); + next_node_op_values.position = + cmp::max(next_node_op_values.position, pos.increment()); + } + } + } +} + +#[derive(Debug, Clone, Default)] +struct OpValueAllocator(ResourceAllocator); + +impl OpValueAllocator { + fn allocate_resource(&mut self) -> OpValue { + let resource_id = self.0.allocate(); + OpValue::Resource(resource_id) + } + + fn allocate_copyable(&mut self) -> OpValue { + OpValue::Copyable(CopyableValueId::new()) + } + + fn allocate_op_value( + &mut self, + node: H::Node, + port: impl Into, + hugr: &H, + ) -> OpValue { + let op = hugr.get_optype(node); + let signature = op.dataflow_signature().expect("dataflow op"); + let port = port.into(); + let ty = match port.direction() { + Direction::Incoming => &signature.input()[port.index()], + Direction::Outgoing => &signature.output()[port.index()], + }; + if type_is_linear(ty) { + self.allocate_resource() + } else { + self.allocate_copyable() + } + } +} + +fn toposort_subgraph<'h, H: HugrView>( + hugr: &'h H, + subgraph: &'h SiblingSubgraph, + sources: impl IntoIterator, +) -> Vec { + fn contains_node( + node: portgraph::NodeIndex, + (subgraph, pg_map): &(&SiblingSubgraph, &H::RegionPortgraphNodes), + ) -> bool { + subgraph.nodes().contains(&pg_map.from_portgraph(node)) + } + + let (pg, pg_map) = hugr.region_portgraph(subgraph.get_parent(hugr)); + let pg: NodeFiltered<_, NodeFilter<_>, _> = + FilteredGraph::new(&pg, contains_node::, |_, _| true, (subgraph, &pg_map)); + let topo: TopoSort<_> = toposort( + pg, + sources.into_iter().map(|n| pg_map.to_portgraph(n)), + Direction::Outgoing, + ); + + topo.map(|n| pg_map.from_portgraph(n)).collect() +} + +#[cfg(test)] +pub(crate) mod tests { + //! Implementation of [`ResourceScopeReport`], used for testing. + //! + //! Tests are found in the parent module. + + use super::*; + use std::collections::{BTreeMap, HashSet}; + + use hugr::HugrView; + + use super::{OpValue, ResourceScope}; + use crate::{ + resource::{Position, ResourceId}, + utils::build_simple_circuit, + TketOp, + }; + + pub type PathEl = (Position, N, Port); + + /// Statistics about a ResourceScope. + #[derive(Debug, Clone)] + pub struct ResourceScopeReport { + pub hugr: H, + pub resource_paths: BTreeMap>>, + pub n_copyable: usize, + } + + impl<'a, H: HugrView> From<&'a ResourceScope> for ResourceScopeReport<&'a H> { + fn from(scope: &'a ResourceScope) -> Self { + let mut resource_paths: BTreeMap>> = BTreeMap::new(); + let mut copyable_values = HashSet::new(); + + for (&node, op_values) in &scope.op_values { + let pos = op_values.position; + for (port, &op_value) in op_values.port_map.iter() { + match op_value { + OpValue::Resource(res) => resource_paths + .entry(res) + .or_default() + .push((pos, node, port)), + OpValue::Copyable(id) => { + copyable_values.insert(id); + } + } + } + } + + for path in resource_paths.values_mut() { + path.sort_unstable(); + } + + Self { + hugr: &scope.hugr, + resource_paths, + n_copyable: copyable_values.len(), + } + } + } + + impl std::fmt::Display for ResourceScopeReport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Found {} copyable values", self.n_copyable)?; + writeln!(f, "Found {} resource paths:", self.resource_paths.len())?; + for (res, path) in &self.resource_paths { + writeln!(f, " - {res:?}:")?; + let mut path = path.iter().peekable(); + while let Some(&(pos, node, port)) = path.next() { + if let Some(&&(next_pos, next_node, next_port)) = path.peek() { + if next_node == node { + debug_assert_eq!(pos, next_pos); + path.next(); + let in_port = port.as_incoming().unwrap(); + let out_port = next_port.as_outgoing().unwrap(); + let op_desc = self.hugr.get_optype(node).description(); + writeln!(f, " * {op_desc}({node:?}) [{in_port} -> {out_port}]",)?; + continue; + } + } + writeln!(f, " * {node:?}@{} [{port}]", pos.to_f64(2))?; + } + writeln!(f)?; + } + Ok(()) + } + } + + #[test] + fn test_position_monotonic() { + const N_HADAMARDS: [usize; 3] = [4, 10, 1]; + // A circuit with 3 qubits and a certain number of H on each qubit + let circ = build_simple_circuit(3, |circ| { + for (qb, n_hadamards) in N_HADAMARDS.iter().enumerate() { + for _ in 0..*n_hadamards { + circ.append(TketOp::H, [qb])?; + } + } + Ok(()) + }) + .unwrap(); + + let scope = ResourceScope::from(&circ); + let first_hadamards = circ + .hugr() + .all_linked_inputs(circ.input_node()) + .map(|(n, _)| n); + + for h in first_hadamards { + let res = scope + .get_all_resources(h) + .into_iter() + .exactly_one() + .unwrap(); + let nodes_on_path = scope.resource_path_iter(res, h, Direction::Outgoing); + let pos_on_path = nodes_on_path.map(|n| scope.get_position(n).unwrap()); + + assert!( + pos_on_path.collect_vec().windows(2).all(|w| w[0] <= w[1]), + "position is not monotonically increasing on path {res:?}" + ); + } + } +} diff --git a/tket/src/resource/types.rs b/tket/src/resource/types.rs new file mode 100644 index 000000000..1f43a05f3 --- /dev/null +++ b/tket/src/resource/types.rs @@ -0,0 +1,214 @@ +//! Core type definitions for TKET resource tracking. +//! +//! This module defines the fundamental types used to track resources and +//! copyable values throughout a HUGR circuit, including resource identifiers, +//! positions, and the mapping structures that associate them with operations. + +use hugr::{types::Signature, Direction, IncomingPort, OutgoingPort, Port, PortIndex}; +use itertools::Itertools; +use num_rational::Rational64; +use uuid::Uuid; + +/// Unique identifier for a linear resource. +/// +/// ResourceIds are assigned in increasing order starting from scope inputs, +/// following the canonical topological ordering. They cannot be constructed +/// manually and must be obtained through a ResourceAllocator. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ResourceId(usize); + +impl ResourceId { + /// Create a new ResourceId. This method should only be called by ResourceAllocator. + fn new(id: usize) -> Self { + Self(id) + } + + /// Get the underlying usize value of this ResourceId. + pub fn as_usize(self) -> usize { + self.0 + } +} + +/// Unique identifier for a copyable value. +/// +/// CopyableValueIds are generated using UUIDs and are unique to the wire +/// attached to a port. They cannot be constructed manually outside of the +/// resource module. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CopyableValueId(Uuid); + +impl CopyableValueId { + /// Create a new CopyableValueId with a random UUID. + /// This method is package-private. + pub(crate) fn new() -> Self { + Self(Uuid::new_v4()) + } +} + +/// Position of an operation along a resource path. +/// +/// Positions are rational numbers that order operations along resource paths. +/// Initially assigned as contiguous integers, they may become non-integer +/// when operations are inserted or removed. +#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Position(Rational64); + +impl std::fmt::Debug for Position { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Position({})", self.0) + } +} + +impl Position { + /// Get position as f64, rounded to the given precision. + pub fn to_f64(&self, precision: usize) -> f64 { + let big = self.0 * Rational64::from_integer(10).pow(precision as i32); + big.round().to_integer() as f64 / 10f64.powi(precision as i32) + } + + /// Increment the position by 1. + pub fn increment(&self) -> Self { + Self(self.0 + 1) + } +} + +/// Value associated with a port, either a resource with position or a copyable value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum OpValue { + /// A linear resource. + Resource(ResourceId), + /// A copyable value. + Copyable(CopyableValueId), +} + +impl OpValue { + /// Returns true if this is a resource value. + pub fn is_resource(&self) -> bool { + matches!(self, OpValue::Resource(..)) + } + + /// Returns true if this is a copyable value. + pub fn is_copyable(&self) -> bool { + matches!(self, OpValue::Copyable(..)) + } + + /// Extract the ResourceId and Position if this is a resource. + pub fn as_resource(&self) -> Option { + match self { + OpValue::Resource(id) => Some(*id), + OpValue::Copyable(..) => None, + } + } + + /// Extract the CopyableValueId if this is a copyable value. + pub fn as_copyable(&self) -> Option { + match self { + OpValue::Resource(..) => None, + OpValue::Copyable(id) => Some(*id), + } + } +} + +/// Map from port indices to values. +/// +/// For an operation with n_in incoming ports and n_out outgoing ports: +/// - Port i (incoming) maps to index i +/// - Port j (outgoing) maps to index n_in + j +#[derive(Debug, Clone)] +pub(super) struct PortMap { + vec: Vec, + num_inputs: usize, +} + +impl PortMap { + pub(super) fn with_default(default: T, signature: &Signature) -> Self + where + T: Clone, + { + let num_inputs = signature.input_count(); + let num_outputs = signature.output_count(); + + debug_assert!( + signature.input_ports().all(|p| p.index() < num_inputs), + "dataflow in ports are not in range 0..num_inputs" + ); + debug_assert!( + signature.output_ports().all(|p| p.index() < num_outputs), + "dataflow out ports are not in range 0..num_outputs" + ); + + Self { + vec: vec![default; num_inputs + num_outputs], + num_inputs, + } + } + + fn index(&self, port: impl Into) -> usize { + let port = port.into(); + match port.direction() { + Direction::Incoming => port.index(), + Direction::Outgoing => self.num_inputs + port.index(), + } + } + + pub(super) fn get(&self, port: impl Into) -> &T { + let port = port.into(); + let index = self.index(port); + &self.vec[index] + } + + pub(super) fn get_slice(&self, dir: Direction) -> &[T] { + match dir { + Direction::Incoming => &self.vec[..self.num_inputs], + Direction::Outgoing => &self.vec[self.num_inputs..], + } + } + + pub(super) fn set(&mut self, port: impl Into, value: impl Into) { + let port = port.into(); + let index = self.index(port); + self.vec[index] = value.into(); + } + + #[allow(unused)] + pub(super) fn iter(&self) -> impl Iterator { + let (inp_slice, out_slice) = self.vec.split_at(self.num_inputs); + let inp_ports = (0..).map(IncomingPort::from).map_into(); + let out_ports = (0..).map(OutgoingPort::from).map_into(); + let inp = inp_ports.zip(inp_slice); + let out = out_ports.zip(out_slice); + inp.chain(out) + } + + #[allow(unused)] + pub(super) fn values(&self) -> impl Iterator { + self.vec.iter() + } +} + +/// Allocator for ResourceIds that ensures they are assigned in increasing +/// order. +#[derive(Debug, Clone)] +pub struct ResourceAllocator { + next_id: usize, +} + +impl ResourceAllocator { + /// Create a new ResourceAllocator starting from ID 0. + pub fn new() -> Self { + Self { next_id: 0 } + } + + /// Allocate the next available ResourceId. + pub fn allocate(&mut self) -> ResourceId { + let id = ResourceId::new(self.next_id); + self.next_id += 1; + id + } +} + +impl Default for ResourceAllocator { + fn default() -> Self { + Self::new() + } +} diff --git a/tket/src/snapshots/tket__resource__tests__2_qubits.snap b/tket/src/snapshots/tket__resource__tests__2_qubits.snap new file mode 100644 index 000000000..f0086c513 --- /dev/null +++ b/tket/src/snapshots/tket__resource__tests__2_qubits.snap @@ -0,0 +1,13 @@ +--- +source: tket/src/resource.rs +expression: info +--- +Found 0 copyable values +Found 2 resource paths: + - ResourceId(0): + * H(Node(7)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(9)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(1): + * H(Node(8)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(9)) [IncomingPort(1) -> OutgoingPort(1)] diff --git a/tket/src/snapshots/tket__resource__tests__2_qubits_add_const_rz.snap b/tket/src/snapshots/tket__resource__tests__2_qubits_add_const_rz.snap new file mode 100644 index 000000000..d0a72f074 --- /dev/null +++ b/tket/src/snapshots/tket__resource__tests__2_qubits_add_const_rz.snap @@ -0,0 +1,15 @@ +--- +source: tket/src/resource.rs +expression: info +--- +Found 1 copyable values +Found 2 resource paths: + - ResourceId(0): + * H(Node(7)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(9)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(12)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(1): + * H(Node(8)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(9)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(13)) [IncomingPort(0) -> OutgoingPort(0)] diff --git a/tket/src/snapshots/tket__resource__tests__2_qubits_add_rz.snap b/tket/src/snapshots/tket__resource__tests__2_qubits_add_rz.snap new file mode 100644 index 000000000..a573ae0fe --- /dev/null +++ b/tket/src/snapshots/tket__resource__tests__2_qubits_add_rz.snap @@ -0,0 +1,15 @@ +--- +source: tket/src/resource.rs +expression: info +--- +Found 1 copyable values +Found 2 resource paths: + - ResourceId(0): + * H(Node(7)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(9)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(10)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(1): + * H(Node(8)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(9)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(11)) [IncomingPort(0) -> OutgoingPort(0)] diff --git a/tket/src/snapshots/tket__resource__tests__2_qubits_add_rz_add_const_rz.snap b/tket/src/snapshots/tket__resource__tests__2_qubits_add_rz_add_const_rz.snap new file mode 100644 index 000000000..0ba25f9cc --- /dev/null +++ b/tket/src/snapshots/tket__resource__tests__2_qubits_add_rz_add_const_rz.snap @@ -0,0 +1,17 @@ +--- +source: tket/src/resource.rs +expression: info +--- +Found 2 copyable values +Found 2 resource paths: + - ResourceId(0): + * H(Node(7)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(9)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(10)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(15)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(1): + * H(Node(8)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(9)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(11)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(16)) [IncomingPort(0) -> OutgoingPort(0)] diff --git a/tket/src/snapshots/tket__resource__tests__4_qubits.snap b/tket/src/snapshots/tket__resource__tests__4_qubits.snap new file mode 100644 index 000000000..323f7a821 --- /dev/null +++ b/tket/src/snapshots/tket__resource__tests__4_qubits.snap @@ -0,0 +1,21 @@ +--- +source: tket/src/resource.rs +expression: info +--- +Found 0 copyable values +Found 4 resource paths: + - ResourceId(0): + * H(Node(7)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(11)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(1): + * H(Node(8)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(11)) [IncomingPort(1) -> OutgoingPort(1)] + + - ResourceId(2): + * H(Node(9)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(12)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(3): + * H(Node(10)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(12)) [IncomingPort(1) -> OutgoingPort(1)] diff --git a/tket/src/snapshots/tket__resource__tests__4_qubits_add_const_rz.snap b/tket/src/snapshots/tket__resource__tests__4_qubits_add_const_rz.snap new file mode 100644 index 000000000..f31c137c3 --- /dev/null +++ b/tket/src/snapshots/tket__resource__tests__4_qubits_add_const_rz.snap @@ -0,0 +1,25 @@ +--- +source: tket/src/resource.rs +expression: info +--- +Found 1 copyable values +Found 4 resource paths: + - ResourceId(0): + * H(Node(7)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(11)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(15)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(1): + * H(Node(8)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(11)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(16)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(2): + * H(Node(9)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(12)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(18)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(3): + * H(Node(10)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(12)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(19)) [IncomingPort(0) -> OutgoingPort(0)] diff --git a/tket/src/snapshots/tket__resource__tests__4_qubits_add_rz.snap b/tket/src/snapshots/tket__resource__tests__4_qubits_add_rz.snap new file mode 100644 index 000000000..b5777ff54 --- /dev/null +++ b/tket/src/snapshots/tket__resource__tests__4_qubits_add_rz.snap @@ -0,0 +1,25 @@ +--- +source: tket/src/resource.rs +expression: info +--- +Found 1 copyable values +Found 4 resource paths: + - ResourceId(0): + * H(Node(7)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(11)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(13)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(1): + * H(Node(8)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(11)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(14)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(2): + * H(Node(9)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(12)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(16)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(3): + * H(Node(10)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(12)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(17)) [IncomingPort(0) -> OutgoingPort(0)] diff --git a/tket/src/snapshots/tket__resource__tests__4_qubits_add_rz_add_const_rz.snap b/tket/src/snapshots/tket__resource__tests__4_qubits_add_rz_add_const_rz.snap new file mode 100644 index 000000000..a62bd0909 --- /dev/null +++ b/tket/src/snapshots/tket__resource__tests__4_qubits_add_rz_add_const_rz.snap @@ -0,0 +1,29 @@ +--- +source: tket/src/resource.rs +expression: info +--- +Found 2 copyable values +Found 4 resource paths: + - ResourceId(0): + * H(Node(7)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(11)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(13)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(20)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(1): + * H(Node(8)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(11)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(14)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(21)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(2): + * H(Node(9)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(12)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(16)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(23)) [IncomingPort(0) -> OutgoingPort(0)] + + - ResourceId(3): + * H(Node(10)) [IncomingPort(0) -> OutgoingPort(0)] + * CX(Node(12)) [IncomingPort(1) -> OutgoingPort(1)] + * Rz(Node(17)) [IncomingPort(0) -> OutgoingPort(0)] + * Rz(Node(24)) [IncomingPort(0) -> OutgoingPort(0)]