Skip to content

Commit e58de1d

Browse files
committed
feat: Add ResourceScope
1 parent 5514164 commit e58de1d

16 files changed

+1250
-98
lines changed

Cargo.lock

Lines changed: 131 additions & 98 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ typetag = "0.2.20"
8787
cool_asserts = "2.0.3"
8888
zstd = "0.13.3"
8989
anyhow = "1.0.98"
90+
num-rational = "0.4"
91+
uuid = { version = "1.0", features = ["v4"] }
9092

9193
[profile.release.package.tket-py]
9294
# Some configurations to reduce the size of tket wheels

tket/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ pest = { workspace = true }
7676
pest_derive = { workspace = true }
7777
zstd = { workspace = true, optional = true }
7878
anyhow = { workspace = true, optional = true }
79+
num-rational = { workspace = true }
80+
uuid = { workspace = true }
7981

8082

8183
[dev-dependencies]
@@ -86,6 +88,7 @@ cool_asserts = { workspace = true }
8688
# Defined here so it can be overridden by the codspeed CI job
8789
# using `cargo add`.
8890
criterion = { version = "0.7.0", features = ["html_reports"] }
91+
insta = "1.43.1"
8992

9093
[[bench]]
9194
name = "bench_main"

tket/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub mod extension;
4949
pub(crate) mod ops;
5050
pub mod optimiser;
5151
pub mod passes;
52+
pub mod resource;
5253
pub mod rewrite;
5354
pub mod serialize;
5455

