Skip to content

Commit ad2a6c0

Browse files
committed
Test standalone circ roundtrip with unsupported graph, and test errors
1 parent 003b830 commit ad2a6c0

File tree

8 files changed

+126
-26
lines changed

8 files changed

+126
-26
lines changed

tket/src/passes/pytket.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use derive_more::{Display, Error, From};
77
use hugr::{HugrView, Node};
88
use itertools::Itertools;
99

10-
use crate::serialize::pytket::OpConvertError;
10+
use crate::serialize::pytket::PytketEncodeOpError;
1111
use crate::Circuit;
1212

1313
use super::find_tuple_unpack_rewrites;
@@ -37,7 +37,7 @@ pub enum PytketLoweringError {
3737
/// An error occurred during the conversion of an operation.
3838
#[display("operation conversion error: {_0}")]
3939
#[from]
40-
OpConversionError(OpConvertError),
40+
OpConversionError(PytketEncodeOpError),
4141
/// The circuit is not fully-contained in a region.
4242
/// Function calls are not supported.
4343
#[display("Non-local operations found. Function calls are not supported.")]

tket/src/serialize/pytket.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ pub use config::{
1515
TypeTranslatorSet,
1616
};
1717
pub use encoder::PytketEncoderContext;
18-
pub use error::{OpConvertError, PytketDecodeError, PytketDecodeErrorInner, PytketEncodeError};
18+
pub use error::{
19+
PytketDecodeError, PytketDecodeErrorInner, PytketEncodeError, PytketEncodeOpError,
20+
};
1921
pub use extension::PytketEmitter;
2022
pub use options::{DecodeInsertionTarget, DecodeOptions, EncodeOptions};
2123

tket/src/serialize/pytket/encoder.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use unsupported_tracker::UnsupportedTracker;
2727

2828
use super::opaque::OpaqueSubgraphs;
2929
use super::{
30-
OpConvertError, PytketEncodeError, METADATA_OPGROUP, METADATA_PHASE, METADATA_Q_REGISTERS,
30+
PytketEncodeError, PytketEncodeOpError, METADATA_OPGROUP, METADATA_PHASE, METADATA_Q_REGISTERS,
3131
};
3232
use crate::circuit::Circuit;
3333
use crate::serialize::pytket::circuit::EncodedCircuitInfo;
@@ -313,7 +313,7 @@ impl<H: HugrView> PytketEncoderContext<H> {
313313
return self.get_wire_values(wire, circ);
314314
}
315315

316-
Err(OpConvertError::WireHasNoValues { wire }.into())
316+
Err(PytketEncodeOpError::WireHasNoValues { wire }.into())
317317
}
318318

