diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d913d1..77841982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Development Tooling**: Use uv as the default dependency manager for local development, CI, documentation builds, and publishing workflows. - **Graph State API**: Replaced legacy `physical_*` graph methods/properties with standard graph-style `nodes`, `edges`, `add_node()`, `add_edge()`, `remove_node()`, `remove_edge()`, and count/query helpers. +- **Pattern Simulator**: Materialize pending output Pauli-frame corrections when returning output statevectors or explicit output measurement results. + +### Removed + +- **Pattern Commands**: Removed explicit `graphqomb.command.X` and `graphqomb.command.Z` correction commands. Output corrections are now represented by `PauliFrame` only, and `.ptn` files containing `X`/`Z` command lines are rejected. ## [0.3.1] - 2026-05-17 diff --git a/docs/source/architecture.rst b/docs/source/architecture.rst index bd73c7c2..3603814b 100644 --- a/docs/source/architecture.rst +++ b/docs/source/architecture.rst @@ -28,7 +28,9 @@ The lowered pattern combines: - a :class:`graphqomb.pauli_frame.PauliFrame` used for dependency tracking, - derived metrics such as depth, space usage, and active volume. -Most scheduled work is serialized as prepare, entangle, and measure commands separated by ``TICK`` slice boundaries. In the current implementation, terminal ``X``/``Z`` correction commands may also remain for unmeasured outputs, so the executable command stream is close to, but not strictly limited to, ``N``, ``E``, ``M``, and ``TICK``. +Most scheduled work is serialized as prepare, entangle, and measure commands separated by ``TICK`` slice boundaries. +Pauli corrections are retained in the :class:`graphqomb.pauli_frame.PauliFrame` rather than emitted as ``X``/``Z`` commands, so the executable command stream is limited to ``N``, ``E``, ``M``, and ``TICK``. +The pattern simulator materializes pending output-frame corrections when returning an output statevector or an explicit output measurement result. Feedforward and causality ------------------------- diff --git a/docs/source/command.rst b/docs/source/command.rst index 34488c59..e853918e 100644 --- a/docs/source/command.rst +++ b/docs/source/command.rst @@ -18,12 +18,6 @@ Command Classes .. autoclass:: graphqomb.command.M :members: -.. autoclass:: graphqomb.command.X - :members: - -.. autoclass:: graphqomb.command.Z - :members: - .. autoclass:: graphqomb.command.TICK :members: diff --git a/graphqomb/command.py b/graphqomb/command.py index 77c1bfe8..8f793fe2 100644 --- a/graphqomb/command.py +++ b/graphqomb/command.py @@ -5,8 +5,6 @@ - `N`: Preparation command. - `E`: Entanglement command. - `M`: Measurement command. -- `X`: X correction command. -- `Z`: Z correction command. - `TICK`: Time slice separator command. - `Command`: Type alias of all commands. """ @@ -76,39 +74,6 @@ def __str__(self) -> str: return f"M: node={self.node}, plane={self.meas_basis.plane}, angle={self.meas_basis.angle}" -@dataclasses.dataclass -class _Correction: - node: int - - -@dataclasses.dataclass -class X(_Correction): - """X correction command. - - Attributes - ---------- - node : `int` - The node index to apply the correction. - """ - - def __str__(self) -> str: - return f"X: node={self.node}" - - -@dataclasses.dataclass -class Z(_Correction): - """Z correction command. - - Attributes - ---------- - node : `int` - The node index to apply the correction. - """ - - def __str__(self) -> str: - return f"Z: node={self.node}" - - @dataclasses.dataclass class TICK: """Time slice separator command. @@ -121,4 +86,4 @@ def __str__(self) -> str: return "TICK" -Command = N | E | M | X | Z | TICK +Command = N | E | M | TICK diff --git a/graphqomb/pattern.py b/graphqomb/pattern.py index 55cea450..259b157f 100644 --- a/graphqomb/pattern.py +++ b/graphqomb/pattern.py @@ -15,7 +15,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING -from graphqomb.command import TICK, Command, E, M, N, X, Z +from graphqomb.command import TICK, Command, E, M, N if TYPE_CHECKING: from collections.abc import Callable, Iterator @@ -162,7 +162,7 @@ def idle_times(self) -> dict[int, int]: idle_times[cmd.node] = current_time - prepared_time[cmd.node] for output_node in self.output_node_indices: - if output_node in prepared_time: + if output_node in prepared_time and output_node not in idle_times: idle_times[output_node] = current_time - prepared_time[output_node] return idle_times @@ -253,7 +253,7 @@ def _ensure_no_operations_on_measured_qubits(pattern: Pattern) -> None: if not set(cmd.nodes).isdisjoint(measured): msg = f"Entanglement operation targets a measured qubit: {cmd}" raise ValueError(msg) - elif isinstance(cmd, (N, X, Z)): + elif isinstance(cmd, N): if cmd.node in measured: msg = f"Operation on a measured qubit: {cmd}" raise ValueError(msg) @@ -286,7 +286,7 @@ def _ensure_no_unprepared_qubit_operations(pattern: Pattern) -> None: if cmd.nodes[0] not in prepared or cmd.nodes[1] not in prepared: msg = f"Entanglement operation targets a qubit that hasn't been prepared yet: {cmd}" raise ValueError(msg) - elif isinstance(cmd, (M, X, Z)) and cmd.node not in prepared: + elif isinstance(cmd, M) and cmd.node not in prepared: msg = f"Operation on a qubit that hasn't been prepared yet: {cmd}" raise ValueError(msg) elif isinstance(cmd, TICK): @@ -295,7 +295,7 @@ def _ensure_no_unprepared_qubit_operations(pattern: Pattern) -> None: def _ensure_measurement_consistency(pattern: Pattern) -> None: - """Ensure that measurements are applied exactly to all non-output qubits and that no output qubit is ever measured. + """Ensure that measurements are applied exactly to all non-output qubits. Parameters ---------- @@ -305,7 +305,7 @@ def _ensure_measurement_consistency(pattern: Pattern) -> None: Raises ------ ValueError - If a measurement targets an output qubit, or if some non-output qubits are never measured. + If some non-output qubits are never measured. """ output_nodes = set(pattern.output_node_indices) non_output_nodes = ( @@ -314,11 +314,8 @@ def _ensure_measurement_consistency(pattern: Pattern) -> None: measured: set[int] = set() for cmd in pattern: if isinstance(cmd, M): - if cmd.node in output_nodes: - msg = f"The command measures an output qubit: {cmd}" - raise ValueError(msg) measured.add(cmd.node) - if measured != non_output_nodes: + if not non_output_nodes.issubset(measured): missing = non_output_nodes - measured msg = f"Missing measurements on qubit(s): {sorted(missing)}" raise ValueError(msg) @@ -346,7 +343,7 @@ def print_pattern( """ def identity_filter(cmd: Command) -> Command | None: - return cmd if isinstance(cmd, (N, E, M, X, Z, TICK)) else None + return cmd if isinstance(cmd, (N, E, M, TICK)) else None if cmd_filter is None: cmd_filter = identity_filter diff --git a/graphqomb/ptn_format.py b/graphqomb/ptn_format.py index d9dbf3f4..8765e667 100644 --- a/graphqomb/ptn_format.py +++ b/graphqomb/ptn_format.py @@ -19,7 +19,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING -from graphqomb.command import TICK, Command, E, M, N, X, Z +from graphqomb.command import TICK, Command, E, M, N from graphqomb.common import ( Axis, AxisMeasBasis, @@ -201,10 +201,6 @@ def _write_command(out: StringIO, cmd: Command) -> None: out.write(f"E {cmd.nodes[0]} {cmd.nodes[1]}\n") elif isinstance(cmd, M): _write_measurement(out, cmd) - elif isinstance(cmd, X): - out.write(f"X {cmd.node}\n") - elif isinstance(cmd, Z): - out.write(f"Z {cmd.node}\n") def _is_positive_pauli_measurement(meas_basis: MeasBasis, pauli_axis: Axis) -> bool: @@ -612,7 +608,7 @@ def _command_nodes(cmd: Command) -> set[int]: `set`\[`int`\] Node ids referenced by the command. """ - if isinstance(cmd, (N, M, X, Z)): + if isinstance(cmd, (N, M)): return {cmd.node} if isinstance(cmd, E): return set(cmd.nodes) @@ -848,16 +844,6 @@ def _parse_command(self, line: str) -> None: self._parse_e_command(parts) elif cmd_type == "M": self._parse_m_command(parts) - elif cmd_type == "X": - if len(parts) != 2: # noqa: PLR2004 - msg = "X command requires exactly one node" - raise ValueError(msg) - self.result.commands.append(X(node=_parse_int(parts[1], "node"))) - elif cmd_type == "Z": - if len(parts) != 2: # noqa: PLR2004 - msg = "Z command requires exactly one node" - raise ValueError(msg) - self.result.commands.append(Z(node=_parse_int(parts[1], "node"))) else: msg = f"Unknown command: {cmd_type}" raise ValueError(msg) diff --git a/graphqomb/qompiler.py b/graphqomb/qompiler.py index 198c283f..d49dec39 100644 --- a/graphqomb/qompiler.py +++ b/graphqomb/qompiler.py @@ -12,7 +12,7 @@ from graphlib import TopologicalSorter from typing import TYPE_CHECKING -from graphqomb.command import TICK, Command, E, M, N, X, Z +from graphqomb.command import TICK, Command, E, M, N from graphqomb.feedforward import check_flow, dag_from_flow from graphqomb.graphstate import odd_neighbors from graphqomb.pattern import Pattern @@ -138,11 +138,7 @@ def _qompile( # Insert TICK between time slices commands.append(TICK()) - for node in graph.output_node_indices: - if meas_basis := graph.meas_bases.get(node): - commands.append(M(node, meas_basis)) - else: - commands.extend((X(node=node), Z(node=node))) + commands.extend(M(node, meas_bases[node]) for node in graph.output_node_indices if node in meas_bases) # Collect input node coordinates input_coords = {node: graph_coords[node] for node in graph.input_node_indices if node in graph_coords} diff --git a/graphqomb/simulator.py b/graphqomb/simulator.py index f2c9a88f..559bc2ec 100644 --- a/graphqomb/simulator.py +++ b/graphqomb/simulator.py @@ -15,7 +15,7 @@ import numpy as np -from graphqomb.command import TICK, E, M, N, X, Z +from graphqomb.command import TICK, E, M, N from graphqomb.common import MeasBasis, Plane from graphqomb.gates import MultiGate, SingleGate, TwoQubitGate from graphqomb.pattern import is_runnable @@ -29,6 +29,9 @@ from graphqomb.pattern import Pattern from graphqomb.simulator_backend import BaseFullStateSimulator +_X_MATRIX = np.asarray([[0, 1], [1, 0]], dtype=np.complex128) +_Z_MATRIX = np.asarray([[1, 0], [0, -1]], dtype=np.complex128) + class SimulatorBackend(Enum): """Enum class for circuit simulator backend. @@ -112,6 +115,8 @@ class PatternSimulator: The list of node indices in the pattern. results : `dict`\[`int`, `bool`\] The measurement results for each node. + output_results : `dict`\[`int`, `bool`\] + Measurement results for output nodes, keyed by logical output index. calc_prob : `bool` Whether to calculate probabilities. """ @@ -119,6 +124,7 @@ class PatternSimulator: state: BaseFullStateSimulator node_indices: list[int] results: dict[int, bool] + output_results: dict[int, bool] calc_prob: bool __pattern: Pattern @@ -131,6 +137,7 @@ def __init__( ) -> None: self.node_indices = list(pattern.input_node_indices.keys()) self.results = {} + self.output_results = {} self.calc_prob = calc_prob self.__pattern = pattern @@ -194,32 +201,52 @@ def _updated_measurement_basis(self, cmd: M) -> MeasBasis: return basis + def _sample_measurement_result( + self, + node_id: int, + meas_basis: MeasBasis, + rng: np.random.Generator, + ) -> bool: + state = self.state.state() + norm_sq = float(np.real(np.vdot(state, state))) + basis_vector = meas_basis.vector() + projected = np.tensordot(basis_vector.conjugate(), state, axes=(0, node_id)) + prob_false = float(np.real(np.vdot(projected, projected)) / norm_sq) + prob_false = min(1.0, max(0.0, prob_false)) + return bool(rng.uniform() >= prob_false) + + def _apply_output_pauli_frame(self, node: int) -> None: + node_id = self.node_indices.index(node) + if self.__pattern.pauli_frame.x_pauli[node]: + self.state.evolve(_X_MATRIX, node_id) + if self.__pattern.pauli_frame.z_pauli[node]: + self.state.evolve(_Z_MATRIX, node_id) + @apply_cmd.register def _(self, cmd: M, *, rng: np.random.Generator) -> None: if self.calc_prob: raise NotImplementedError - result = rng.uniform() < 1 / 2 node_id = self.node_indices.index(cmd.node) - self.state.measure(node_id, self._updated_measurement_basis(cmd), result) + if cmd.node in self.__pattern.output_node_indices: + meas_basis = self._updated_measurement_basis(cmd) + result = self._sample_measurement_result(node_id, meas_basis, rng) + else: + meas_basis = self._updated_measurement_basis(cmd) + result = rng.uniform() < 1 / 2 + + self.state.measure(node_id, meas_basis, result) self.results[cmd.node] = result self.node_indices.remove(cmd.node) + if cmd.node in self.__pattern.output_node_indices: + qindex = self.__pattern.output_node_indices[cmd.node] + self.output_results[qindex] = result + return + if result: self.__pattern.pauli_frame.meas_flip(cmd.node) - @apply_cmd.register - def _(self, cmd: X, *, rng: np.random.Generator) -> None: # noqa: ARG002 - node_id = self.node_indices.index(cmd.node) - if self.__pattern.pauli_frame.x_pauli[cmd.node]: - self.state.evolve(np.asarray([[0, 1], [1, 0]]), node_id) - - @apply_cmd.register - def _(self, cmd: Z, *, rng: np.random.Generator) -> None: # noqa: ARG002 - node_id = self.node_indices.index(cmd.node) - if self.__pattern.pauli_frame.z_pauli[cmd.node]: - self.state.evolve(np.asarray([[1, 0], [0, -1]]), node_id) - @apply_cmd.register def _(self, cmd: TICK, *, rng: np.random.Generator) -> None: # TICK is a time separator that doesn't affect quantum state @@ -240,6 +267,12 @@ def simulate(self, rng: np.random.Generator | None = None) -> None: for cmd in self.__pattern.commands: self.apply_cmd(cmd, rng=rng) + # Only remaining output nodes still have state-vector axes; measured outputs + # remain in output_node_indices but have been removed from node_indices. + for node in self.node_indices: + if node in self.__pattern.output_node_indices: + self._apply_output_pauli_frame(node) + # Create a mapping from current node indices to output node indices permutation = [self.__pattern.output_node_indices[node] for node in self.node_indices] diff --git a/graphqomb/stim_compiler.py b/graphqomb/stim_compiler.py index bcea6f04..db62cd84 100644 --- a/graphqomb/stim_compiler.py +++ b/graphqomb/stim_compiler.py @@ -10,7 +10,7 @@ from io import StringIO from typing import TYPE_CHECKING -from graphqomb.command import TICK, E, M, N, X, Z +from graphqomb.command import TICK, E, M, N from graphqomb.common import Axis, MeasBasis, determine_pauli_axis from graphqomb.noise_model import ( Coordinate, @@ -91,12 +91,6 @@ def _process_commands(self) -> None: self._handle_measure(cmd.node, cmd.meas_basis) elif isinstance(cmd, TICK): self._handle_tick() - elif isinstance(cmd, (X, Z)): - cmd_name = type(cmd).__name__ - msg = ( - f"Unsupported command for stim compilation: {cmd_name}. X/Z correction commands are not supported." - ) - raise NotImplementedError(msg) else: msg = f"Unsupported command for stim compilation: {type(cmd).__name__}" raise TypeError(msg) @@ -275,7 +269,6 @@ def stim_compile( Stim only supports Clifford gates, therefore this compiler only supports Pauli measurements (X, Y, Z basis) which correspond to Clifford operations. Non-Pauli measurements will raise a ValueError. - Patterns containing X or Z correction commands will raise a NotImplementedError. Examples -------- diff --git a/tests/test_pattern.py b/tests/test_pattern.py index 6b83c896..74755240 100644 --- a/tests/test_pattern.py +++ b/tests/test_pattern.py @@ -6,7 +6,7 @@ import pytest -from graphqomb.command import TICK, E, M, N, X, Z +from graphqomb.command import TICK, E, M, N from graphqomb.common import Plane, PlannerMeasBasis from graphqomb.graphstate import GraphState from graphqomb.pattern import Pattern @@ -58,8 +58,6 @@ def test_pattern_depth_counts_tick_commands( E(nodes=(nodes[0], nodes[1])), M(node=nodes[1], meas_basis=meas_basis), TICK(), - X(node=nodes[2]), - Z(node=nodes[2]), ) pattern = Pattern( @@ -82,8 +80,6 @@ def test_pattern_depth_is_zero_without_ticks( N(node=nodes[0]), E(nodes=(nodes[0], nodes[1])), M(node=nodes[1], meas_basis=meas_basis), - X(node=nodes[2]), - Z(node=nodes[2]), ) pattern = Pattern( diff --git a/tests/test_ptn_format.py b/tests/test_ptn_format.py index 47137f71..ad14ec6d 100644 --- a/tests/test_ptn_format.py +++ b/tests/test_ptn_format.py @@ -7,7 +7,7 @@ import pytest -from graphqomb.command import TICK, E, M, N, X, Z +from graphqomb.command import TICK, E, M, N from graphqomb.common import Axis, AxisMeasBasis, Plane, PlannerMeasBasis, Sign, determine_pauli_axis from graphqomb.graphstate import GraphState from graphqomb.pattern import Pattern @@ -83,7 +83,7 @@ def create_measured_output_pattern_with_detector() -> Pattern: return qompile(graph, {in_node: {out_node}}, parity_check_group=[{in_node}]) -def command_signature(cmd: Command) -> tuple[Any, ...]: # noqa: PLR0911 +def command_signature(cmd: Command) -> tuple[Any, ...]: """Return a behavior-level signature for a pattern command.""" if isinstance(cmd, N): return ("N", cmd.node, cmd.coordinate) @@ -94,10 +94,6 @@ def command_signature(cmd: Command) -> tuple[Any, ...]: # noqa: PLR0911 if pauli_axis is not None: return ("M", cmd.node, pauli_axis, cmd.meas_basis.angle) return ("M", cmd.node, cmd.meas_basis.plane, cmd.meas_basis.angle) - if isinstance(cmd, X): - return ("X", cmd.node) - if isinstance(cmd, Z): - return ("Z", cmd.node) if isinstance(cmd, TICK): return ("TICK",) return ("UNKNOWN", type(cmd).__name__) @@ -261,8 +257,6 @@ def test_loads_commands() -> None: E 0 1 M 0 XY 0 M 1 XY pi/2 -X 2 -Z 2 """ result = loads(ptn_str) @@ -272,8 +266,6 @@ def test_loads_commands() -> None: assert any(isinstance(c, N) and c.node == 3 and c.coordinate == (1.0, 2.0) for c in commands) assert any(isinstance(c, E) and c.nodes == (0, 1) for c in commands) assert any(isinstance(c, M) and c.node == 0 for c in commands) - assert any(isinstance(c, X) and c.node == 2 for c in commands) - assert any(isinstance(c, Z) and c.node == 2 for c in commands) def test_loads_timeslices() -> None: @@ -648,9 +640,6 @@ def test_loads_preserves_non_contiguous_node_ids() -> None: M 10 X + [2] M 20 Y - -[3] -X 30 -Z 30 .xflow 10 -> 20 .zflow 20 -> 30 @@ -670,7 +659,6 @@ def test_loads_preserves_non_contiguous_node_ids() -> None: with pytest.raises(NotImplementedError, match="read-only"): result.pauli_frame.graphstate.add_edge(10, 30) assert any(isinstance(cmd, N) and cmd.node == 20 for cmd in result.commands) - assert any(isinstance(cmd, X) and cmd.node == 30 for cmd in result.commands) @pytest.mark.parametrize( @@ -684,6 +672,8 @@ def test_loads_preserves_non_contiguous_node_ids() -> None: (".version 1\n[1]\n[0]\n", "monotonically increasing"), (".version 1\n[0]\nM 0 XY pi/0\n", "denominator"), (".version 1\n.detector\n", "requires at least one node"), + (".version 1\n[0]\nX 0\n", "Unknown command: X"), + (".version 1\n[0]\nZ 0\n", "Unknown command: Z"), ], ) def test_loads_rejects_malformed_input(ptn_str: str, message: str) -> None: diff --git a/tests/test_simulator.py b/tests/test_simulator.py new file mode 100644 index 00000000..ec12f2fc --- /dev/null +++ b/tests/test_simulator.py @@ -0,0 +1,80 @@ +"""Tests for pattern simulation behavior.""" + +from __future__ import annotations + +import numpy as np + +from graphqomb.command import M +from graphqomb.common import Axis, AxisMeasBasis, Sign +from graphqomb.graphstate import GraphState +from graphqomb.pattern import Pattern +from graphqomb.pauli_frame import PauliFrame +from graphqomb.simulator import PatternSimulator, SimulatorBackend +from graphqomb.statevec import StateVector + + +def _single_output_pattern(*, measured: bool, axis: Axis = Axis.Z) -> tuple[Pattern, int]: + graph = GraphState() + node = graph.add_node() + graph.register_input(node, 0) + graph.register_output(node, 0) + + pauli_frame = PauliFrame(graph, xflow={}, zflow={}) + commands = (M(node, AxisMeasBasis(axis, Sign.PLUS)),) if measured else () + pattern = Pattern( + input_node_indices=graph.input_node_indices, + output_node_indices=graph.output_node_indices, + commands=commands, + pauli_frame=pauli_frame, + ) + return pattern, node + + +def test_pattern_simulator_applies_output_x_frame_to_statevector() -> None: + """An unmeasured output statevector should include pending X frame corrections.""" + pattern, node = _single_output_pattern(measured=False) + pattern.pauli_frame.x_flip(node) + simulator = PatternSimulator(pattern, SimulatorBackend.StateVector) + simulator.state = StateVector([1.0, 0.0]) + + simulator.simulate() + + np.testing.assert_allclose(simulator.state.state(), np.asarray([0.0, 1.0])) + + +def test_pattern_simulator_applies_output_z_frame_to_statevector() -> None: + """An unmeasured output statevector should include pending Z frame corrections.""" + pattern, node = _single_output_pattern(measured=False) + pattern.pauli_frame.z_flip(node) + simulator = PatternSimulator(pattern, SimulatorBackend.StateVector) + simulator.state = StateVector([1 / np.sqrt(2), 1 / np.sqrt(2)]) + + simulator.simulate() + + np.testing.assert_allclose(simulator.state.state(), np.asarray([1 / np.sqrt(2), -1 / np.sqrt(2)])) + + +def test_pattern_simulator_measures_output_after_pauli_frame() -> None: + """Output measurement results should be reported after applying the output Pauli frame.""" + pattern, node = _single_output_pattern(measured=True) + pattern.pauli_frame.x_flip(node) + simulator = PatternSimulator(pattern, SimulatorBackend.StateVector) + simulator.state = StateVector([0.0, 1.0]) + + simulator.simulate(rng=np.random.default_rng(123)) + + assert simulator.results == {node: False} + assert simulator.output_results == {0: False} + + +def test_pattern_simulator_measures_output_z_frame_in_x_basis() -> None: + """A pending output Z frame should flip an X-basis output measurement result.""" + pattern, node = _single_output_pattern(measured=True, axis=Axis.X) + pattern.pauli_frame.z_flip(node) + simulator = PatternSimulator(pattern, SimulatorBackend.StateVector) + simulator.state = StateVector([1 / np.sqrt(2), 1 / np.sqrt(2)]) + + simulator.simulate(rng=np.random.default_rng(123)) + + assert simulator.results == {node: True} + assert simulator.output_results == {0: True} diff --git a/tests/test_stim_compiler.py b/tests/test_stim_compiler.py index f3d2102f..116c76e3 100644 --- a/tests/test_stim_compiler.py +++ b/tests/test_stim_compiler.py @@ -7,7 +7,7 @@ import pytest -from graphqomb.command import TICK, E +from graphqomb.command import TICK, E, M, N from graphqomb.common import Axis, AxisMeasBasis, Plane, PlannerMeasBasis, Sign from graphqomb.graphstate import GraphState from graphqomb.noise_model import ( @@ -475,8 +475,8 @@ def test_stim_compile_unsupported_basis() -> None: stim_compile(pattern) -def test_stim_compile_unsupported_output_corrections() -> None: - """Test that X/Z correction commands in a pattern raise NotImplementedError.""" +def test_stim_compile_unmeasured_output_has_no_correction_commands() -> None: + """Unmeasured outputs should compile without terminal correction commands.""" graph = GraphState() in_node = graph.add_node() out_node = graph.add_node() @@ -491,8 +491,13 @@ def test_stim_compile_unsupported_output_corrections() -> None: xflow = {in_node: {out_node}} pattern = qompile(graph, xflow) - with pytest.raises(NotImplementedError, match="X/Z correction commands are not supported"): - stim_compile(pattern) + assert all(isinstance(cmd, (N, E, M, TICK)) for cmd in pattern.commands) + assert all(not (isinstance(cmd, M) and cmd.node == out_node) for cmd in pattern.commands) + + stim_str = stim_compile(pattern) + + assert isinstance(stim_str, str) + assert stim_str def test_stim_compile_empty_pattern() -> None: