Skip to content
Merged
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
24 changes: 15 additions & 9 deletions pyzx/circuit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from .gates import (Gate, gate_types, NOT, Y, Z, HAD, XPhase, YPhase, ZPhase, U2, U3, S, T, SX, SWAP, RXX, RZZ, CNOT,
CY, CZ, CHAD, CSX, XCX, CRX, CRY, CRZ, CPhase, CU3, CU, CSWAP, Tofolli, CCZ, ParityPhase, FSim,
Measurement, PhaseGadget)
Measurement, PhaseGadget, ConditionalGate)

from ..graph.base import BaseGraph
from ..utils import EdgeType
Expand Down Expand Up @@ -403,8 +403,8 @@ def from_quipper_file(fname: str) -> 'Circuit':
@staticmethod
def from_qasm(s: str) -> 'Circuit':
"""Produces a :class:`Circuit` based on a QASM input string.
Supports OpenQASM 2 and 3, including ``reset`` and ``measure``
statements.
Supports OpenQASM 2 and 3, including ``reset``, ``measure``,
and ``if`` (classical control / feedforward) statements.
It currently doesn't support custom gates that have parameters."""
from .qasmparser import QASMParser
p = QASMParser()
Expand All @@ -413,8 +413,8 @@ def from_qasm(s: str) -> 'Circuit':
@staticmethod
def from_qasm_file(fname: str) -> 'Circuit':
"""Produces a :class:`Circuit` based on a QASM description of a circuit.
Supports OpenQASM 2 and 3, including ``reset`` and ``measure``
statements.
Supports OpenQASM 2 and 3, including ``reset``, ``measure``,
and ``if`` (classical control / feedforward) statements.
It currently doesn't support custom gates that have parameters."""
from .qasmparser import QASMParser
p = QASMParser()
Expand All @@ -440,15 +440,21 @@ def to_qasm(self, version: int = 2) -> str:
else:
s = """OPENQASM 2.0;\ninclude "qelib1.inc";\n"""
s += "qreg q[{!s}];\n".format(self.qubits)
# Collect classical register declarations from measurement gates.
# Collect classical register declarations from measurement and conditional gates.
cregs: Dict[str, int] = {}
for g in self.gates:
if isinstance(g, Measurement) and g.result_symbol is not None:
# result_symbol is e.g. "c[2]"; extract register name and index.
if '[' in g.result_symbol:
if isinstance(g, Measurement):
if g.result_symbol is not None and '[' in g.result_symbol:
# result_symbol is e.g. "c[2]"; extract register name and index.
regname, idx_str = g.result_symbol.split('[', 1)
idx = int(idx_str.rstrip(']')) + 1
cregs[regname] = max(cregs.get(regname, 0), idx)
elif g.result_bit is not None:
# result_bit uses the default "c" register.
cregs["c"] = max(cregs.get("c", 0), g.result_bit + 1)
if isinstance(g, ConditionalGate):
regname = g.condition_register
cregs[regname] = max(cregs.get(regname, 0), g.register_size)
if version == 3:
for regname, size in cregs.items():
s += "bit[{!s}] {};\n".format(size, regname)
Expand Down
135 changes: 132 additions & 3 deletions pyzx/circuit/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,11 @@ def to_qasm(self) -> str:
param = ""
if self.print_phase:
if hasattr(self, "phase"):
param = "({}*pi)".format(float(self.phase))
try:
param = "({}*pi)".format(float(self.phase))
except (TypeError, ValueError):
# Symbolic (Poly) phase — emit as-is.
param = "({}*pi)".format(self.phase)
elif hasattr(self, "phases"):
param = "({})".format(",".join("{}*pi".format(float(p)) for p in self.phases))
return "{}{} {};".format(n, param, ", ".join(args))
Expand Down Expand Up @@ -1373,6 +1377,120 @@ def reposition(self, mask, bit_mask=None):
return g


class ConditionalGate(Gate):
"""A gate that is applied only when a classical register equals a given value.

Corresponds to the OpenQASM 2 ``if (creg == val) gate args;`` syntax.
In the ZX-diagram, single-qubit Z/X-rotation inner gates are represented
by multiplying the gate's phase by a boolean condition polynomial built
from the register bits.

Limitations:

* Only single-qubit Z and X rotations (ZPhase, Z, S, T, XPhase, NOT,
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'

def __init__(self, condition_register: str, condition_value: int,
inner_gate: 'Gate', register_size: int) -> None:
if condition_value < 0 or condition_value >= (1 << register_size):
raise ValueError(
"Condition value {} is out of range for a {}-bit register "
"(must be 0..{})".format(
condition_value, register_size, (1 << register_size) - 1))
self.condition_register = condition_register
self.condition_value = condition_value
self.inner_gate = inner_gate
self.register_size = register_size
self.target = inner_gate.target # type: ignore

def __str__(self) -> str:
return "if({}=={}) {}".format(
self.condition_register, self.condition_value, self.inner_gate)

def __eq__(self, other: object) -> bool:
if not isinstance(other, ConditionalGate):
return False
return (self.condition_register == other.condition_register
and self.condition_value == other.condition_value
and self.inner_gate == other.inner_gate
and self.register_size == other.register_size)

def _max_target(self) -> int:
return self.inner_gate._max_target()

def copy(self) -> 'ConditionalGate':
return ConditionalGate(
self.condition_register, self.condition_value,
self.inner_gate.copy(), self.register_size)

def reposition(self, mask, bit_mask=None):
g = self.copy()
g.inner_gate = g.inner_gate.reposition(mask, bit_mask)
g.target = g.inner_gate.target # type: ignore
return g

def to_qasm(self) -> str:
inner_qasm = self.inner_gate.to_qasm()
return "if({}=={}) {}".format(
self.condition_register, self.condition_value, inner_qasm)

def _build_condition_poly(self, g: BaseGraph[VT, ET]) -> 'Poly': # type: ignore[name-defined]
"""Build a boolean polynomial representing the register condition.

