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
3 changes: 3 additions & 0 deletions documentation/source/general/changelog/changelog-dev.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ API Changes
Development
-----------

* Added a PyZX converter

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Added a PyZX converter
* Added a PyZX bidirectional conversion interace

make sure to add yourself to first time contributors at the bottom of this file.

(`PR #782 <https://github.com/eclipse-qrisp/Qrisp/pull/782>`_).

* Added Dependabot configuration for automated dependency updates
(grouped by type, with labels applied automatically).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Interfacing
QuantumCircuit.from_qasm_file
QuantumCircuit.to_cirq
QuantumCircuit.from_cirq
QuantumCircuit.to_pyzx
QuantumCircuit.from_pyzx



Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
qrisp.QuantumCircuit.from\_pyzx
===============================

.. currentmodule:: qrisp

.. automethod:: QuantumCircuit.from_pyzx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
qrisp.QuantumCircuit.to\_pyzx
=============================

.. currentmodule:: qrisp

.. automethod:: QuantumCircuit.to_pyzx
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ dev-testing = [
"qiskit-aer",
"pyscf",
"cirq",
"pyzx",
"tqecd",
"iqm-client[qrisp]",
]
Expand Down
34 changes: 34 additions & 0 deletions src/qrisp/circuit/quantum_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,20 @@ def to_cirq(self):

return convert_to_cirq(self)

def to_pyzx(self):
"""Method to convert the given QuantumCircuit to a PyZX Circuit.

Returns
-------
pyzx.Circuit
A PyZX Circuit equivalent to the Qrisp QuantumCircuit.

"""
# NOTE: This is here to avoid circular imports
from qrisp.interface import convert_to_pyzx

return convert_to_pyzx(self)

@classmethod
def from_cirq(cls, cirq_circuit):
"""Class method to create QuantumCircuits from Cirq Circuits.
Expand Down Expand Up @@ -2207,6 +2221,26 @@ def from_cirq(cls, cirq_circuit):

return convert_from_cirq(cirq_circuit)

@classmethod
def from_pyzx(cls, pyzx_circuit):
"""Class method to create QuantumCircuits from PyZX Circuits.

Parameters
----------
pyzx_circuit : pyzx.Circuit
The PyZX Circuit to convert.

Returns
-------
QuantumCircuit
The converted QuantumCircuit.

"""
# NOTE: This is here to avoid circular imports
from qrisp.interface import convert_from_pyzx

return convert_from_pyzx(pyzx_circuit)

def to_pdag(self, remove_artificials: bool = False):
"""Method to convert the given QuantumCircuit to a PermeabilityGraph.

Expand Down
1 change: 1 addition & 0 deletions src/qrisp/interface/converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
from qrisp.interface.converter.qulacs_converter import *
from qrisp.interface.converter.stim_converter import *
from qrisp.interface.converter.cirq_converter import *
from qrisp.interface.converter.pyzx_converter import *
273 changes: 273 additions & 0 deletions src/qrisp/interface/converter/pyzx_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
"""********************************************************************************
* Copyright (c) 2026 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know when I originally wrote the issue to follow the approach of the cirq converter. However, this old approach leaves way too many if elif chains in both converters.

from qrisp import QuantumCircuit, RYGate, RZGate, SXGate, SwapGate, HGate, ZGate, u3Gate
import numpy as np
from functools import partial
from fractions import Fraction


def convert_to_pyzx(qrisp_circuit):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add type hints. I know the other files are missing this. But we should get into the habit of making thisngs easier when we automate type checks in the future.

"""Convert a Qrisp QuantumCircuit to a PyZX Circuit.

Parameters
----------
qrisp_circuit : qrisp.QuantumCircuit
The Qrisp QuantumCircuit to convert.

Returns
-------
pyzx.Circuit
A pyzx.Circuit equivalent to the input Qrisp circuit.

Raises
------
ImportError
If PyZX is not installed.
ValueError
If a gate is not supported by the converter.

Notes
-----
Unknown gates are decomposed by Qrisp's transpiler before conversion.
The converter transpiles all unknown gates together, then checks if any
new unknown gates appeared. This repeats until all gates are known
or transpilation makes no progress.

