Skip to content

Commit 4fc5a25

Browse files
authored
fix: Incorrect matching of pytket parameters to unsupported graph ports (#1561)
Fixes #1516 There was a problem in how we match pytket command parameters that reference a unsupported subgraph output port. The encoder generated the name using a sequential id for each port, but the decoder used the `PortIndex` instead. Fixed the bug, added the issue repro as a guppy example, and made an LLM create a reduced rust test case. Note that while this is a needed bug fix, there's a bigger problem that I'm not fixing in this PR: Due to how pytket circuits use parameters, each time we find a new unknown variable name the decoder adds it as a new angle input to the region being decoded. This is useful when decoding user-generated pytket circuits, but in general if the pytket circuit was encoded from a Hugr region there shouldn't be any new inputs added. These new inputs end up making the Hugr invalid instead. There's different ways we could avoid this, but I'll do that in a follow up PR.
1 parent 54e855f commit 4fc5a25

5 files changed

Lines changed: 101 additions & 5 deletions

File tree

4.76 KB
Binary file not shown.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# /// script
2+
# requires-python = ">=3.14"
3+
# dependencies = [
4+
# "guppylang==0.21.13",
5+
# ]
6+
# ///
7+
8+
from pathlib import Path
9+
from sys import argv
10+
11+
from guppylang import guppy
12+
from guppylang.std.builtins import owned
13+
from guppylang.std.quantum import qubit
14+
from guppylang.std.quantum.functional import ch
15+
16+
17+
@guppy
18+
def test(q1: qubit @ owned, q2: qubit @ owned) -> tuple[qubit, qubit]:
19+
q1, q2 = ch(q1, q2)
20+
return (q1, q2)
21+
22+
23+
program = test.compile_function()
24+
Path(argv[0]).with_suffix(".hugr").write_bytes(program.to_bytes())

tket-py/test/test_pass.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
inline_funcs,
1212
NormalizeGuppy,
1313
ModifierResolverPass,
14+
GlobalScope,
1415
)
1516
from tket._state import CompilationState
1617
from tket_exts import tket_registry
@@ -21,7 +22,12 @@
2122
from hypothesis import given, settings
2223

2324
from tket.passes import PytketHugrPass
24-
from pytket.passes import CliffordSimp, SquashRzPhasedX, SequencePass
25+
from pytket.passes import (
26+
CliffordSimp,
27+
SquashRzPhasedX,
28+
RemoveRedundancies,
29+
SequencePass,
30+
)
2531
from hugr.build.base import Hugr
2632

2733
import numpy as np
@@ -297,3 +303,23 @@ def test_inline_functions() -> None:
297303
all = InlineFunctions(heuristic=inline_funcs.All())(hugr)
298304

299305
assert _count_ops(all, "Call") == 0
306+
307+
308+
def test_issue_1516() -> None:
309+
"""Regression test for issue 1516.
310+
311+
This was caused by a bug in the decoder that injected new parameter inputs when decoding a modified pytket circuit back into an existing region.
312+
313+
<https://github.com/quantinuum/tket2/issues/1516>
314+
"""
315+
hugr = _hugr_from_path("test_files/guppy_examples/issue_1516.hugr")
316+
317+
# Ensure that the hugr is valid before we start.
318+
CompilationState.from_python(hugr).validate()
319+
320+
opt = PytketHugrPass(RemoveRedundancies()).with_scope(
321+
GlobalScope.PRESERVE_ENTRYPOINT
322+
)
323+
opt_hugr = opt(hugr, inplace=False)
324+
325+
CompilationState.from_python(opt_hugr).validate()