For ``if (reg == val)`` with an *n*-bit register, the condition is
the product over all bits *i* of either ``bit_var_i`` (when bit *i*
of *val* is 1) or ``(1 - bit_var_i)`` (when bit *i* is 0).
"""
from ..symbolic import Poly, new_var as sym_new_var, new_const
result: Poly = new_const(1)
for i in range(self.register_size):
bit_var = sym_new_var(
"{}[{}]".format(self.condition_register, i),
is_bool=True, registry=g.var_registry)
if (self.condition_value >> i) & 1:
result = result * bit_var
else:
result = result * (new_const(1) - bit_var)
return result

def to_graph(self, g: BaseGraph[VT, ET], q_mapper: TargetMapper[VT],
c_mapper: TargetMapper[VT]) -> None:
inner = self.inner_gate
# Single-qubit Z or X rotations: multiply phase by condition polynomial.
if isinstance(inner, ZPhase):
cond = self._build_condition_poly(g)
phase = cond * inner.phase
self.graph_add_node(g, q_mapper, VertexType.Z, inner.target,
q_mapper.next_row(inner.target), phase)
q_mapper.advance_next_row(inner.target)
elif isinstance(inner, XPhase):
cond = self._build_condition_poly(g)
phase = cond * inner.phase
self.graph_add_node(g, q_mapper, VertexType.X, inner.target,
q_mapper.next_row(inner.target), phase)
q_mapper.advance_next_row(inner.target)
else:
raise NotImplementedError(
"ConditionalGate.to_graph() is not supported for gate type "
"'{}'. Only single-qubit Z and X rotations (ZPhase, Z, S, T, "
"XPhase, NOT, and their subclasses) are currently "
"supported.".format(type(inner).__name__))


class DiscardBit(Gate):
name = 'DiscardBit'
def __init__(self, target):
Expand Down Expand Up @@ -1425,6 +1543,11 @@ def __eq__(self, other: object) -> bool:

def to_qasm(self) -> str:
if self.result_symbol is not None:
if '[' not in self.result_symbol:
raise TypeError(
"Measurement result_symbol '{}' is not a valid QASM "
"classical bit reference (expected 'reg[index]')".format(
self.result_symbol))
return "measure q[{:d}] -> {};".format(self.target, self.result_symbol)
if self.result_bit is not None:
return "measure q[{:d}] -> c[{:d}];".format(self.target, self.result_bit)
Expand Down Expand Up @@ -1467,8 +1590,13 @@ def to_graph_ground(self, g, q_mapper, c_mapper):
def to_graph_symbolic_boolean(self, g, q_mapper):
"""Represent the measurement as a node with symbolic boolean phases."""
r = q_mapper.next_row(self.target)
symbol_name = self.result_symbol if self.result_symbol is not None else f"m{self.target}"
phase = new_var(name=f"{symbol_name}", is_bool=True, registry=g.var_registry)
if self.result_symbol is not None:
symbol_name = self.result_symbol
elif self.result_bit is not None:
symbol_name = "c[{}]".format(self.result_bit)
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,
q_mapper,
VertexType.X,
Expand Down Expand Up @@ -1525,6 +1653,7 @@ def to_graph(self, g, q_mapper, c_mapper, ground=False):
"PostSelect": PostSelect,
"DiscardBit": DiscardBit,
"Measurement": Measurement,
"ConditionalGate": ConditionalGate,
}

qasm_gate_table: Dict[str, Type[Gate]] = {
Expand Down
126 changes: 122 additions & 4 deletions pyzx/circuit/graphparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,117 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Dict, List, Optional
import warnings
from typing import Dict, List, Optional, Union

from . import Circuit
from .gates import InitAncilla, Measurement, Reset, TargetMapper
from ..utils import EdgeType, VertexType, FloatInt, FractionLike
from .gates import (Gate, InitAncilla, Measurement, Reset, TargetMapper,
ConditionalGate, ZPhase, XPhase, NOT, Z, S, T)
from ..utils import EdgeType, VertexType, FloatInt, FractionLike, settings
from ..graph import Graph
from ..graph.base import BaseGraph, VT, ET
from ..symbolic import Poly, new_var

def _poly_phase_to_conditional_gate(
phase: Poly, vertex_type: VertexType, qubit: int
) -> Optional[ConditionalGate]:
"""Try to convert a symbolic Poly phase into a ConditionalGate.