"""
try:
from pyzx import Circuit
except (ModuleNotFoundError, ImportError) as exc:
raise ImportError("PyZX must be installed to be able to use the Qrisp to PyZX converter.") from exc
from pyzx import settings

settings.strict_phase_types = False # this enables PyZX to (approximately) convert float phases to a Fraction

gate_map = {
"cx": "CNOT",
"cy": "CY",
"cz": "CZ",
"swap": "SWAP",
"h": "HAD",
"x": "NOT",
"y": "Y",
"z": "Z",
"rx": "XPhase",
"ry": "YPhase",
"rz": "ZPhase",
"u3": "U3",
"s": "S",
"t": "T",
"s_dg": None,
"t_dg": None,
"p": None,
"sx": "SX",
"sx_dg": None,
"xxyy": None,
"rxx": "RXX",
"rzz": "RZZ",
"measure": "Measurement",
"reset": "Reset",
# skip qubit allocation and deallocation ops in the converter, as well as identity and global phases
"qb_alloc": None,
"qb_dealloc": None,
"gphase": None,
"id": None,
}

# repeatedly transpile unknown gates until only known ones remain
def _unknown_names(circuit):
return {instr.op.name for instr in circuit.data if instr.op.name not in gate_map}

while True:
unknown = _unknown_names(qrisp_circuit)
if not unknown:
break

def _transpile_predicate(op, _unknown=unknown):
return op.name in _unknown

try:
transpiled = qrisp_circuit.transpile(transpile_predicate=_transpile_predicate)
except Exception as exc:
raise ValueError(
f"Gates {unknown} could not be transpiled and are not supported by the Qrisp to Cirq converter."

@purva-thakre purva-thakre Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I htink you mean the pyzx converter here 😄 .

) from exc

new_unknown = _unknown_names(transpiled)
if new_unknown == unknown:
names = ", ".join(sorted(unknown))
raise ValueError(
f"The following gates could not be decomposed into elementary "
f"instructions: {names}. Try transpiling the circuit with "
f"Qrisp's transpile() method before calling to_pyzx(), or "
f"use only gates supported natively by the converter."
)

qrisp_circuit = transpiled

num_qubits = qrisp_circuit.num_qubits()
pyzx_circuit = Circuit(num_qubits)

qubit_map = {}
for i, q in enumerate(qrisp_circuit.qubits):
qubit_map[q] = i

for instr in qrisp_circuit.data:
name = instr.op.name
qubits = instr.qubits
params = instr.op.params if hasattr(instr.op, "params") else []

pyxz_gate = gate_map[name]
pyxz_op_qubits = [qubit_map[q] for q in qubits]

# gate with no direct PyXZ equivalent
if pyxz_gate is None:
if name in ["id", "gphase", "qb_alloc", "qb_dealloc"]:
pass
elif name == "s_dg":
pyzx_circuit.add_gate("U3", *pyxz_op_qubits, 0, 0, Fraction(-1, 2))
elif name == "t_dg":
pyzx_circuit.add_gate("U3", *pyxz_op_qubits, 0, 0, Fraction(-1, 4))
elif name == "p":
pyzx_circuit.add_gate("U3", *pyxz_op_qubits, 0, 0, params[0] / np.pi)
elif name == "sx_dg":
pyzx_circuit.add_gate("XPhase", *pyxz_op_qubits, Fraction(-1, 2))
# decompose via its .definition circuit (e.g. xxyy)
elif instr.op.definition:
pyzx_circuit.append(convert_to_pyzx(instr.op.definition), mask=pyxz_op_qubits)
else:
raise ValueError(f"{name} gate has no PyXZ equivalent and no definition to decompose.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a mock test to verify the converter raises an error as expected.

continue

if params:
if name in ["rx", "ry", "rz", "u3", "rxx", "rzz"]:
pyzx_circuit.add_gate(pyxz_gate, *pyxz_op_qubits, *[p / np.pi for p in params])
else:
raise ValueError(f"{name} gate has a parameter but is not in rx, ry, rz, u3, rxx, rzz.")
else:
pyzx_circuit.add_gate(pyxz_gate, *pyxz_op_qubits)

return pyzx_circuit


def convert_from_pyzx(pyzx_circuit):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type hints here as well

"""Convert a PyZX QuantumCircuit to a Qrisp Circuit.

