diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0d5b58282..e46c0ff3a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,10 +2,18 @@ Changelog ========= -Version 17.2 +Version 17.3 ============ * Use the native ``reset`` operation to implement :class:`qiskit.circuit.Reset`. + `#148 `_ + +Version 17.2 +============ + +* Bugfix in :class:`IQMOptimizeSingleQubitGates`` where the angles are not properly computed for circuits + with prx gates on qubits holding a resonator state, i.e. circuits that require running without move gate validation. + `#147 `_ Version 17.1 ============ diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 05b1d328a..cb267f849 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -173,11 +173,11 @@ circuit(s) are sampled: .. note:: - IQM Client is transitioning from server-side API ``APIVariant.V1`` to a new ``APIVariant.V2``. + IQM Client is transitioning from server-side API ``V1`` to a new ``V2``. You may see a deprecation warning in the terminal. You can ignore it; when ``V2`` becomes the default, Qiskit-on-IQM will transparently start using it. Alternatively, you can set an environment variable - ``IQM_CLIENT_API_VARIANT`` to value ``APIVariant.V2``. The choice of the API version does not affect the use - of Qiskit-on-IQM. + ``IQM_CLIENT_API_VARIANT`` to value ``V2`` and change the backend URL (normally, this means removing + the ``/cocos`` part from the url). The choice of the API version does not affect the use of Qiskit-on-IQM. You can optionally provide IQMBackend specific options as additional keyword arguments to :meth:`.IQMBackend.run`, documented at :meth:`.IQMBackend.create_run_request`. diff --git a/src/iqm/qiskit_iqm/iqm_transpilation.py b/src/iqm/qiskit_iqm/iqm_transpilation.py index 9985bf875..4d9b92df3 100644 --- a/src/iqm/qiskit_iqm/iqm_transpilation.py +++ b/src/iqm/qiskit_iqm/iqm_transpilation.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Transpilation tool to optimize the decomposition of single-qubit gates tailored to IQM hardware.""" +import math import warnings import numpy as np @@ -23,6 +24,8 @@ from qiskit.transpiler.passes import BasisTranslator, Optimize1qGatesDecomposition, RemoveBarriers from qiskit.transpiler.passmanager import PassManager +TOLERANCE = 1e-10 # The tolerance for equivalence checking against zero. + class IQMOptimizeSingleQubitGates(TransformationPass): r"""Optimize the decomposition of single-qubit gates for the IQM gate set. @@ -55,6 +58,7 @@ def __init__(self, drop_final_rz: bool = True, ignore_barriers: bool = False): self._ignore_barriers = ignore_barriers def run(self, dag: DAGCircuit) -> DAGCircuit: + # pylint: disable=too-many-branches self._validate_ops(dag) # accumulated RZ angles for each qubit, from the beginning of the circuit to the current gate rz_angles: list[float] = [0] * dag.num_qubits() @@ -68,9 +72,12 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: for node in dag.topological_op_nodes(): if node.name == 'u': qubit_index = dag.find_bit(node.qargs[0])[0] - dag.substitute_node( - node, RGate(node.op.params[0], np.pi / 2 - node.op.params[2] - rz_angles[qubit_index]) - ) + if math.isclose(node.op.params[0], 0, abs_tol=TOLERANCE): + dag.remove_op_node(node) + else: + dag.substitute_node( + node, RGate(node.op.params[0], np.pi / 2 - node.op.params[2] - rz_angles[qubit_index]) + ) phase = node.op.params[1] + node.op.params[2] dag.global_phase += phase / 2 rz_angles[qubit_index] += phase @@ -89,6 +96,15 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: # been transformed). This choice of local phases is in principle arbitrary, so maybe it # makes no sense to convert it into active z rotations if we hit a barrier? pass + elif node.name == 'move': + qb, res = dag.find_bit(node.qargs[0])[0], dag.find_bit(node.qargs[1])[0] + rz_angles[res], rz_angles[qb] = rz_angles[qb], rz_angles[res] + elif node.name == 'cz': + pass # rz_angles are commute through CZ gates + else: + raise ValueError( + f'Unexpected operation {node.name} in circuit given to IQMOptimizeSingleQubitGates pass' + ) if not self._drop_final_rz: for qubit_index, rz_angle in enumerate(rz_angles): diff --git a/tests/fake_backends/test_fake_deneb.py b/tests/fake_backends/test_fake_deneb.py index 58233d17f..218d7468b 100644 --- a/tests/fake_backends/test_fake_deneb.py +++ b/tests/fake_backends/test_fake_deneb.py @@ -204,18 +204,21 @@ def test_qiskit_transpile_for_ghz_with_fake_deneb_noise_model(): def test_transpiling_works_but_backend_run_doesnt_with_unsupported_gates(): backend = IQMFakeDeneb() - num_qb = 6 + num_qb = 1 qc_list = [] for _ in range(4): qc_list.append(QuantumCircuit(num_qb)) - qc_list[0].h(1) - qc_list[1].sdg(2) - qc_list[2].t(3) - qc_list[3].s(4) + qc_list[0].h(0) + qc_list[1].sdg(0) + qc_list[2].t(0) + qc_list[3].s(0) + + for i in range(4): + qc_list[i].measure_all() for qc in qc_list: - backend.run(transpile(qc, backend=backend), shots=1000) + backend.run(transpile_to_IQM(qc, backend=backend), shots=1000) with pytest.raises( ValueError, diff --git a/tests/test_iqm_transpilation.py b/tests/test_iqm_transpilation.py index 113347e41..d27bff28d 100644 --- a/tests/test_iqm_transpilation.py +++ b/tests/test_iqm_transpilation.py @@ -14,9 +14,14 @@ """Testing IQM transpilation. """ +import math + import numpy as np import pytest from qiskit import QuantumCircuit, transpile +from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary +from qiskit.transpiler import PassManager +from qiskit.transpiler.passes import BasisTranslator from qiskit_aer import AerSimulator from iqm.qiskit_iqm.fake_backends.fake_adonis import IQMFakeAdonis @@ -24,7 +29,8 @@ from iqm.qiskit_iqm.fake_backends.fake_deneb import IQMFakeDeneb from iqm.qiskit_iqm.iqm_circuit_validation import validate_circuit from iqm.qiskit_iqm.iqm_move_layout import generate_initial_layout -from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates +from iqm.qiskit_iqm.iqm_transpilation import TOLERANCE, IQMOptimizeSingleQubitGates, optimize_single_qubit_gates +from iqm.qiskit_iqm.move_gate import MoveGate from tests.utils import get_mocked_backend @@ -150,3 +156,23 @@ def test_qiskit_native_transpiler(move_architecture, optimization_level): qc.measure_all() transpiled_circuit = transpile(qc, backend=backend, optimization_level=optimization_level) validate_circuit(transpiled_circuit, backend) + + +def test_optimize_single_qubit_gates_works_on_invalid_move_sandwich(): + """Tests that the optimization pass works on a circuit with an invalid MOVE sandwich. + In case the user is wanting to use the higher energy levels but also optimize the SQGs in the circuit.""" + qc = QuantumCircuit(2) + qc.rz(0.5, 1) + qc.append(MoveGate(), [1, 0]) + qc.x(1) + qc.append(MoveGate(), [1, 0]) + basis_circuit = PassManager([BasisTranslator(SessionEquivalenceLibrary, ['r', 'cz', 'move'])]).run(qc) + transpiled_circuit = PassManager( + [BasisTranslator(SessionEquivalenceLibrary, ['r', 'cz', 'move']), IQMOptimizeSingleQubitGates(True, True)] + ).run(basis_circuit) + assert transpiled_circuit.count_ops()['r'] == 1 + assert transpiled_circuit.count_ops()['move'] == 2 + for gate in transpiled_circuit: + if gate.operation.name == 'r': + assert math.isclose(gate.operation.params[0], np.pi, rel_tol=TOLERANCE) + assert math.isclose(gate.operation.params[1], 0, abs_tol=TOLERANCE)