tket/src/resource.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
//! Resource tracking within HUGR DFG subgraphs.
2+
//!
3+
//! This module implements the resource tracking system. It provides
4+
//! facilities for tracking linear resources (such as qubits) through quantum
5+
//! circuits represented as HUGR subgraphs.
6+
//!
7+
//! # Overview
8+
//!
9+
//! HUGR has a notion of "Value": the data that corresponds to a wire within a
10+
//! dataflow graph. It further has a notion of "linear value" a.k.a non-copyable
11+
//! value: a value that cannot be copied or discarded (implicitly).
12+
//!
13+
//! As far as HUGR is concerned, a linear value (or any value, for that matter)
14+
//! is born at an op's output and dies at the next op's input. TKET introduces
15+
//! the notion of "Resource" to extend the lifetime of a linear value over
16+
//! multiple ops.
17+
//!
18+
//! If a linear value appears both in an op's input and output, we say that it
19+
//! is "resource-preserving". Using [`ResourceFlow`], we can track resources
20+
//! as they "flow" through multiple operations. The chains of
21+
//! resource-preserving ops acting on a same resource form a so-called resource
22+
//! path.
23+
//!
24+
//! # Resources and Copyable Values
25+
//!
26+
//! Resource tracking distinguishes between two types of values:
27+
//!
28+
//! - **Linear resources**: Non-copyable values that form resource paths through
29+
//! the circuit. Each resource has a unique [`ResourceId`] and operations on
30+
//! the same resource are ordered by a [`Position`].
31+
//! - **Copyable values**: Regular values that can be copied and discarded
32+
//! freely. Each is identified by a unique [`CopyableValueId`].
33+
//!
34+
//! # Resource Scope
35+
//!
36+
//! Tracking resources is not free: there is a one-off linear cost to compute
37+
//! the resource paths, plus a linear memory cost to store them.
38+
//!
39+
//! Use a [`SiblingSubgraph`] to define a region of a `HUGR`, within which
40+
//! resources should be tracked. You can then construct a resource-tracked scope
41+
//! using [`ResourceScope::new`].
42+
43+
// Public API exports
44+
pub use flow::{DefaultResourceFlow, ResourceFlow, UnsupportedOp};
45+
pub use scope::{ResourceScope, ResourceScopeConfig};
46+
pub use types::{CopyableValueId, OpValue, Position, ResourceAllocator, ResourceId};
47+
48+
// Internal modules
49+
mod flow;
50+
mod scope;
51+
mod types;
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use hugr::{
56+
builder::{DFGBuilder, Dataflow, DataflowHugr},
57+
extension::prelude::qb_t,
58+
hugr::views::SiblingSubgraph,
59+
ops::handle::DataflowParentID,
60+
types::Signature,
61+
CircuitUnit, Hugr,
62+
};
63+
64+
use itertools::Itertools;
65+
use rstest::rstest;
66+
67+
use crate::{
68+
extension::rotation::{rotation_type, ConstRotation},
69+
resource::scope::tests::ResourceScopeReport,
70+
TketOp,
71+
};
72+
73+
use super::ResourceScope;
74+
75+
// Gate being commuted has a non-linear input
76+
fn circ(n_qubits: usize, add_rz: bool, add_const_rz: bool) -> Hugr {
77+
let build = || {
78+
let out_qb_row = vec![qb_t(); n_qubits];
79+
let mut inp_qb_row = out_qb_row.clone();
80+
if add_rz {
81+
inp_qb_row.push(rotation_type());
82+
};
83+
let mut dfg = DFGBuilder::new(Signature::new(inp_qb_row, out_qb_row))?;
84+
85+
let (qubits, f) = if add_rz {
86+
let mut inputs = dfg.input_wires().collect_vec();
87+
let f = inputs.pop().unwrap();
88+
(inputs, Some(f))
89+
} else {
90+
(dfg.input_wires().collect_vec(), None)
91+
};
92+
93+
let mut circ = dfg.as_circuit(qubits);
94+
95+
for i in 0..n_qubits {
96+
circ.append(TketOp::H, [i])?;
97+
}
98+
for i in (0..n_qubits).step_by(2) {
99+
if i + 1 < n_qubits {
100+
circ.append(TketOp::CX, [i, i + 1])?;
101+
}
102+
}
103+
if let Some(f) = f {
104+
for i in 0..n_qubits {
105+
circ.append_and_consume(
106+
TketOp::Rz,
107+
[CircuitUnit::Linear(i), CircuitUnit::Wire(f)],
108+
)?;
109+
}
110+
}
111+
if add_const_rz {
112+
let const_angle = circ.add_constant(ConstRotation::PI_2);
113+
for i in 0..n_qubits {
114+
circ.append_and_consume(
115+
TketOp::Rz,
116+
[CircuitUnit::Linear(i), CircuitUnit::Wire(const_angle)],
117+
)?;
118+
}
119+
}
120+
let qbs = circ.finish();
121+
dfg.finish_hugr_with_outputs(qbs)
122+
};
123+
build().unwrap()
124+
}
125+
126+
#[rstest]
127+
#[case(2, false, false)]
128+
#[case(2, true, false)]
129+
#[case(2, false, true)]
130+
#[case(2, true, true)]
131+
#[case(4, false, false)]
132+
#[case(4, true, false)]
133+
#[case(4, false, true)]
134+
#[case(4, true, true)]
135+
fn test_resource_scope_creation(
136+
#[case] n_qubits: usize,
137+
#[case] add_rz: bool,
138+
#[case] add_const_rz: bool,
139+
) {
140+
let circ = circ(n_qubits, add_rz, add_const_rz);
141+
let subgraph =
142+
SiblingSubgraph::try_new_dataflow_subgraph::<_, DataflowParentID>(&circ).unwrap();
143+
let scope = ResourceScope::new(&circ, subgraph);
144+
let info = ResourceScopeReport::from(&scope);
145+
146+
let mut name = format!("{n_qubits}_qubits");
147+
if add_rz {
148+
name.push('_');
149+
name.push_str("add_rz");
150+
}
151+
if add_const_rz {
152+
name.push('_');
153+
name.push_str("add_const_rz");
154+
}
155+
156+
assert_eq!(info.resource_paths.len(), n_qubits);
157+
assert_eq!(info.n_copyable, add_const_rz as usize + add_rz as usize);
158+
insta::assert_snapshot!(name, info);
159+
}
160+
}