The polynomial must consist entirely of boolean variables from the
same classical register (with names like ``reg[i]``). The function
evaluates the polynomial at every possible bit assignment to find
the unique assignment that produces a non-zero result, which gives
both the condition value and the inner gate phase.

Returns ``None`` when the polynomial cannot be interpreted as a
condition — e.g. variables from different registers, non-boolean
variables, complex coefficients, or more than one non-zero
assignment (which would mean the phase is not a simple conditional).

This function is only called for Z-type vertices. X-type vertices
are skipped because their boolean phases are ambiguous with
measurement outcome vertices (see the call site in
``graph_to_circuit``).
"""
from fractions import Fraction
from ..symbolic import Var
import re
# Collect all boolean variables across all terms.
bit_pattern = re.compile(r'^(\w+)\[(\d+)\]$')
reg_name: Optional[str] = None
bit_vars: Dict[int, Var] = {}
for _coeff, term in phase.terms:
for var, _exp in term.vars:
if not var.is_bool:
return None
m = bit_pattern.match(var.name)
if m is None:
return None
name = m.group(1)
idx = int(m.group(2))
if reg_name is None:
reg_name = name
elif reg_name != name:
return None # Variables from different registers.
bit_vars[idx] = var
if reg_name is None or not bit_vars:
return None
reg_size = max(bit_vars.keys()) + 1
# Evaluate the polynomial at each possible bit assignment.
# A valid condition polynomial is non-zero for exactly one assignment.
if reg_size > 16:
warnings.warn(
"Conditional gate extraction is O(2^n) in register size; "
"register '{}' has {} bits ({} evaluations).".format(
reg_name, reg_size, 1 << reg_size))
cond_value: Optional[int] = None
inner_phase_value: Optional[Fraction] = None
for val in range(1 << reg_size):
var_map: Dict[Var, Union[float, complex, Fraction]] = {}
for idx, var in bit_vars.items():
var_map[var] = Fraction((val >> idx) & 1)
result = phase.substitute(var_map)
# After full substitution, sum all constant coefficients and
# reduce mod 2 since phases are in units of pi.
total: Fraction = Fraction(0)
for c, t in result.terms:
if t.vars:
return None # Not fully substituted.
if isinstance(c, complex):
return None
total += Fraction(c)
total = total % 2
if total == 0:
continue
if cond_value is not None:
return None # Non-zero for more than one assignment.
cond_value = val
inner_phase_value = Fraction(total).limit_denominator(settings.float_to_fraction_max_denominator)
if cond_value is None or inner_phase_value is None:
return None
inner_phase = inner_phase_value
if vertex_type == VertexType.Z:
if inner_phase == 1:
inner_gate: Gate = Z(qubit)
elif inner_phase == Fraction(1, 2):
inner_gate = S(qubit)
elif inner_phase == Fraction(-1, 2) or inner_phase == Fraction(3, 2):
inner_gate = S(qubit, adjoint=True)
elif inner_phase == Fraction(1, 4):
inner_gate = T(qubit)
elif inner_phase == Fraction(-1, 4) or inner_phase == Fraction(7, 4):
inner_gate = T(qubit, adjoint=True)
else:
inner_gate = ZPhase(qubit, inner_phase)
else:
# X-type vertices are skipped at the call site because their
# boolean phases are ambiguous with measurement outcomes.
raise ValueError(
"Unsupported vertex type {} for conditional gate "
"extraction.".format(vertex_type))
return ConditionalGate(reg_name, cond_value, inner_gate, reg_size)


def graph_to_circuit(g:BaseGraph[VT,ET], split_phases:bool=True) -> Circuit:
inputs = g.inputs()
qs = g.qubits()
Expand Down Expand Up @@ -83,7 +185,23 @@ def graph_to_circuit(g:BaseGraph[VT,ET], split_phases:bool=True) -> Circuit:
# (The corresponding Reset gate is emitted when the state
# preparation vertex on this qubit is processed.)
continue
if phase!=0 and not split_phases:
if isinstance(phase, Poly):
# Only extract Z-type vertices as conditional gates.
# X-type vertices with boolean phases are ambiguous:
# measurement outcomes (from Measurement.to_graph_symbolic_boolean)
# produce the same X spider structure as conditional NOT/XPhase
# gates, so we cannot distinguish them here. Conditional
# Z rotations are unambiguous because measurements never
# create Z spiders with boolean phases.
cgate = None
if t == VertexType.Z:
cgate = _poly_phase_to_conditional_gate(phase, t, int(q))
if cgate is not None:
c.add_gate(cgate)
elif phase != 0:
gate_name = "ZPhase" if t == VertexType.Z else "XPhase"
c.add_gate(gate_name, q, phase=phase)
elif phase!=0 and not split_phases:
if t == VertexType.Z: c.add_gate("ZPhase", q, phase=phase)
else: c.add_gate("XPhase", q, phase=phase)
elif t == VertexType.Z and phase.denominator == 2:
Expand Down
Loading
Loading