qbraid-qir icon indicating copy to clipboard operation
qbraid-qir copied to clipboard

Improve circuit pre-processing for `Cirq` converter with existing gate decomposers

Open ryanhill1 opened this issue 1 year ago • 22 comments

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:

  1. Bad style
  2. 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 avatar May 17 '24 23:05 ryanhill1

@ryanhill1 Is this still open? I'd love to work on this!

arulandu avatar Nov 08 '24 19:11 arulandu

@arulandu It is, go for it!

ryanhill1 avatar Nov 11 '24 15:11 ryanhill1

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?

arulandu avatar Nov 11 '24 19:11 arulandu

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.

ryanhill1 avatar Nov 11 '24 20:11 ryanhill1

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?

arulandu avatar Nov 11 '24 20:11 arulandu

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?

vtomole avatar Nov 11 '24 21:11 vtomole

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

arulandu avatar Nov 11 '24 22:11 arulandu

CZ is in PYQIR_OP_MAP. Why not call two_qubit_matrix_to_cz_operations like CZTargetGateset does?

vtomole avatar Nov 11 '24 22:11 vtomole

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

ryanhill1 avatar Nov 11 '24 22:11 ryanhill1

I already call two_qubit_matrix_to_cz_operations at the moment. I'll use ^ for CZPowGate and will update afterwards. Thank you!

arulandu avatar Nov 11 '24 23:11 arulandu

Tried this in 530a0cacf5a024ae606834ec38cb4e4ea2a11d23. Running into a max-recursion depth issue with decompose. Will debug through Cirq implementation tomorrow

arulandu avatar Nov 12 '24 02:11 arulandu

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)

vtomole avatar Nov 12 '24 19:11 vtomole

@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.

arulandu avatar Jan 08 '25 22:01 arulandu

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,
        )

vtomole avatar Jan 13 '25 21:01 vtomole

Got it let me try this.

arulandu avatar Jan 13 '25 21:01 arulandu

@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

arulandu avatar Jan 13 '25 23:01 arulandu

Oh! Add cirq.PauliMeasurementGate to your set also.

vtomole avatar Jan 13 '25 23:01 vtomole

@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

arulandu avatar Jan 14 '25 01:01 arulandu

I'll let @ryanhill1 speak on that.

vtomole avatar Jan 14 '25 02:01 vtomole

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.

ryanhill1 avatar Jan 14 '25 02:01 ryanhill1

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.

vtomole avatar Jan 14 '25 02:01 vtomole

This issue is once again up for grabs. Feel free to work off of PR https://github.com/qBraid/qbraid-qir/pull/184

ryanhill1 avatar May 20 '25 17:05 ryanhill1