319319
/// Given a node in the HUGR, returns all the [`TrackedValue`]s associated
@@ -378,7 +378,7 @@ impl<H: HugrView> PytketEncoderContext<H> {
378378

379379
match self.get_wire_values(wire, circ) {
380380
Ok(values) => tracked_values.extend(values.iter().copied()),
381-
Err(PytketEncodeError::OpConversionError(OpConvertError::WireHasNoValues {
381+
Err(PytketEncodeError::OpEncoding(PytketEncodeOpError::WireHasNoValues {
382382
wire,
383383
})) => unknown_values.push(wire),
384384
Err(e) => panic!(
@@ -1186,7 +1186,7 @@ impl<N: HugrNode> NodeInputValues<N> {
11861186
pub fn try_into_tracked_values(self) -> Result<TrackedValues, PytketEncodeError<N>> {
11871187
match self.unknown_values.is_empty() {
11881188
true => Ok(self.tracked_values),
1189-
false => Err(OpConvertError::WireHasNoValues {
1189+
false => Err(PytketEncodeOpError::WireHasNoValues {
11901190
wire: self.unknown_values[0],
11911191
}
11921192
.into()),

tket/src/serialize/pytket/encoder/value_tracker.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use tket_json_rs::register::ElementId as RegisterUnit;
2222
use crate::circuit::Circuit;
2323
use crate::serialize::pytket::extension::RegisterCount;
2424
use crate::serialize::pytket::{
25-
OpConvertError, PytketEncodeError, RegisterHash, METADATA_B_REGISTERS,
25+
PytketEncodeError, PytketEncodeOpError, RegisterHash, METADATA_B_REGISTERS,
2626
METADATA_INPUT_PARAMETERS,
2727
};
2828

@@ -315,7 +315,7 @@ impl<N: HugrNode> ValueTracker<N> {
315315
wire: Wire<N>,
316316
values: impl IntoIterator<Item = Val>,
317317
circ: &Circuit<impl HugrView<Node = N>>,
318-
) -> Result<(), OpConvertError<N>> {
318+
) -> Result<(), PytketEncodeOpError<N>> {
319319
let values = values.into_iter().map(|v| v.into()).collect_vec();
320320

321321
// Remove any qubit/bit used here from the unused set.
@@ -337,7 +337,7 @@ impl<N: HugrNode> ValueTracker<N> {
337337
unexplored_neighbours,
338338
};
339339
if self.wires.insert(wire, tracked).is_some() {
340-
return Err(OpConvertError::WireAlreadyHasValues { wire });
340+
return Err(PytketEncodeOpError::WireAlreadyHasValues { wire });
341341
}
342342

343343
if unexplored_neighbours == 0 {
@@ -429,7 +429,7 @@ impl<N: HugrNode> ValueTracker<N> {
429429
self,
430430
circ: &Circuit<impl HugrView<Node = N>>,
431431
region: N,
432-
) -> Result<ValueTrackerResult, OpConvertError<N>> {
432+
) -> Result<ValueTrackerResult, PytketEncodeOpError<N>> {
433433
let output_node = circ.hugr().get_io(region).unwrap()[1];
434434

435435
// Ordered list of qubits and bits at the output of the circuit.
@@ -440,7 +440,7 @@ impl<N: HugrNode> ValueTracker<N> {
440440
let wire = Wire::new(node, port);
441441
let values = self
442442
.peek_wire_values(wire)
443-
.ok_or_else(|| OpConvertError::WireHasNoValues { wire })?;
443+
.ok_or_else(|| PytketEncodeOpError::WireHasNoValues { wire })?;
444444
for value in values {
445445
match value {
446446
TrackedValue::Qubit(qb) => qubit_outputs.push(self.qubit_register(*qb).clone()),

tket/src/serialize/pytket/error.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::serialize::pytket::opaque::SubgraphId;
1515
#[derive(Display, derive_more::Debug, Error)]
1616
#[non_exhaustive]
1717
#[debug(bounds(N: HugrNode))]
18-
pub enum OpConvertError<N = hugr::Node> {
18+
pub enum PytketEncodeOpError<N: HugrNode = hugr::Node> {
1919
/// Tried to decode a tket1 operation with not enough parameters.
2020
#[display(
2121
"Operation {} is missing encoded parameters. Expected at least {expected} but only \"{}\" were specified.",
@@ -48,7 +48,7 @@ pub enum OpConvertError<N = hugr::Node> {
4848
/// Tried to query the values associated with an unexplored wire.
4949
///
5050
/// This reflects a bug in the operation encoding logic of an operation.
51-
#[display("Could not find values associated with wire {wire}.")]
51+
#[display("Could not find values associated with {wire}.")]
5252
WireHasNoValues {
5353
/// The wire that has no values.
5454
wire: Wire<N>,
@@ -61,13 +61,21 @@ pub enum OpConvertError<N = hugr::Node> {
6161
/// The wire that already has values.
6262
wire: Wire<N>,
6363
},
64+
/// Cannot encode subgraphs with nested structure or non-local edges in an standalone circuit.
65+
#[display("Cannot encode subgraphs with nested structure or non-local edges in an standalone circuit. Unsupported nodes: {}",
66+
nodes.iter().join(", "),
67+
)]
68+
UnsupportedStandaloneSubgraph {
69+
/// The nodes that are part of the unsupported subgraph.
70+
nodes: Vec<N>,
71+
},
6472
}
6573

6674
/// Error type for conversion between tket ops and pytket operations.
6775
#[derive(derive_more::Debug, Display, Error, From)]
6876
#[non_exhaustive]
6977
#[debug(bounds(N: HugrNode))]
70-
pub enum PytketEncodeError<N = hugr::Node> {
78+
pub enum PytketEncodeError<N: HugrNode = hugr::Node> {
7179
/// Tried to encode a non-dataflow region.
7280
#[display("Cannot encode non-dataflow region at {region} with type {optype}.")]
7381
NonDataflowRegion {
@@ -78,7 +86,7 @@ pub enum PytketEncodeError<N = hugr::Node> {
7886
},
7987
/// Operation conversion error.
8088
#[from]
81-
OpConversionError(OpConvertError<N>),
89+
OpEncoding(PytketEncodeOpError<N>),
8290
/// Custom user-defined error raised while encoding an operation.
8391
#[display("Error while encoding operation: {msg}")]
8492
CustomError {
@@ -98,7 +106,7 @@ pub enum PytketEncodeError<N = hugr::Node> {
98106
UnsupportedSubgraphHasNoRegisters {},
99107
}
100108

101-
impl<N> PytketEncodeError<N> {
109+
impl<N: HugrNode> PytketEncodeError<N> {
102110
/// Create a new error with a custom message.
103111
pub fn custom(msg: impl ToString) -> Self {
104112
Self::CustomError {

tket/src/serialize/pytket/opaque.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ impl<N: HugrNode> OpaqueSubgraphs<N> {
138138
return Err(PytketEncodeError::custom(format!("Barrier operation with opgroup {OPGROUP_OPAQUE_HUGR} points to an unknown subgraph: {subgraph_id}")));
139139
}
140140

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

144144
Ok(())
@@ -171,7 +171,9 @@ impl<N> Default for OpaqueSubgraphs<N> {
171171
/// # Errors
172172
///
173173
/// Returns an error if the payload is invalid.
174-
fn parse_external_payload<N>(payload: &str) -> Result<Option<SubgraphId>, PytketEncodeError<N>> {
174+
fn parse_external_payload<N: HugrNode>(
175+
payload: &str,
176+
) -> Result<Option<SubgraphId>, PytketEncodeError<N>> {
175177
// Check if the payload is inline, without fully copying it to memory.
176178
#[derive(serde::Deserialize)]
177179
struct PartialPayload {

tket/src/serialize/pytket/opaque/payload.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ use hugr::package::Package;
99
use hugr::types::Type;
1010
use hugr::{HugrView, Wire};
1111

12-
use crate::serialize::pytket::{PytketDecodeError, PytketDecodeErrorInner};
12+
use crate::serialize::pytket::{
13+
PytketDecodeError, PytketDecodeErrorInner, PytketEncodeError, PytketEncodeOpError,
14+
};
1315

1416
use super::SubgraphId;
1517

@@ -108,12 +110,28 @@ impl OpaqueSubgraphPayload {
108110
/// Create a new payload for an opaque subgraph in the Hugr.
109111
///
110112
/// Encodes the subgraph into a hugr envelope.
113+
///
114+
/// # Errors
115+
///
116+
/// Returns an error if a node in the subgraph has children or non-local const edges.
111117
pub fn new_inline<N: HugrNode>(
112118
subgraph: &SiblingSubgraph<N>,
113119
hugr: &impl HugrView<Node = N>,
114-
) -> Self {
120+
) -> Result<Self, PytketEncodeError<N>> {
115121
let signature = subgraph.signature(hugr);
116122

123+
if !subgraph.function_calls().is_empty()
124+
|| subgraph
125+
.nodes()
126+
.iter()
127+
.any(|n| hugr.children(*n).next().is_some())
128+
{
129+
return Err(PytketEncodeOpError::UnsupportedStandaloneSubgraph {
130+
nodes: subgraph.nodes().to_vec(),
131+
}
132+
.into());
133+
}
134+
117135
let mut inputs = Vec::with_capacity(subgraph.incoming_ports().iter().map(Vec::len).sum());
118136
for subgraph_inputs in subgraph.incoming_ports() {
119137
let Some((inp_node, inp_port0)) = subgraph_inputs.first() else {
@@ -135,11 +153,11 @@ impl OpaqueSubgraphPayload {
135153
.store_str(EnvelopeConfig::text())
136154
.unwrap();
137155

138-
Self::Inline {
156+
Ok(Self::Inline {
139157
hugr_envelope,
140158
inputs: signature.input().iter().cloned().zip(inputs).collect(),
141159
outputs: signature.output().iter().cloned().zip(outputs).collect(),
142-
}
160+
})
143161
}
144162

145163
/// Load a payload encoded in a json string.

tket/src/serialize/pytket/tests.rs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use hugr::builder::{
77
Container, Dataflow, DataflowHugr, DataflowSubContainer, FunctionBuilder, HugrBuilder,
88
ModuleBuilder, SubContainer,
99
};
10-
use hugr::extension::prelude::{bool_t, qb_t};
10+
use hugr::extension::prelude::{bool_t, option_type, qb_t, UnwrapBuilder};
1111

1212
use hugr::hugr::hugrmut::HugrMut;
1313
use hugr::ops::handle::FuncID;
14-
use hugr::ops::{OpParent, Value};
14+
use hugr::ops::{OpParent, OpType, Value};
1515
use hugr::std_extensions::arithmetic::float_ops::FloatOps;
1616
use hugr::types::Signature;
1717
use hugr::HugrView;
@@ -27,8 +27,9 @@ use crate::extension::bool::BoolOp;
2727
use crate::extension::rotation::{rotation_type, ConstRotation, RotationOp};
2828
use crate::extension::sympy::SympyOpDef;
2929
use crate::extension::TKET1_EXTENSION_ID;
30+
use crate::serialize::pytket::extension::OpaqueTk1Op;
3031
use crate::serialize::pytket::{
31-
DecodeInsertionTarget, DecodeOptions, EncodeOptions, EncodedCircuit,
32+
DecodeInsertionTarget, DecodeOptions, EncodeOptions, EncodedCircuit, PytketEncodeOpError,
3233
};
3334
use crate::TketOp;
3435

@@ -277,6 +278,51 @@ fn circ_parameterized() -> Circuit {
277278
hugr.into()
278279
}
279280

281+
/// A circuit with a nested unsupported operation.
282+
///
283+
/// Tries to allocate a qubit, and panics if it fails.
284+
/// This creates an unsupported conditional inside the region.
285+
#[fixture]
286+
fn circ_flat_opaque() -> Circuit {
287+
let input_t = vec![qb_t(), qb_t()];
288+
let output_t = vec![qb_t(), qb_t()];
289+
let mut h = FunctionBuilder::new("flat_opaque", Signature::new(input_t, output_t)).unwrap();
290+
291+
let [q1, q2] = h.input_wires_arr();
292+
293+
// An unsupported tk1-only operation.
294+
let mut tk1op = tket_json_rs::circuit_json::Operation::default();
295+
tk1op.op_type = tket_json_rs::optype::OpType::CH;
296+
tk1op.n_qb = Some(2);
297+
let op: OpType = OpaqueTk1Op::new_from_op(&tk1op, 2, 0)
298+
.as_extension_op()
299+
.into();
300+
let [q1, q2] = h.add_dataflow_op(op, [q1, q2]).unwrap().outputs_arr();
301+
302+
let hugr = h.finish_hugr_with_outputs([q1, q2]).unwrap();
303+
hugr.into()
304+
}
305+
306+
/// A circuit with a nested unsupported operation.
307+
///
308+
/// Tries to allocate a qubit, and panics if it fails.
309+
/// This creates an unsupported conditional inside the region.
310+
#[fixture]
311+
fn circ_nested_opaque() -> Circuit {
312+
let input_t = vec![];
313+
let output_t = vec![qb_t()];
314+
let mut h = FunctionBuilder::new("nested_opaque", Signature::new(input_t, output_t)).unwrap();
315+
316+
let [maybe_q] = h
317+
.add_dataflow_op(TketOp::TryQAlloc, [])
318+
.unwrap()
319+
.outputs_arr();
320+
let [q] = h.build_unwrap_sum(1, option_type(qb_t()), maybe_q).unwrap();
321+
322+
let hugr = h.finish_hugr_with_outputs([q]).unwrap();
323+
hugr.into()
324+
}
325+
280326
/// A circuit with a recursive function call.
281327
#[fixture]
282328
fn circ_recursive() -> Circuit {
@@ -606,6 +652,7 @@ fn json_file_roundtrip(#[case] circ: impl AsRef<std::path::Path>) {
606652
#[case::preset_qubits(circ_preset_qubits(), 1)]
607653
#[case::preset_parameterized(circ_parameterized(), 1)]
608654
#[case::nested_dfgs(circ_nested_dfgs(), 1)]
655+
#[case::flat_opaque(circ_flat_opaque(), 1)]
609656
fn circuit_standalone_roundtrip(#[case] circ: Circuit, #[case] num_circuits: usize) {
610657
let circ_signature = circ.circuit_signature().into_owned();
611658

@@ -639,12 +686,35 @@ fn circuit_standalone_roundtrip(#[case] circ: Circuit, #[case] num_circuits: usi
639686
compare_serial_circs(ser, &reser);
640687
}
641688

689+
/// Test that more complex unsupported subgraphs (nested structure, non-local edges) are rejected when encoding a standalone circuit.
690+
#[rstest]
691+
//#[case::nested_opaque(circ_nested_opaque())] TODO: Raises a different error
692+
#[case::global_defs(circ_global_defs())]
693+
#[case::recursive(circ_recursive())]
694+
fn test_complex_unsupported_subgraphs(#[case] circ: Circuit) {
695+
use cool_asserts::assert_matches;
696+
697+
use crate::serialize::pytket::PytketEncodeError;
698+
699+
println!("{}", circ.mermaid_string());
700+
701+
let try_encoded = EncodedCircuit::new_standalone(&circ, EncodeOptions::new());
702+
assert_matches!(
703+
try_encoded,
704+
Err(PytketEncodeError::OpEncoding(
705+
PytketEncodeOpError::UnsupportedStandaloneSubgraph { .. }
706+
))
707+
);
708+
}
709+
642710
/// Test the serialisation roundtrip from a tket circuit into an EncodedCircuit and back.
643711
#[rstest]
644712
#[case::meas_ancilla(circ_measure_ancilla(), 1)]
645713
#[case::preset_qubits(circ_preset_qubits(), 1)]
646714
#[case::preset_parameterized(circ_parameterized(), 1)]
647715
#[case::nested_dfgs(circ_nested_dfgs(), 1)]
716+
#[case::flat_opaque(circ_flat_opaque(), 1)]
717+
//#[case::nested_opaque(circ_nested_opaque(), 1)] TODO: Raises a different error
648718
#[case::global_defs(circ_global_defs(), 1)]
649719
#[case::recursive(circ_recursive(), 1)]
650720
// TODO: fix edge case: non-local edge from an unsupported node inside a nested CircBox

0 commit comments

Comments
 (0)