Improve circuit pre-processing for `Cirq` converter with existing gate decomposers
Before passing an input cirq circuit to the QIR converter, we first decompose the circuit to ensure that it only uses gates / operations that are supported by QIR, see PYQIR_OP_MAP. Right now, to accomplish this, we use a preprocess_circuit function which loops through each operation, and uses a try / except to determine if the operation is supported, and if not, applies a naive cirq.decompose_once to the circuit.
https://github.com/qBraid/qbraid-qir/blob/3ef504fe2f2f7c9ecd990649b23d70ccb255551a/qbraid_qir/cirq/passes.py#L34-L43
For the majority of use-cases, this works. However, it is:
- Bad style
- Inefficient
Instead, we would like to use one of the built-in Cirq transforms such as cirq.optimize_for_target_gateset to automatically ensure that the input circuit conforms to the supported QIR operations. In doing so, hopefully we can eliminate the try except blocks in passes.py, and re-implement preprocess_circuit without the clunky helpers.
@ryanhill1 Is this still open? I'd love to work on this!
@arulandu It is, go for it!
I'm thinking of creating a Gateset by extending https://quantumai.google/reference/python/cirq/TwoQubitCompilationTargetGateset. However, I'm unsure how to implement self._decompose_two_qubit_operation. Could we just use https://quantumai.google/reference/python/cirq/CZTargetGateset?
I'll admit, I'm not super familiar with how cirq.CompilationTargetGateset work. It seems like you just start by passing in a list of cirq.Gate, so to start, this would be the set gates supported by map_cirq_op_to_pyqir_callable(), I think? And then, perhaps there's code from qbraid_qir/qasm3/maps.py and/or qbraid_qir/qasm3/linalg.py that we can move to the top-level and generalize to expand that list or assist with some of the decompositions? What gates are included in the cirq.CZTargetGateset? If it maps to the pyqir gateset that we are targeting closely, I guess that could be a reasonable starting point.
@TheGupta2012 @skushnir123 perhaps you guys could take a look here and weigh in?
Or if @vtomole has any insights, as someone who is much closer to the Cirq stack, that could be super helpful as well.
cirq.CZTargetGateset is cirq.CZPowGate, PhasedXZGate, MeasurementGate, GlobalPhaseGate. Yes, it should indeed take the list of gates, but you need to implement the abstract method for decomposing gates. TwoQubitCompilationTargetGateset does some of the work and only requires you to implement a 2q unitary decomposition. Should we be implementing a Clifford-T decomposition like here and then replace iSWAP with SWAP + Xs?
What gateset do ya'll want to decompose to? The list in PYQIR_OP_MAP is kinda big. You can just decompose to Clifford + T if the the input gate is not in that list.
I'm unsure how to implement
self._decompose_two_qubit_operation
Why not copy CZTargetGateset's implementation?
I would copy their implementation and I could decompose PhasedXZ into {Rx,Rz}, but I'm unsure what to do about the CZPowGate and GlobalPhaseGate
CZ is in PYQIR_OP_MAP. Why not call two_qubit_matrix_to_cz_operations like CZTargetGateset does?
You can leave out GlobalPhaseGate as there's not a way to represent that in pyqir at the moment (to my knowledge).
And yes, like Victory said, the "CZ" gate is already accounted for in our current implementation, so if the CZPowGate exponent matches to a regular Pauli Z, then we can just map that directly to pyqir._native.cz. Otherwise, you can look at it like ControlledGate(ZPowGate) and follow the different ZPowGate exponent cases that are given in opsets.py:
https://github.com/qBraid/qbraid-qir/blob/4f009c3deffc2e6df26a740fa1efbb51a2a725de/qbraid_qir/cirq/opsets.py#L112-L118
I already call two_qubit_matrix_to_cz_operations at the moment. I'll use ^ for CZPowGate and will update afterwards. Thank you!
Tried this in 530a0cacf5a024ae606834ec38cb4e4ea2a11d23. Running into a max-recursion depth issue with decompose. Will debug through Cirq implementation tomorrow
The recursion issue is cause your decomposer is not getting to your target gateset. Here is a decomposer that will
# Copyright 2018 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Sequence, Union, Type, List
import cirq
from cirq.protocols.decompose_protocol import DecomposeResult
class QirTargetGateSet(cirq.TwoQubitCompilationTargetGateset):
def __init__(
self,
*,
atol: float = 1e-8,
allow_partial_czs: bool = False,
additional_gates: Sequence[
Union[Type["cirq.Gate"], "cirq.Gate", "cirq.GateFamily"]
] = (),
preserve_moment_structure: bool = True,
) -> None:
super().__init__(
cirq.IdentityGate,
cirq.HPowGate,
cirq.XPowGate,
cirq.YPowGate,
cirq.ZPowGate,
cirq.SWAP,
cirq.CNOT,
cirq.CZ,
cirq.TOFFOLI,
cirq.ResetChannel,
*additional_gates,
name="QirTargetGateset",
preserve_moment_structure=preserve_moment_structure,
)
self.allow_partial_czs = allow_partial_czs
self.atol = atol
@property
def postprocess_transformers(self) -> List["cirq.TRANSFORMER"]:
return []
def _decompose_single_qubit_operation(
self, op: "cirq.Operation", moment_idx: int
) -> DecomposeResult:
qubit = op.qubits[0]
mat = cirq.unitary(op)
for gate in cirq.single_qubit_matrix_to_gates(mat, self.atol):
yield gate(qubit)
def _decompose_two_qubit_operation(self, op: "cirq.Operation", _) -> "cirq.OP_TREE":
if not cirq.has_unitary(op):
return NotImplemented
return cirq.two_qubit_matrix_to_cz_operations(
op.qubits[0],
op.qubits[1],
cirq.unitary(op),
allow_partial_czs=self.allow_partial_czs,
atol=self.atol,
)
circuit = cirq.testing.random_circuit(qubits=4, n_moments=6, op_density=0.8)
# Compile the circuit for CZ+ QIR Target Gateset
gateset = QirTargetGateSet()
qir_circuit = cirq.optimize_for_target_gateset(circuit, gateset=gateset)
@ryanhill1 I've got this working (since the decomp is different w this approach the tests which manually check the qir need to be redone still), except for measurement. I'm considering using cirq.defer_measurements to move the measurements to the end of the circuit, pull them out, optimize the gates w.r.t. the gateset then add back the measurements. Is there a better approach that deals with measurement? For example, in the above code @vtomole, testing on the following circuit fails:
qubit = cirq.LineQubit.range(1)
circuit = cirq.Circuit()
ps = cirq.X(qubit[0])
meas_gates = cirq.measure_single_paulistring(ps)
circuit.append(meas_gates)
TypeError: cirq.unitary failed. Value doesn't have a (non-parameterized) unitary effect.
type: <class 'cirq.ops.gate_operation.GateOperation'>
value: cirq.measure_single_paulistring(((1+0j)*cirq.X(cirq.LineQubit(0))))
The value failed to satisfy any of the following criteria:
- A `_unitary_(self)` method that returned a value besides None or NotImplemented.
- A `_decompose_(self)` method that returned a list of unitary operations.
- An `_apply_unitary_(self, args) method that returned a value besides None or NotImplemented.
Add cirq.MeasurementGate to your gateset
super().__init__(
cirq.IdentityGate,
cirq.HPowGate,
cirq.XPowGate,
cirq.YPowGate,
cirq.ZPowGate,
cirq.SWAP,
cirq.CNOT,
cirq.CZ,
cirq.TOFFOLI,
cirq.ResetChannel,
cirq.MeasurementGate,
*additional_gates,
name="QirTargetGateset",
preserve_moment_structure=preserve_moment_structure,
)
Got it let me try this.
@vtomole I remember trying this actually. It doesn't work because it gives
cirq.unitary failed. Value doesn't have a (non-parameterized) unitary effect.
type: <class 'cirq.ops.gate_operation.GateOperation'>
value: cirq.measure_single_paulistring(((1+0j)*cirq.X(cirq.LineQubit(0))))
The value failed to satisfy any of the following criteria:
- A `_unitary_(self)` method that returned a value besides None or NotImplemented.
- A `_decompose_(self)` method that returned a list of unitary operations.
- An `_apply_unitary_(self, args) method that returned a value besides None or NotImplemented.
I think every thing in the gateset has to be a unitary so measurement isn't permitted
Oh! Add cirq.PauliMeasurementGate to your set also.
@vtomole most tests pass now, except the new optimize gateset prefers the decomposition of H into Ry and Z instead of leaving it as H. this causes some test failures. should we be avoiding this or can we change the hardcoded tests? the only other test that fails is conditional gates.
see run
I'll let @ryanhill1 speak on that.
The purpose of this "pre-processing" function is to decompose gates within the circuit that are not supported by pyqir into ones that are. Gates in the circuit that are already supported by pyqir shouldn't be decomposed any further.
except the new optimize gateset prefers the decomposition of H into Ry and Z instead of leaving it as H
@arulandu You can modify def _decompose_single_qubit_operation so it doesn't do this.
the only other test that fails is conditional gates.
You'll need to define a def _decompose_three_qubit_operation.
This issue is once again up for grabs. Feel free to work off of PR https://github.com/qBraid/qbraid-qir/pull/184