Repository: Qiskit/qiskit — pull request #12511 (python)
Title: fix issue/12311 with fractional gate in basis

<!--\r\n⚠️ If you do not respect this template, your pull request will be closed.\r\n⚠️ Your pull request title should be short detailed and understandable for all.\r\n⚠️ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠️ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [x] I have added the tests to cover my changes.\r\n- [x] I have updated the documentation accordingly.\r\n- [x] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nThis fixes an issue where an exception would be raised if transpiling a unitary gate to a backend with an RZZGate in its basis however transpiling with only `basis_gates` specified would transpile fine. \r\n\r\nfixes #12311 \r\n\r\n### Details and comments\r\nIn this case, where RZZ is the only entangling gate in the basis, decomposition of a 2q unitary gate wouldn't get synthesized in the `UnitarySynthesis` pass but would instead be synthesized in the `HighLevelSynthesis` pass because this would call the instruction's `_define` method which calls `two_qubit_cnot_decompose`. A subsequent `BasisTranslator` pass ultimately maps the cx to the rzz basis.  I'm not sure whether this indirect path to unitary synthesis is intentional.\r\n\r\nThis pr resolves the specific example posed in the issue.\r\n\r\n

Resolved issues:
title: 2Q gate synthesis with fractional gates
body: ### Environment\r\n\r\n- **Qiskit version**: main\r\n- **Python version**: doesn't matter\r\n- **Operating system**: doesn't matter\r\n\r\n\r\n### What is happening?\r\n\r\nWhen I build a Qiskit `Target` that supports two qubit basis gates with arbitrary rotation angle, it cannot transpile the circuit if the added basis gate is other than `RZXGate` or `RXXGate`.\r\n\r\n### How can we reproduce the issue?\r\n\r\n```python\r\nfrom qiskit.providers.fake_provider import GenericBackendV2\r\nfrom qiskit import quantum_info as qi, transpile, QuantumCircuit\r\n\r\nbackend = GenericBackendV2(2, basis_gates=["rz", "rx", "rzz"])\r\nqc = QuantumCircuit(2)\r\nqc.unitary(qi.random_unitary(4), [0, 1])\r\nisa = transpile(qc, target=backend.target)\r\n```\r\nthis results in an error\r\n```\r\nTypeError: ParameterExpression with unbound parameters (dict_keys([Parameter(ϴ)])) cannot be cast to a float.\r\n```\r\nbecause it tries to get the unitary matrix of parameterized `RZZGate(Parameter(ϴ))` instruction.\r\n\r\nOn the other hand, it transpiles circuit when I directly specify the basis gates.\r\n```python\r\nisa = transpile(qc, basis_gates=["rz", "rx", "rzz"])\r\n```\r\n\r\n### What should happen?\r\n\r\n- Qiskit transpiler should support arbitrary fractional two qubit basis gates, e.g. `RZX, RXX, RYY, RZZ, RXY, ...`. \r\n- Transpiling with `Target` and giving `basis_gates` should behave identically.\r\n\r\nThis is related but might be a feature request; even if I give `RZXGate(Parameter(ϴ))` in the target, current synthesis algorithm only picks pi/4 rotation. The synthesis pass should consider another angle if there is possibility of reducing the circuit depth.\r\n\r\n### Any suggestions?\r\n\r\nNo. I'm not familiar with synthesis passes.

Hints:

Fix patch:
diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py\nindex ab7c5e04649f..19356adf69c9 100644\n--- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py\n+++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py\n@@ -49,6 +49,7 @@\n     CZGate,\n     RXXGate,\n     RZXGate,\n+    RZZGate,\n     ECRGate,\n     RXGate,\n     SXGate,\n@@ -779,6 +780,8 @@ def _replace_parameterized_gate(op):\n                 op = RXXGate(pi / 2)\n             elif isinstance(op, RZXGate) and isinstance(op.params[0], Parameter):\n                 op = RZXGate(pi / 4)\n+            elif isinstance(op, RZZGate) and isinstance(op.params[0], Parameter):\n+                op = RZZGate(pi / 2)\n             return op\n \n         try:\n

