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
21 changes: 19 additions & 2 deletions pyzx/circuit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +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.
It ignores all the non-unitary instructions like measurements in the file.
Supports OpenQASM 2 and 3, including ``reset`` and ``measure``
statements.
It currently doesn't support custom gates that have parameters."""
from .qasmparser import QASMParser
p = QASMParser()
Expand All @@ -412,7 +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.
It ignores all the non-unitary instructions like measurements in the file.
Supports OpenQASM 2 and 3, including ``reset`` and ``measure``
statements.
It currently doesn't support custom gates that have parameters."""
from .qasmparser import QASMParser
p = QASMParser()
Expand All @@ -438,6 +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.
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:
regname, idx_str = g.result_symbol.split('[', 1)
idx = int(idx_str.rstrip(']')) + 1
cregs[regname] = max(cregs.get(regname, 0), idx)
if version == 3:
for regname, size in cregs.items():
s += "bit[{!s}] {};\n".format(size, regname)
else:
for regname, size in cregs.items():
s += "creg {}[{!s}];\n".format(regname, size)
for g in self.gates:
s += g.to_qasm() + "\n"
return s
Expand Down
46 changes: 46 additions & 0 deletions pyzx/circuit/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,42 @@ def reposition(self, mask, bit_mask=None):
return g


class Reset(Gate):
"""Reset an existing qubit to |0⟩.

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.
"""
name = 'Reset'

def __init__(self, target: int):
self.target = target
self.label = target

def __str__(self) -> str:
return "Reset({})".format(self.target)

def __eq__(self, other: object) -> bool:
if not isinstance(other, Reset):
return False
return self.target == other.target

def to_qasm(self) -> str:
return "reset q[{:d}];".format(self.target)

def reposition(self, mask, bit_mask=None):
g = self.copy()
g.target = mask[self.target]
g.label = g.target
return g


