Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
38 changes: 38 additions & 0 deletions test_files/guppy_optimization/func_decls/func_decls.flat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "guppylang >=0.21.6",
# ]
# ///

from pathlib import Path
from sys import argv

from guppylang import guppy
from guppylang.std.angles import angle
from guppylang.std.quantum import rx, h, qubit


@guppy.declare
def func1() -> float: ...


@guppy.declare
def func2(f: float, q: qubit) -> float: ...


@guppy.comptime
def unknown_rotations(q: qubit) -> None:
rotation = func1()
rx(q, angle(rotation))

# Some operations that we can optimize away.
h(q)
h(q)

other_rotation = func2(rotation + 1.0, q)
rx(q, angle(other_rotation))


program = unknown_rotations.compile_function()
Path(argv[0]).with_suffix(".hugr").write_bytes(program.to_bytes())
Binary file not shown.
Binary file not shown.
34 changes: 34 additions & 0 deletions test_files/guppy_optimization/func_decls/func_decls.opt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "guppylang >=0.21.6",
# ]
# ///

from pathlib import Path
from sys import argv

from guppylang import guppy
from guppylang.std.angles import angle
from guppylang.std.quantum import rx, qubit


@guppy.declare
def func1() -> float: ...


@guppy.declare
def func2(f: float, q: qubit) -> float: ...


@guppy
def unknown_rotations(q: qubit) -> None:
rotation = func1()
rx(q, angle(rotation))

other_rotation = func2(rotation + 1.0, q)
rx(q, angle(other_rotation))


program = unknown_rotations.compile_function()
Path(argv[0]).with_suffix(".hugr").write_bytes(program.to_bytes())
38 changes: 38 additions & 0 deletions test_files/guppy_optimization/func_decls/func_decls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "guppylang >=0.21.6",
# ]
# ///

from pathlib import Path
from sys import argv

from guppylang import guppy
from guppylang.std.angles import angle
from guppylang.std.quantum import rx, h, qubit


@guppy.declare
def func1() -> float: ...


@guppy.declare
def func2(f: float, q: qubit) -> float: ...


@guppy
def unknown_rotations(q: qubit) -> None:
rotation = func1()
rx(q, angle(rotation))

# Some operations that we can optimize away.
h(q)
h(q)

other_rotation = func2(rotation + 1.0, q)
rx(q, angle(other_rotation))


program = unknown_rotations.compile_function()
Path(argv[0]).with_suffix(".hugr").write_bytes(program.to_bytes())
4 changes: 4 additions & 0 deletions tket-qsystem/tests/guppy_opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ fn count_gates(h: &impl HugrView) -> HashMap<SmolStr, usize> {
#[case::false_branch("false_branch", Some(vec![
("TKET1.tk1op", 1), ("tket.quantum.H", 1), ("tket.quantum.QAlloc", 1), ("tket.quantum.MeasureFree", 1)
]))]
#[should_panic = "xfail"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this new test case related to the rest of the patch? I don't see how.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the test wasn't actually generating parametric barriers.

I added a qubit parameter to func2 in the guppy program, so now the call gets encoded as a barrier on a qubit with a rotation parameter.

#[case::func_decls("func_decls", Some(vec![
("TKET1.tk1op", 2), ("tket.quantum.symbolic_angle", 1)
]))]
#[cfg_attr(miri, ignore)] // Opening files is not supported in (isolated) miri
fn optimize_flattened_guppy(#[case] name: &str, #[case] xfail: Option<Vec<(&str, usize)>>) {
let mut hugr = load_guppy_circuit(name, HugrFileType::Flat)
Expand Down
25 changes: 21 additions & 4 deletions tket/src/serialize/pytket/decoder/subgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,35 @@ impl<'h> PytketDecoderContext<'h> {
&mut self,
qubits: &[TrackedQubit],
bits: &[TrackedBit],
params: &[LoadedParameter],
payload: &OpaqueSubgraphPayload,
) -> Result<DecodeStatus, PytketDecodeError> {
let mut load_params = |input_params: &[String]| {
input_params
.iter()
.map(|p| self.load_half_turns(p))
.collect_vec()
};
let status = match payload {
OpaqueSubgraphPayload::External { id } => {
self.insert_external_subgraph(*id, qubits, bits, params)
OpaqueSubgraphPayload::External { id, input_params } => {
let loaded_params = load_params(input_params);
self.insert_external_subgraph(*id, qubits, bits, &loaded_params)
}
OpaqueSubgraphPayload::Inline {
hugr_envelope,
inputs,
outputs,
} => self.insert_inline_subgraph(hugr_envelope, inputs, outputs, qubits, bits, params),
input_params,
} => {
let loaded_params = load_params(input_params);
self.insert_inline_subgraph(
hugr_envelope,
inputs,
outputs,
qubits,
bits,
&loaded_params,
)
}
}?;

// Mark the used qubits and bits as outdated.
Expand Down
5 changes: 3 additions & 2 deletions tket/src/serialize/pytket/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,6 @@ impl<H: HugrView> PytketEncoderContext<H> {
let subgraph_id = self
.opaque_subgraphs
.register_opaque_subgraph(subgraph.clone());
let payload = OpaqueSubgraphPayload::new_external(subgraph_id);

// Collects the input values for the subgraph.
//
Expand All @@ -604,6 +603,8 @@ impl<H: HugrView> PytketEncoderContext<H> {
.map(|p| self.values.param_expression(p).to_owned())
.collect();

let payload = OpaqueSubgraphPayload::new_external(subgraph_id, input_param_exprs.clone());

// Update the values in the node's outputs, and extend `op_values` with
// any new output values.
//
Expand Down Expand Up @@ -660,7 +661,7 @@ impl<H: HugrView> PytketEncoderContext<H> {
let args = MakeOperationArgs {
num_qubits: op_values.qubits.len(),
num_bits: op_values.bits.len(),
params: Cow::Borrowed(&input_param_exprs),
params: Cow::Borrowed(&[]),
};
let mut pytket_op = make_tk1_operation(tket_json_rs::OpType::Barrier, args);
pytket_op.data = Some(serde_json::to_string(&payload).unwrap());
Expand Down
15 changes: 11 additions & 4 deletions tket/src/serialize/pytket/extension/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,17 @@ impl PytketDecoder for CoreDecoder {
op_type: PytketOptype::Barrier,
data: Some(payload),
..
} => match OpaqueSubgraphPayload::load_str(payload, decoder.extension_registry()) {
Ok(payload) => decoder.insert_subgraph_from_payload(qubits, bits, params, &payload),
_ => Ok(DecodeStatus::Unsupported),
},
} =>
// Load an opaque subgraph from a barrier.
//
// Note that pytket can delete parameters to a barrier operation, so
// we encode them in the payload instead of reading `params`.
{
match OpaqueSubgraphPayload::load_str(payload, decoder.extension_registry()) {
Ok(payload) => decoder.insert_subgraph_from_payload(qubits, bits, &payload),
_ => Ok(DecodeStatus::Unsupported),
}
}
PytketOperation {
op_type: PytketOptype::CircBox,
op_box:
Expand Down
6 changes: 4 additions & 2 deletions tket/src/serialize/pytket/opaque.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,17 @@ impl<N: HugrNode> OpaqueSubgraphs<N> {
return Ok(());
};

let Some(subgraph_id) = OpaqueSubgraphPayload::parse_external_id(&payload) else {
let Some((subgraph_id, input_arguments)) =
OpaqueSubgraphPayload::parse_external_payload(&payload)
else {
// Not an External Payload, nothing to do.
return Ok(());
};
if !self.contains(subgraph_id) {
return Err(PytketEncodeError::custom(format!("Barrier operation with external subgraph payload points to an unknown subgraph: {subgraph_id}")));
}

let payload = OpaqueSubgraphPayload::new_inline(&self[subgraph_id], hugr)?;
let payload = OpaqueSubgraphPayload::new_inline(&self[subgraph_id], hugr, input_arguments)?;
command.op.data = Some(serde_json::to_string(&payload).unwrap());

Ok(())
Expand Down
37 changes: 30 additions & 7 deletions tket/src/serialize/pytket/opaque/payload.rs
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the main change, the payloads now include the input_params, and we read that field when decoding.

Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ pub enum OpaqueSubgraphPayload {
External {
/// The ID of the subgraph in the `OpaqueSubgraphs` registry.
id: SubgraphId,
/// Input parameters to the subgraph.
///
/// Pytket may delete parameters specified on a barrier command, so we
/// need to encode them here instead.
input_params: Vec<String>,
},
/// An inline payload, carrying the encoded envelope for the HUGR subgraph.
#[serde(rename = "HugrInline")]
Expand All @@ -108,14 +113,25 @@ pub enum OpaqueSubgraphPayload {
/// The types can also be inferred from the encoded hugr or linked
/// subcircuit, but we store them here for robustness.
outputs: Vec<(Type, EncodedEdgeID)>,
/// Input parameters to the subgraph.
///
/// Pytket may delete parameters specified on a barrier command, so we
/// need to encode them here instead.
input_params: Vec<String>,
},
}

impl OpaqueSubgraphPayload {
/// Create an external payload by referencing a subgraph in the tracked by
/// an [`EncodedCircuit`][super::super::EncodedCircuit].
pub fn new_external(subgraph_id: SubgraphId) -> Self {
Self::External { id: subgraph_id }
pub fn new_external(
subgraph_id: SubgraphId,
input_params: impl IntoIterator<Item = String>,
) -> Self {
Self::External {
id: subgraph_id,
input_params: input_params.into_iter().collect(),
}
}

/// Create a new payload for an opaque subgraph in the Hugr.
Expand All @@ -133,6 +149,7 @@ impl OpaqueSubgraphPayload {
pub fn new_inline<N: HugrNode>(
subgraph: &OpaqueSubgraph<N>,
hugr: &impl HugrView<Node = N>,
input_params: impl IntoIterator<Item = String>,
) -> Result<Self, PytketEncodeError<N>> {
let signature = subgraph.signature();

Expand Down Expand Up @@ -167,6 +184,7 @@ impl OpaqueSubgraphPayload {
hugr_envelope,
inputs: signature.input().iter().cloned().zip(inputs).collect(),
outputs: signature.output().iter().cloned().zip(outputs).collect(),
input_params: input_params.into_iter().collect(),
})
}

Expand Down Expand Up @@ -215,26 +233,31 @@ impl OpaqueSubgraphPayload {
matches!(self, Self::External { .. })
}

/// Parse the contents of an [`OpaqueSubgraphPayload::External`] from a string payload.
/// Parse the contents of an [`OpaqueSubgraphPayload::External`] from a
/// string payload.
///
/// Returns the subgraph ID and the list of input parameter expressions, if
/// the payload is a valid [`OpaqueSubgraphPayload::External`].
///
/// Returns `None` if the payload is [`OpaqueSubgraphPayload::Inline`] or not an
/// [`OpaqueSubgraphPayload`].
/// Returns `None` if the payload is [`OpaqueSubgraphPayload::Inline`] or
/// not an [`OpaqueSubgraphPayload`].
///
/// This method is more efficient than calling [Self::load_str], as it
/// requires no allocations.
pub fn parse_external_id(payload: &str) -> Option<SubgraphId> {
pub fn parse_external_payload(payload: &str) -> Option<(SubgraphId, Vec<String>)> {
#[derive(serde::Deserialize)]
#[serde(rename = "HugrExternal")]
#[serde(tag = "typ")]
struct PartialPayload {
pub id: SubgraphId,
pub input_params: Vec<String>,
}

// Deserialize the payload if it is External, avoiding a full copy to memory
// for the other variant.
serde_json::from_str::<PartialPayload>(payload)
.ok()
.map(|payload| payload.id)
.map(|payload| (payload.id, payload.input_params))
}

/// Returns `true` if a string encodes an [`OpaqueSubgraphPayload`].
Expand Down
Loading