Parameters
----------
pyzx_circuit : pyzx.Circuit
The PyZX Circuit to convert.

Returns
-------
qrisp.QuantumCircuit
A qrisp.QuantumCircuit equivalent to the input PyZX circuit.

Raises
------
ValueError
If a gate is not supported by the converter.

Notes
-----
Gates that exist in PyZX but not in Qrisp are either substituted directly if they
have a straightforward equivalent (applies to U2 and several controlled gates),
or it is used PyZX's to_basic_gates() method to decompose those gates.
"""
qc = QuantumCircuit(pyzx_circuit.qubits)

gate_map = {
# single-qubit gates
"NOT": qc.x,
"Y": qc.y,
"Z": qc.z,
"HAD": qc.h,
"XPhase": qc.rx,
"YPhase": qc.ry,
"ZPhase": qc.rz,
"U2": partial(qc.u3, np.pi / 2),
"U3": qc.u3,
"SX": qc.sx,
"S": qc.s,
"T": qc.t,
# multi-qubits gates
"CNOT": qc.cx,
"CY": qc.cy,
"CZ": qc.cz,
"CRX": qc.crx,
"CRY": lambda phase, x, y: qc.append(RYGate(phase).control(), [x, y]),
"CRZ": lambda phase, x, y: qc.append(RZGate(phase).control(), [x, y]),
"CSX": lambda x, y: qc.append(SXGate().control(), [x, y]),
"CPhase": qc.cp,
"ParityPhase": None,
"PhaseGadget": None,
"XCX": None,
"SWAP": qc.swap,
"CSWAP": lambda x, y, z: qc.append(SwapGate().control(), [x, y, z]),
"CHAD": lambda x, y: qc.append(HGate().control(), [x, y]),
"Tof": qc.ccx,
"CCZ": lambda x, y, z: qc.append(ZGate().control(2), [x, y, z]),
"CU3": lambda theta, phi, lam, x, y: qc.append(u3Gate(theta, phi, lam).control(), [x, y]),
"RZZ": qc.rzz,
"RXX": qc.rxx,
"FSim": None,
# non-unitary operations
"Measurement": qc.measure,
"Reset": qc.reset,
"InitAncilla": None,
"PostSelect": None,
"DiscardBit": None,
"ConditionalGate": None,
}

def add_gate(gate):
# single-qubit, parameter-free gates and non-unitary operations
if gate.name in ["NOT", "Y", "Z", "HAD", "SX", "S", "T", "Measurement", "Reset"]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure the dagger of S, T and SX are also handled correctly.

gate_map[gate.name](gate.target)
# single-qubit, one-parameter gates
elif gate.name in ["XPhase", "YPhase", "ZPhase"]:
gate_map[gate.name](float(gate.phase) * np.pi, gate.target)
# single-qubit, multi-parameter gates
elif gate.name in ["U2", "U3"]:
gate_map[gate.name](*[float(p) * np.pi for p in gate.phases], gate.target)

# two-qubit, paratemeter-free gates
elif gate.name in ["CNOT", "CY", "CZ", "CSX", "SWAP", "CHAD"]:
gate_map[gate.name](gate.control, gate.target)
# two-qubit, one-paratemeter gates
elif gate.name in ["CRX", "CRY", "CRZ", "CPhase", "RZZ", "RXX"]:
gate_map[gate.name](float(gate.phase) * np.pi, gate.control, gate.target)
# two-qubit, multi-paratemeter gates
elif gate.name in ["CU3"]:
gate_map[gate.name](*[float(p) * np.pi for p in gate.phases], gate.control, gate.target)
# multi-qubit, paratemeter-free gates
elif gate.name in ["CSWAP", "Tof", "CCZ"]:
gate_map[gate.name](gate.ctrl1, gate.ctrl2, gate.target)

for gate in pyzx_circuit.gates:
if gate_map[gate.name] is not None:
add_gate(gate)
else:
# try with pyzx's basic gate decomposition
for _gate in gate.to_basic_gates():
if gate_map[_gate.name] is not None:
add_gate(_gate)
else:
raise ValueError(f"{_gate.name} gate has no Qrisp equivalent and cannot be decomposed either.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a mock test to verify the converter raises an error when it runs into this.


return qc
Loading
Loading