tket/src/serialize/pytket/decoder/subgraph.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,12 @@ impl<'h> PytketDecoderContext<'h> {
190190

191191
let mut output_qubits = qubits;
192192
let mut output_bits = bits;
193+
let mut output_param_count = 0;
193194

194-
for ((out_idx, ty), (src, src_port)) in subgraph
195+
for (ty, (src, src_port)) in subgraph
195196
.signature()
196197
.output()
197198
.iter()
198-
.enumerate()
199199
.zip_eq(subgraph.outgoing_ports())
200200
{
201201
// Output wire from the subgraph. Depending on the type, we may need
@@ -225,7 +225,10 @@ impl<'h> PytketDecoderContext<'h> {
225225
wire_bits.unwrap().iter().cloned(),
226226
)?;
227227
} else if PARAMETER_TYPES.contains(ty) {
228-
let param_name = id.output_parameter(out_idx);
228+
// The encoder names opaque parameter outputs by the number of
229+
// parameter values exposed to pytket.
230+
let param_name = id.output_parameter(output_param_count);
231+
output_param_count += 1;
229232
let param = if ty == &rotation_type() {
230233
LoadedParameter::rotation(wire)
231234
} else {

tket/src/serialize/pytket/tests.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use hugr::hugr::hugrmut::HugrMut;
3131
use hugr::ops::handle::FuncID;
3232
use hugr::ops::{OpParent, OpType, Value};
3333
use hugr::std_extensions::arithmetic::float_ops::FloatOps;
34-
use hugr::types::{Signature, SumType};
34+
use hugr::types::{Signature, SumType, Type};
3535
use hugr::{Hugr, HugrView};
3636
use itertools::Itertools;
3737
use rstest::{fixture, rstest};
@@ -792,6 +792,44 @@ fn circ_complex_param_type() -> Hugr {
792792
h.finish_hugr_with_outputs([float_tuple]).unwrap()
793793
}
794794

795+
/// A circuit with an unsupported subgraph whose first output is not exposed as
796+
/// a pytket parameter, followed by a float output used by a supported gate.
797+
///
798+
/// Regression test for <https://github.com/Quantinuum/tket2/issues/1516>
799+
#[fixture]
800+
fn circ_unsupported_subgraph_skipped_output_before_param() -> Hugr {
801+
let tuple_float_t = Type::from(SumType::new_tuple(vec![float64_type()]));
802+
let input_t = vec![qb_t()];
803+
let output_t = vec![qb_t(), tuple_float_t.clone()];
804+
let mut h = FunctionBuilder::new(
805+
"unsupported_subgraph_skipped_output_before_param",
806+
Signature::new(input_t, output_t),
807+
)
808+
.unwrap();
809+
let [q] = h.input_wires_arr();
810+
811+
let func = h
812+
.module_root_builder()
813+
.declare(
814+
"tuple_and_float",
815+
Signature::new(vec![], vec![tuple_float_t, float64_type()]).into(),
816+
)
817+
.unwrap();
818+
819+
let call = h.call(&func, &[], []).unwrap();
820+
let [float_tuple, angle] = call.outputs_arr();
821+
let [angle] = h
822+
.add_dataflow_op(RotationOp::from_halfturns_unchecked, [angle])
823+
.unwrap()
824+
.outputs_arr();
825+
let [q] = h
826+
.add_dataflow_op(TketOp::Ry, [q, angle])
827+
.unwrap()
828+
.outputs_arr();
829+
830+
h.finish_hugr_with_outputs([q, float_tuple]).unwrap()
831+
}
832+
795833
/// A program with two unsupported subgraphs not associated to any qubit or bit.
796834
/// <https://github.com/CQCL/tket2/issues/1294>
797835
#[fixture]
@@ -1112,6 +1150,11 @@ fn fail_on_modified_hugr(circ_tk1_ops: Hugr) {
11121150
#[case::order_edge(circ_order_edge(), 1, CircuitRoundtripTestConfig::Default)]
11131151
#[case::bool_conversion(circ_bool_conversion(), 1, CircuitRoundtripTestConfig::Default)]
11141152
#[case::complex_param_type(circ_complex_param_type(), 1, CircuitRoundtripTestConfig::Default)]
1153+
#[case::unsupported_subgraph_skipped_output_before_param(
1154+
circ_unsupported_subgraph_skipped_output_before_param(),
1155+
1,
1156+
CircuitRoundtripTestConfig::Default
1157+
)]
11151158
// TODO: We need to track [`EncodedCircuitInfo`] for nested CircBoxes too. We
11161159
// have temporarily disabled encoding of DFG and function calls as CircBoxes to
11171160
// avoid an error here.

0 commit comments

Comments
 (0)