class PostSelect(Gate):
"""Post-select a qubit in a specified state.

Expand Down Expand Up @@ -1384,8 +1420,17 @@ def __eq__(self, other: object) -> bool:
if not isinstance(other, Measurement): return False
if self.target != other.target: return False
if self.result_bit != other.result_bit: return False
Comment thread
dlyongemallo marked this conversation as resolved.
if self.result_symbol != other.result_symbol: return False
return True

def to_qasm(self) -> str:
if self.result_symbol is not None:
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)
raise TypeError("Measurement on qubit {} has no result destination".format(
self.target))

def reposition(self, mask, bit_mask = None):
g = self.copy()
g.target = mask[self.target]
Expand Down Expand Up @@ -1476,6 +1521,7 @@ def to_graph(self, g, q_mapper, c_mapper, ground=False):
"RXX": RXX,
"FSim": FSim,
"InitAncilla": InitAncilla,
"Reset": Reset,
"PostSelect": PostSelect,
"DiscardBit": DiscardBit,
"Measurement": Measurement,
Expand Down
76 changes: 62 additions & 14 deletions pyzx/circuit/graphparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from typing import Dict, List, Optional

from . import Circuit
from .gates import Measurement, TargetMapper
from .gates import InitAncilla, Measurement, Reset, TargetMapper
from ..utils import EdgeType, VertexType, FloatInt, FractionLike
from ..graph import Graph
from ..graph.base import BaseGraph, VT, ET
Expand All @@ -30,9 +30,17 @@ def graph_to_circuit(g:BaseGraph[VT,ET], split_phases:bool=True) -> Circuit:
ty = g.types()
phases = g.phases()
rows: Dict[FloatInt,List[VT]] = {}
# Map (vertex_type, phase) → InitAncilla state string.
_reverse_state_map: Dict[tuple, str] = {
(VertexType.Z, 0): '+',
(VertexType.Z, 1): '-',
(VertexType.X, 0): '0',
(VertexType.X, 1): '1',
}
input_qubits = set(qs[v] for v in inputs)

c = Circuit(len(inputs))

for v in g.vertices():
if v in inputs: continue
r = g.row(v)
Expand All @@ -44,6 +52,23 @@ def graph_to_circuit(g:BaseGraph[VT,ET], split_phases:bool=True) -> Circuit:
phase = phases[v]
t = ty[v]
neigh = [w for w in g.neighbors(v) if rs[w]<r]
if len(neigh) == 0:
# No backward neighbour: state preparation vertex.
qi = int(q)
if t == VertexType.X and phase == 0 and q in input_qubits:
# Qubit already has an input boundary → mid-circuit reset.
# NOTE: this heuristic assumes any disconnected X(0) spider
# on an input qubit is a reset. It could misidentify a
# manually constructed graph that uses X(0) preparation on
# an input qubit for a different purpose.
c.add_gate(Reset(qi))
continue
state = _reverse_state_map.get((t, phase))
if state is not None:
c.add_gate(InitAncilla(qi, state))
continue
raise TypeError("Graph doesn't seem circuit like: "
"vertex {} has no parents".format(v))
if len(neigh) != 1:
raise TypeError("Graph doesn't seem circuit like: multiple parents")
n = neigh[0]
Expand All @@ -53,6 +78,11 @@ def graph_to_circuit(g:BaseGraph[VT,ET], split_phases:bool=True) -> Circuit:
c.add_gate("HAD", q)
if t == VertexType.BOUNDARY: #vertex is an output
continue
if g.is_ground(v):
# Ground vertex: discard/trace-out, part of a Reset pair.
# (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 t == VertexType.Z: c.add_gate("ZPhase", q, phase=phase)
else: c.add_gate("XPhase", q, phase=phase)
Expand Down Expand Up @@ -118,8 +148,9 @@ def circuit_to_graph(
qubit = i+c.qubits
v = g.add_vertex(VertexType.BOUNDARY, qubit, 0)
inputs.append(v)
q_mapper.add_label(qubit, 1)
c_mapper.set_prev_vertex(qubit, v)
c_mapper.add_label(i, 1)
c_mapper.set_qubit(i, qubit)
c_mapper.set_prev_vertex(i, v)


for gate in c.gates:
Expand All @@ -128,24 +159,41 @@ def circuit_to_graph(
measure_targets.add(gate.target)
if gate.name == 'InitAncilla':
l = gate.label # type: ignore
try:
q_mapper.add_label(l, q_mapper.next_row_or_default(l, q_mapper.max_row() - 1))
except ValueError:
if l in q_mapper.labels():
raise ValueError("Ancilla label {} already in use".format(str(l)))
vtype, phase = gate.get_vertex_info() # type: ignore
q_mapper.add_label(l, q_mapper.next_row_or_default(l, q_mapper.max_row() - 1))
v = g.add_vertex(vtype, q_mapper.to_qubit(l), q_mapper.next_row(l), phase)
q_mapper.set_prev_vertex(l, v)
q_mapper.advance_next_row(l)
elif gate.name == 'Reset':
# Model reset as discard (ground) then preparation |0⟩,
# creating a wire break. The ground connection traces out
# the qubit, modelling reset as a CPTP map.
l = gate.label # type: ignore
q = q_mapper.to_qubit(l)
# The qubit is being reused, so clear any pending measurement
# marker. If the qubit is measured again later without a
# subsequent reset, it will be re-added.
measure_targets.discard(q)
r = q_mapper.next_row(l)
u = q_mapper.prev_vertex(l)
# Effect vertex: discard the old state by tracing out.
effect_v = g.add_vertex(VertexType.Z, q, r, 0, ground=True)
g.add_edge((u, effect_v), EdgeType.SIMPLE)
# State vertex: prepare the new |0⟩ state.
state_v = g.add_vertex(VertexType.X, q, r + 1, 0)
q_mapper.set_prev_vertex(l, state_v)
q_mapper.set_next_row(l, r + 2)
elif gate.name == 'PostSelect':
l = gate.label # type: ignore
try:
q = q_mapper.to_qubit(l)
r = q_mapper.next_row(l)
u = q_mapper.prev_vertex(l)
q_mapper.set_next_row(l, r + 1)
q_mapper.remove_label(l)
except ValueError:
if l not in q_mapper.labels():
raise ValueError("PostSelect label {} is not in use".format(str(l)))
q = q_mapper.to_qubit(l)
r = q_mapper.next_row(l)
u = q_mapper.prev_vertex(l)
q_mapper.set_next_row(l, r + 1)
q_mapper.remove_label(l)
vtype, phase = gate.get_vertex_info() # type: ignore
v = g.add_vertex(vtype, q, r, phase)
g.add_edge((u,v),EdgeType.SIMPLE)
Expand Down
80 changes: 69 additions & 11 deletions pyzx/circuit/qasmparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from typing import List, Dict, Tuple, Optional

from . import Circuit
from .gates import Gate, qasm_gate_table, Measurement
from .gates import Gate, qasm_gate_table, Measurement, Reset
from ..utils import settings


Expand All @@ -33,6 +33,7 @@ def __init__(self) -> None:
self.gates: List[Gate] = []
self.custom_gates: Dict[str,Circuit] = {}
self.registers: Dict[str,Tuple[int,int]] = {}
self.cregisters: Dict[str,int] = {}
self.qubit_count: int = 0
self.bit_count: int = 0
self.circuit: Optional[Circuit] = None
Expand All @@ -41,7 +42,9 @@ def parse(self, s: str, strict:bool=True) -> Circuit:
self.gates = []
self.custom_gates = {}
self.registers = {}
self.cregisters = {}
self.qubit_count = 0
self.bit_count = 0
self.circuit = None
lines = s.splitlines()
r = []
Expand Down Expand Up @@ -78,7 +81,8 @@ def parse(self, s: str, strict:bool=True) -> Circuit:
for c in commands:
self.gates.extend(self.parse_command(c, self.registers))

circ = Circuit(self.qubit_count)
self.bit_count = sum(self.cregisters.values())
circ = Circuit(self.qubit_count, bit_amount=self.bit_count)
circ.gates = self.gates
self.circuit = circ
return self.circuit
Expand Down Expand Up @@ -141,16 +145,70 @@ def extract_command_parts(self, c: str) -> Tuple[str,List[Fraction],List[str]]:
def parse_command(self, c: str, registers: Dict[str,Tuple[int,int]]) -> List[Gate]:
gates: List[Gate] = []
name, phases, args = self.extract_command_parts(c)
if name in ("barrier","creg", "id"): return gates
if name in ("barrier", "id"): return gates
if name == "creg":
regname, sizep = args[0].split("[", 1)
size = int(sizep[:-1])
self.cregisters[regname] = size
return gates
if name == "measure":
target, result_bit = args[0].split(' -> ')
# Extract the register name and index separately for both target and result
_, target_idx = target.split('[')
result_reg_name, result_idx = result_bit.split('[')
# Remove the trailing ']' and convert to int
target_qbit = int(target_idx[:-1])
gate = Measurement(target_qbit, result_symbol=f"{result_reg_name}[{result_idx[:-1]}]")
gates.append(gate)
# Strip all spaces so we can split on '->' regardless of spacing.
raw = args[0].replace(' ', '')
target, result_bit = raw.split('->')
if '[' in target:
# Single qubit: measure q[0] -> c[0];
regname, target_idx = target.split('[', 1)
if regname not in registers:
raise TypeError("Invalid register {}".format(regname))
target_qbit = registers[regname][0] + int(target_idx[:-1])
result_reg_name, result_idx = result_bit.split('[', 1)
result_index = int(result_idx[:-1])
if result_reg_name not in self.cregisters:
raise TypeError("Undeclared classical register {}".format(
result_reg_name))
if result_index >= self.cregisters[result_reg_name]:
raise TypeError(
"Index {} out of range for classical register {} "
"of size {}".format(
result_index, result_reg_name,
self.cregisters[result_reg_name]))
gate = Measurement(target_qbit, result_symbol=f"{result_reg_name}[{result_index}]")
gates.append(gate)
else:
# Register broadcast: measure q -> c.
if target not in registers:
raise TypeError("Invalid register {}".format(target))
if result_bit not in self.cregisters:
raise TypeError("Undeclared classical register {}".format(
result_bit))
start, size = registers[target]
if size > self.cregisters[result_bit]:
raise TypeError(
"Quantum register {} (size {}) is larger than "
"classical register {} (size {})".format(
target, size, result_bit,
self.cregisters[result_bit]))
for i in range(size):
gate = Measurement(start + i, result_symbol=f"{result_bit}[{i}]")
gates.append(gate)
return gates
if name == "reset":
# Reset initializes a qubit to |0⟩ state.
# In OpenQASM: `reset q[0];` or `reset q;` (for entire register)
for a in args:
if "[" in a:
regname, valp = a.split("[", 1)
val = int(valp[:-1])
if regname not in registers:
raise TypeError("Invalid register {}".format(regname))
qubit = registers[regname][0] + val
gates.append(Reset(qubit))
else:
if a not in registers:
raise TypeError("Invalid register {}".format(a))
start, size = registers[a]
for i in range(size):
gates.append(Reset(start + i))
return gates
if name in ("opaque", "if"):
raise TypeError("Unsupported operation {}".format(c))
Expand Down
4 changes: 3 additions & 1 deletion pyzx/symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def new_const(coeff: Union[int, Fraction]) -> Poly:
?factor : base ("^" exponent)?
base : intf | frac | decimal | pi | pifrac | var | "(" expr ")"
exponent : intf
var : CNAME
var : CNAME ("[" INT "]")?
intf : INT
decimal : DECIMAL
pi : "\\pi" | "pi" | "π"
Expand Down Expand Up @@ -464,6 +464,8 @@ def exponent(self, items: List[Any]) -> int:

def var(self, items: List[Any]) -> Poly:
v = str(items[0])
if len(items) > 1 and items[1] is not None:
v += "[{}]".format(int(items[1]))
return self._new_var(v)

def pi(self, _: List[Any]) -> Poly:
Expand Down
Loading