Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Hence, occasionally changes will be backwards incompatible (although they will a

## [Unreleased]

### Added
- `pyzx.simplify.drop_orphan_reset_discards`: opt-in cleanup pass that removes the disconnected `boundary -- Z(0) -- X(_rN)` components left behind by `Circuit.to_graph(elide_initial_resets=False)` on circuits with leading resets, matching the elided graph with |0⟩ applied to those inputs (by @dlyongemallo).

### Fixed
- Reset gate representation changed from ground-based to symbolic boolean paradigm, avoiding paradigm-mixing issues that destroyed measurement phases during simplification of circuits with mid-circuit resets. As a side effect, `graph_to_circuit` now recovers conditional X-type rotations (`NOT`, `XPhase`, `SX`), since X-type vertices on the qubit wire are unambiguous now that measurement outcomes are represented as leaves. `Circuit.to_graph` gains an opt-in `elide_initial_resets` flag (default `False`) that skips the discard chain for a `Reset` on an unmodified input wire, useful for circuits with OpenQASM-style implicit |0⟩ inputs. Each `_rN` reset variable is allocated to the lowest name not already in the graph's variable registry to avoid aliasing user-supplied phases, and `graph_to_circuit` excludes vertices on classical-bit wires (e.g. the `Z`/`X` pair from `DiscardBit`) so hybrid graphs round-trip without extra phantom qubits (by @dlyongemallo).

## [0.10.2] - 2026-05-01

## [0.10.1] - 2026-04-28
Expand Down
23 changes: 18 additions & 5 deletions pyzx/circuit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,18 +293,31 @@ def to_graph(
zh:bool=False,
compress_rows:bool=True,
backend:Optional[str]=None,
elide_initial_resets:bool=False,
) -> BaseGraph:
"""Turns the circuit into a ZX-Graph.
If ``compress_rows`` is set, it tries to put single qubit gates on different qubits,
on the same row."""
on the same row.

``elide_initial_resets`` (default False) skips the discard chain
for a ``Reset`` on an unmodified input wire. Defaults to False
because programmatically-constructed circuits may have
uninitialized inputs, where a leading ``Reset`` is not
redundant. Set to True only when those inputs are already
represented as explicit |0⟩ states (e.g. via
:meth:`initialize_qubits` or OpenQASM-style implicit |0⟩
inputs); otherwise eliding turns the leading ``Reset`` into a
no-op instead of trace-out-and-reprepare. See
:func:`circuit_to_graph` for details."""
from .graphparser import circuit_to_graph

return circuit_to_graph(
self if zh else self.to_basic_gates(),
compress_rows,
backend,
initialize_qubits=self._initialize_qubits,
postselect_qubits=self._postselect_qubits
compress_rows,
backend,
initialize_qubits=self._initialize_qubits,
postselect_qubits=self._postselect_qubits,
elide_initial_resets=elide_initial_resets,
)

def to_tensor(self, preserve_scalar:bool=True, strategy:str='naive') -> np.ndarray:
Expand Down
49 changes: 30 additions & 19 deletions pyzx/circuit/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,11 +1305,14 @@ class Reset(Gate):
Corresponds to the OpenQASM ``reset`` instruction, which discards
the current qubit state and unconditionally prepares ``|0⟩``.

In the ZX-diagram this is represented as a Z spider connected to
ground (tracing out / discarding the qubit) followed by a
disconnected X spider with phase 0 (state preparation ``|0⟩``).
This mirrors the ``DiscardBit`` pattern and models reset as a CPTP
map.
In the ZX-diagram this is represented as a Z(0) spider on the
qubit wire with an X(``_rN``) leaf hanging off it, followed by a
disconnected X(0) leaf for the fresh ``|0⟩`` preparation. The
boolean variable ``_rN`` appears nowhere else, so marginalising
over it gives the partial-trace semantics of the discard. The
two leaves are tagged in vertex data with ``outcome_type``
set to ``'reset_discard'`` and ``'reset_state'`` respectively,
so the gate can be recognised on round-trip.
"""
name = 'Reset'

Expand Down Expand Up @@ -1388,19 +1391,12 @@ class ConditionalGate(Gate):
Limitations:

* Only single-qubit Z and X rotations (ZPhase, Z, S, T, XPhase, NOT,
and their subclasses) are supported as inner gates. Other gates,
and their subclasses) are supported as inner gates. Other gates,
including HAD (which is single-qubit but not a Z/X rotation),
CNOT, and CZ, raise ``NotImplementedError`` in ``to_graph()``.
Conditional HAD is a known gap for QEC Pauli-frame-correction use
cases and requires a decomposition into Z/X rotations or a
dedicated graph representation.

* Conditional X rotations (XPhase, NOT) convert to the graph correctly
but cannot be recovered by ``graph_to_circuit()`` because X-type
vertices with boolean phases are indistinguishable from measurement
outcome vertices. They are emitted as raw ``XPhase`` gates with a
symbolic ``Poly`` phase instead. The QASM round-trip (Circuit →
QASM string → Circuit) is unaffected.
"""
name = 'ConditionalGate'

Expand Down Expand Up @@ -1562,7 +1558,14 @@ def reposition(self, mask, bit_mask = None):
return g

def to_graph_symbolic_boolean(self, g, q_mapper):
"""Represent the measurement as a node with symbolic boolean phases."""
"""Represent the measurement as a Z spider with a symbolic-phase leaf.

Places a Z(0) spider on the qubit wire and attaches the classical
outcome as a degree-1 X leaf carrying the symbolic boolean phase,
tagged with ``outcome_type='measurement'`` in vertex data. The
leaf is off-wire, so the qubit wire continues through the Z(0)
spider unaffected by the outcome.
"""
r = q_mapper.next_row(self.target)
if self.result_symbol is not None:
symbol_name = self.result_symbol
Expand All @@ -1571,13 +1574,21 @@ def to_graph_symbolic_boolean(self, g, q_mapper):
else:
symbol_name = "m{}".format(self.target)
phase = new_var(name=symbol_name, is_bool=True, registry=g.var_registry)
_ = self.graph_add_node(g,
# Z(0) on the qubit wire: the measurement itself.
meas_v = self.graph_add_node(g,
q_mapper,
VertexType.X,
VertexType.Z,
self.target,
r,
phase=phase)
q_mapper.set_next_row(self.target, r+1)
r)
# X(phase) as a leaf: the classical outcome, branching off.
outcome_v = g.add_vertex(VertexType.X,
q_mapper.to_qubit(self.target), r + 0.5, phase)
g.add_edge((meas_v, outcome_v), EdgeType.SIMPLE)
g.set_vdata(outcome_v, 'outcome_type', 'measurement')
# Store the classical destination explicitly so it survives
# ``g.substitute_variables(...)`` unwrapping the Poly phase.
g.set_vdata(outcome_v, 'result_symbol', symbol_name)
q_mapper.set_next_row(self.target, r + 1)

def to_graph(self, g, q_mapper, _c_mapper):
self.to_graph_symbolic_boolean(g, q_mapper)
Expand Down
Loading