diff --git a/test_files/guppy_optimization/func_decls/func_decls.flat.hugr b/test_files/guppy_optimization/func_decls/func_decls.flat.hugr new file mode 100644 index 000000000..50622c184 Binary files /dev/null and b/test_files/guppy_optimization/func_decls/func_decls.flat.hugr differ diff --git a/test_files/guppy_optimization/func_decls/func_decls.flat.py b/test_files/guppy_optimization/func_decls/func_decls.flat.py new file mode 100644 index 000000000..320f5106f --- /dev/null +++ b/test_files/guppy_optimization/func_decls/func_decls.flat.py @@ -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()) diff --git a/test_files/guppy_optimization/func_decls/func_decls.hugr b/test_files/guppy_optimization/func_decls/func_decls.hugr new file mode 100644 index 000000000..605294b82 Binary files /dev/null and b/test_files/guppy_optimization/func_decls/func_decls.hugr differ diff --git a/test_files/guppy_optimization/func_decls/func_decls.opt.hugr b/test_files/guppy_optimization/func_decls/func_decls.opt.hugr new file mode 100644 index 000000000..924789f86 Binary files /dev/null and b/test_files/guppy_optimization/func_decls/func_decls.opt.hugr differ diff --git a/test_files/guppy_optimization/func_decls/func_decls.opt.py b/test_files/guppy_optimization/func_decls/func_decls.opt.py new file mode 100644 index 000000000..5347e9941 --- /dev/null +++ b/test_files/guppy_optimization/func_decls/func_decls.opt.py @@ -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()) diff --git a/test_files/guppy_optimization/func_decls/func_decls.py b/test_files/guppy_optimization/func_decls/func_decls.py new file mode 100644 index 000000000..b764453f9 --- /dev/null +++ b/test_files/guppy_optimization/func_decls/func_decls.py @@ -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()) diff --git a/tket-qsystem/tests/guppy_opt.rs b/tket-qsystem/tests/guppy_opt.rs index 65957e1a0..d8fbe9b57 100644 --- a/tket-qsystem/tests/guppy_opt.rs +++ b/tket-qsystem/tests/guppy_opt.rs @@ -100,6 +100,10 @@ fn count_gates(h: &impl HugrView) -> HashMap { #[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"] +#[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>) { let mut hugr = load_guppy_circuit(name, HugrFileType::Flat) diff --git a/tket/src/serialize/pytket/decoder/subgraph.rs b/tket/src/serialize/pytket/decoder/subgraph.rs index b09a1999c..791f432ae 100644 --- a/tket/src/serialize/pytket/decoder/subgraph.rs +++ b/tket/src/serialize/pytket/decoder/subgraph.rs @@ -30,18 +30,35 @@ impl<'h> PytketDecoderContext<'h> { &mut self, qubits: &[TrackedQubit], bits: &[TrackedBit], - params: &[LoadedParameter], payload: &OpaqueSubgraphPayload, ) -> Result { + 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. diff --git a/tket/src/serialize/pytket/encoder.rs b/tket/src/serialize/pytket/encoder.rs index a3dd3c0fd..2cb9c6c9f 100644 --- a/tket/src/serialize/pytket/encoder.rs +++ b/tket/src/serialize/pytket/encoder.rs @@ -578,7 +578,6 @@ impl PytketEncoderContext { 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. // @@ -604,6 +603,8 @@ impl PytketEncoderContext { .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. // @@ -660,7 +661,7 @@ impl PytketEncoderContext { 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()); diff --git a/tket/src/serialize/pytket/extension/core.rs b/tket/src/serialize/pytket/extension/core.rs index 3cabde873..aec85240b 100644 --- a/tket/src/serialize/pytket/extension/core.rs +++ b/tket/src/serialize/pytket/extension/core.rs @@ -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: diff --git a/tket/src/serialize/pytket/opaque.rs b/tket/src/serialize/pytket/opaque.rs index e86b23b27..1405e6ef0 100644 --- a/tket/src/serialize/pytket/opaque.rs +++ b/tket/src/serialize/pytket/opaque.rs @@ -144,7 +144,9 @@ impl OpaqueSubgraphs { 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(()); }; @@ -152,7 +154,7 @@ impl OpaqueSubgraphs { 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(()) diff --git a/tket/src/serialize/pytket/opaque/payload.rs b/tket/src/serialize/pytket/opaque/payload.rs index 5794f3369..4bd97f8a8 100644 --- a/tket/src/serialize/pytket/opaque/payload.rs +++ b/tket/src/serialize/pytket/opaque/payload.rs @@ -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, }, /// An inline payload, carrying the encoded envelope for the HUGR subgraph. #[serde(rename = "HugrInline")] @@ -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, }, } 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, + ) -> Self { + Self::External { + id: subgraph_id, + input_params: input_params.into_iter().collect(), + } } /// Create a new payload for an opaque subgraph in the Hugr. @@ -133,6 +149,7 @@ impl OpaqueSubgraphPayload { pub fn new_inline( subgraph: &OpaqueSubgraph, hugr: &impl HugrView, + input_params: impl IntoIterator, ) -> Result> { let signature = subgraph.signature(); @@ -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(), }) } @@ -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 { + pub fn parse_external_payload(payload: &str) -> Option<(SubgraphId, Vec)> { #[derive(serde::Deserialize)] #[serde(rename = "HugrExternal")] #[serde(tag = "typ")] struct PartialPayload { pub id: SubgraphId, + pub input_params: Vec, } // Deserialize the payload if it is External, avoiding a full copy to memory // for the other variant. serde_json::from_str::(payload) .ok() - .map(|payload| payload.id) + .map(|payload| (payload.id, payload.input_params)) } /// Returns `true` if a string encodes an [`OpaqueSubgraphPayload`].