tket/src/resource/flow.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//! Resource flow logic for tracking how resources move through operations.
2+
//!
3+
//! This module defines the [`ResourceFlow`] trait that specifies how resources
4+
//! flow through operations, along with a default implementation.
5+
6+
use crate::resource::types::ResourceId;
7+
use derive_more::derive::{Display, Error};
8+
use hugr::ops::{OpTrait, OpType};
9+
use hugr::types::Type;
10+
use itertools::{EitherOrBoth, Itertools};
11+
12+
/// Error type for unsupported operations in ResourceFlow implementations.
13+
#[derive(Debug, Display, Clone, PartialEq, Error)]
14+
#[display("Unsupported operation '{op_type:?}'")]
15+
pub struct UnsupportedOp {
16+
/// The operation type that is unsupported.
17+
pub op_type: OpType,
18+
}
19+
20+
/// Trait for specifying how resources flow through operations.
21+
///
22+
/// This trait allows different implementations to define how linear resources
23+
/// are mapped from inputs to outputs through various operation types.
24+
pub trait ResourceFlow {
25+
/// Map resource IDs from operation inputs to outputs.
26+
///
27+
/// Takes an operation type and the resource IDs of the operation's inputs.
28+
/// The i-th entry is Some(resource_id) if the i-th port is a linear type,
29+
/// None otherwise. Returns the resource IDs of the operation's outputs
30+
/// in port order. Output resource IDs should be one of the input resource
31+
/// IDs for resource-preserving operations, or None for new resources or
32+
/// non-linear types.
33+
///
34+
/// # Arguments
35+
/// * `op` - The operation type
36+
/// * `inputs` - Resource IDs for each input port (None for non-linear
37+
/// types)
38+
///
39+
/// # Returns
40+
/// Resource IDs for each output port, or UnsupportedOp if the operation
41+
/// cannot be handled by this implementation.
42+
fn map_resources(
43+
&self,
44+
op: &OpType,
45+
inputs: &[Option<ResourceId>],
46+
) -> Result<Vec<Option<ResourceId>>, UnsupportedOp>;
47+
}
48+
49+
/// Default implementation of ResourceFlow.
50+
///
51+
/// This implementation considers that an operation is resource-preserving if
52+
/// whenever the i-th input or output is linear, then the i-th input type
53+
/// matches the i-th output. The i-th input is then mapped to the i-th output.
54+
///
55+
/// Otherwise, all input resources are discarded and all outputs will be given
56+
/// fresh resource IDs.
57+
#[derive(Debug, Clone, Default)]
58+
pub struct DefaultResourceFlow;
59+
60+
impl DefaultResourceFlow {
61+
/// Create a new DefaultResourceFlow instance.
62+
pub fn new() -> Self {
63+
Self
64+
}
65+
66+
/// Check if a type is linear (non-copyable).
67+
fn is_linear_type(ty: &Type) -> bool {
68+
!ty.copyable()
69+
}
70+
71+
/// Determine if an operation is resource-preserving based on input/output
72+
/// types.
73+
fn is_resource_preserving(input_types: &[Type], output_types: &[Type]) -> bool {
74+
// An operation is resource-preserving if for each i, if input[i] or
75+
// output[i] is linear, then type(input[i]) == type(output[i])
76+
77+
for io_ty in input_types.iter().zip_longest(output_types.iter()) {
78+
let (input_ty, output_ty) = match io_ty {
79+
EitherOrBoth::Both(input_ty, output_ty) => (input_ty, output_ty),
80+
EitherOrBoth::Left(ty) | EitherOrBoth::Right(ty) => {
81+
if Self::is_linear_type(ty) {
82+
// linear type on one side, nothing on the other
83+
return false;
84+
}
85+
continue;
86+
}
87+
};
88+
89+
if Self::is_linear_type(input_ty) || Self::is_linear_type(output_ty) {
90+
// If input/output is linear, both must be the same type
91+
if input_ty != output_ty {
92+
return false;
93+
}
94+
}
95+
}
96+
97+
true
98+
}
99+
}
100+
101+
impl ResourceFlow for DefaultResourceFlow {
102+
fn map_resources(
103+
&self,
104+
op: &OpType,
105+
inputs: &[Option<ResourceId>],
106+
) -> Result<Vec<Option<ResourceId>>, UnsupportedOp> {
107+
let signature = op.dataflow_signature().expect("dataflow op");
108+
let input_types = signature.input_types();
109+
let output_types = signature.output_types();
110+
111+
debug_assert_eq!(
112+
inputs.len(),
113+
input_types.len(),
114+
"Input resource array length must match operation input count"
115+
);
116+
117+
if Self::is_resource_preserving(input_types, output_types) {
118+
Ok(retain_linear_types(inputs.to_vec(), output_types))
119+
} else {
120+
// Resource-producing/consuming: all linear outputs are new resources (None)
121+
Ok(vec![None; output_types.len()])
122+
}
123+
}
124+
}
125+
126+
fn retain_linear_types(
127+
mut resources: Vec<Option<ResourceId>>,
128+
types: &[Type],
129+
) -> Vec<Option<ResourceId>> {
130+
for (ty, resource) in types.iter().zip(resources.iter_mut()) {
131+
if ty.copyable() {
132+
*resource = None;
133+
}
134+
}
135+
resources
136+
}

0 commit comments

Comments
 (0)