Test patch:
diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py\nindex fc933cd8f666..e1c8063a4575 100644\n--- a/test/python/transpiler/test_basis_translator.py\n+++ b/test/python/transpiler/test_basis_translator.py\n@@ -16,6 +16,7 @@\n import os\n \n from numpy import pi\n+import scipy\n \n from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit\n from qiskit import transpile\n@@ -33,13 +34,17 @@\n     XGate,\n     SXGate,\n     CXGate,\n+    RXGate,\n+    RZZGate,\n )\n from qiskit.converters import circuit_to_dag, dag_to_circuit, circuit_to_instruction\n from qiskit.exceptions import QiskitError\n+from qiskit.providers.fake_provider import GenericBackendV2\n from qiskit.quantum_info import Operator\n from qiskit.transpiler.target import Target, InstructionProperties\n from qiskit.transpiler.exceptions import TranspilerError\n from qiskit.transpiler.passes.basis import BasisTranslator, UnrollCustomDefinitions\n+from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n from qiskit.circuit.library.standard_gates.equivalence_library import (\n     StandardEquivalenceLibrary as std_eqlib,\n )\n@@ -1223,3 +1228,48 @@ def __init__(self):\n \n         out = BasisTranslator(eq_lib, {"my_h", "my_cx"}, target)(qc)\n         self.assertEqual(out, expected)\n+\n+    def test_fractional_gate_in_basis_from_string(self):\n+        """Test transpiling with RZZ in basis with only basis_gates option."""\n+        num_qubits = 2\n+        seed = 9169\n+        basis_gates = ["rz", "rx", "rzz"]\n+        qc = QuantumCircuit(num_qubits)\n+        mat = scipy.stats.unitary_group.rvs(2**num_qubits, random_state=seed)\n+        qc.unitary(mat, range(num_qubits))\n+        pm = generate_preset_pass_manager(\n+            optimization_level=1, basis_gates=basis_gates, seed_transpiler=134\n+        )\n+        cqc = pm.run(qc)\n+        self.assertEqual(Operator(qc), Operator(cqc))\n+\n+    def test_fractional_gate_in_basis_from_backendv2(self):\n+        """Test transpiling with RZZ in basis of backendv2."""\n+        num_qubits = 2\n+        seed = 9169\n+        basis_gates = ["rz", "rx", "rzz"]\n+        qc = QuantumCircuit(num_qubits)\n+        mat = scipy.stats.unitary_group.rvs(2**num_qubits, random_state=seed)\n+        qc.unitary(mat, range(num_qubits))\n+        backend = GenericBackendV2(num_qubits, basis_gates=basis_gates)\n+        target = backend.target\n+        pm = generate_preset_pass_manager(optimization_level=1, target=target, seed_transpiler=134)\n+        cqc = pm.run(qc)\n+        self.assertEqual(Operator(qc), Operator.from_circuit(cqc))\n+\n+    def test_fractional_gate_in_basis_from_custom_target(self):\n+        """Test transpiling with RZZ in basis of custom target."""\n+        num_qubits = 2\n+        seed = 9169\n+        qc = QuantumCircuit(num_qubits)\n+        mat = scipy.stats.unitary_group.rvs(2**num_qubits, random_state=seed)\n+        qc.unitary(mat, range(num_qubits))\n+        target = Target()\n+        target.add_instruction(RZGate(self.theta), {(i,): None for i in range(qc.num_qubits)})\n+        target.add_instruction(RXGate(self.phi), {(i,): None for i in range(qc.num_qubits)})\n+        target.add_instruction(\n+            RZZGate(self.lam), {(i, i + 1): None for i in range(qc.num_qubits - 1)}\n+        )\n+        pm = generate_preset_pass_manager(optimization_level=1, target=target, seed_transpiler=134)\n+        cqc = pm.run(qc)\n+        self.assertEqual(Operator(qc), Operator.from_circuit(cqc))\n