From 1e8bf1e54457d3178d1c574fea0de21a3d2862b1 Mon Sep 17 00:00:00 2001 From: richrines1 <85512171+richrines1@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:36:18 -0600 Subject: [PATCH] rewrite `qss.serialize_circuits` to support newest qiskit (#854) also adds workarounds for a number of known qpy bugs, and fixes some bugs in our previous implementation (which didn't recurse into gate .definitions, and therefore could break or silently modify gates if circuits weren't flattened prior to submission) fixes: #837 (notebook checks won't pass until after this is deployed) --- checks-superstaq/requirements.txt | 2 +- qiskit-superstaq/qiskit_superstaq/__init__.py | 3 +- .../qiskit_superstaq/custom_gates.py | 49 +-- .../qiskit_superstaq/custom_gates_test.py | 68 +--- .../qiskit_superstaq/serialization.py | 331 ++++++++++++++---- .../qiskit_superstaq/serialization_test.py | 220 ++++++++++-- qiskit-superstaq/requirements.txt | 3 +- 7 files changed, 461 insertions(+), 215 deletions(-) diff --git a/checks-superstaq/requirements.txt b/checks-superstaq/requirements.txt index d4960ecaf..e441e13dc 100644 --- a/checks-superstaq/requirements.txt +++ b/checks-superstaq/requirements.txt @@ -1,4 +1,4 @@ -black[jupyter]>=23.1.0 +black[jupyter,colorama]>=23.1.0 flake8>=3.8.4 flake8-pyproject>=1.2.3 flake8-type-checking>=2.1.0 diff --git a/qiskit-superstaq/qiskit_superstaq/__init__.py b/qiskit-superstaq/qiskit_superstaq/__init__.py index b64806422..86fef3c5b 100644 --- a/qiskit-superstaq/qiskit_superstaq/__init__.py +++ b/qiskit-superstaq/qiskit_superstaq/__init__.py @@ -1,4 +1,4 @@ -from . import compiler_output, serialization, validation +from . import compiler_output, custom_gates, serialization, validation from ._version import __version__ from .compiler_output import active_qubit_indices, measured_clbit_indices, measured_qubit_indices from .custom_gates import ( @@ -20,6 +20,7 @@ "AQTiCCXGate", "AQTiToffoliGate", "compiler_output", + "custom_gates", "deserialize_circuits", "measured_qubit_indices", "measured_clbit_indices", diff --git a/qiskit-superstaq/qiskit_superstaq/custom_gates.py b/qiskit-superstaq/qiskit_superstaq/custom_gates.py index f7c5ab893..1142c8c6f 100644 --- a/qiskit-superstaq/qiskit_superstaq/custom_gates.py +++ b/qiskit-superstaq/qiskit_superstaq/custom_gates.py @@ -1,7 +1,7 @@ from __future__ import annotations import functools -from typing import Callable, Dict, Optional, Tuple, Union +from typing import Optional, Tuple, Union import numpy as np import numpy.typing as npt @@ -505,50 +505,3 @@ def __init__(self, label: Optional[str] = None) -> None: AQTiToffoliGate = AQTiCCXGate - - -_custom_gate_resolvers: Dict[str, Callable[..., qiskit.circuit.Gate]] = { - "acecr": lambda rads: AceCR(rads=rads), - "acecr_rx": lambda *rads: AceCR(*rads), - "zzswap": ZZSwapGate, - "ix": iXGate, - "ixdg": iXdgGate, - "iccx": iCCXGate, - "iccx_o0": AQTiCCXGate, - "iccx_o1": lambda: iCCXGate(ctrl_state="01"), - "iccx_o2": lambda: iCCXGate(ctrl_state="10"), - "iccxdg": iCCXdgGate, - "iccxdg_o0": lambda: iCCXdgGate(ctrl_state="00"), - "iccxdg_o1": lambda: iCCXdgGate(ctrl_state="01"), - "iccxdg_o2": lambda: iCCXdgGate(ctrl_state="10"), -} - - -def custom_resolver(gate: qiskit.circuit.Instruction) -> Optional[qiskit.circuit.Gate]: - """Recovers a custom gate type from a generic `qiskit.circuit.Gate`. - - The resolution is done using `gate.definition.name` rather than `gate.name`, as the former - is set by all `qiskit-superstaq` custom gates and the latter may be modified by calls - such as `qiskit.QuantumCircuit.qasm()`. - - Args: - gate: The input gate instruction from which to recover a custom gate type. - - Returns: - A `qiskit.circuit.Gate` if the gate definition name is in the `_custom_gate_resolver` - dictionary (or the definition name is "parallel_gates"). - """ - - if gate.definition and gate.definition.name == "parallel_gates": - component_gates = [custom_resolver(inst) or inst for inst, _, _ in gate.definition] - return ParallelGates(*component_gates, label=gate.label) - - if gate.definition and gate.definition.name in _custom_gate_resolvers: - new_gate = _custom_gate_resolvers[gate.definition.name](*gate.params) - elif gate.name in _custom_gate_resolvers: - new_gate = _custom_gate_resolvers[gate.name](*gate.params) - else: - return None - - new_gate.label = gate.label - return new_gate diff --git a/qiskit-superstaq/qiskit_superstaq/custom_gates_test.py b/qiskit-superstaq/qiskit_superstaq/custom_gates_test.py index 37ea52ccd..7c482bd77 100644 --- a/qiskit-superstaq/qiskit_superstaq/custom_gates_test.py +++ b/qiskit-superstaq/qiskit_superstaq/custom_gates_test.py @@ -1,5 +1,5 @@ # pylint: disable=missing-function-docstring,missing-class-docstring -from typing import List, Set +from typing import Set import numpy as np import pytest @@ -219,69 +219,3 @@ def test_aqticcx() -> None: ) np.allclose(qiskit.quantum_info.Operator(qc), correct_unitary) - - -def test_custom_resolver() -> None: - custom_gates: List[qiskit.circuit.Gate] = [ - qss.AceCR("+-"), - qss.AceCR("-+"), - qss.AceCR("+-", sandwich_rx_rads=1.23), - qss.AceCR(np.pi), - qss.AceCR(np.pi / 3, np.pi / 2), - qss.AceCR(0), - qss.ZZSwapGate(1.23), - qss.AQTiCCXGate(), - qss.custom_gates.iXGate(), - qss.custom_gates.iXdgGate(), - qss.custom_gates.iCCXGate(), - qss.custom_gates.iCCXGate(ctrl_state="01"), - qss.custom_gates.iCCXGate(ctrl_state="10"), - qss.custom_gates.iCCXdgGate(ctrl_state="00"), - qss.custom_gates.iCCXdgGate(), - qss.custom_gates.iCCXdgGate(ctrl_state="01"), - qss.custom_gates.iCCXdgGate(ctrl_state="10"), - ] - - generic_gates = [] - - for custom_gate in custom_gates: - generic_gate = qiskit.circuit.Gate( - custom_gate.name, - custom_gate.num_qubits, - custom_gate.params, - label=str(id(custom_gate)), - ) - generic_gate.definition = custom_gate.definition - generic_gates.append(generic_gate) - assert generic_gate != custom_gate - - resolved_gate = qss.custom_gates.custom_resolver(generic_gate) - assert resolved_gate == custom_gate - assert resolved_gate.label == str(id(custom_gate)) # (labels aren't included in __eq__) - - parallel_gates = qss.ParallelGates( - qiskit.circuit.library.RXGate(4.56), qiskit.circuit.library.CXGate(), *custom_gates - ) - parallel_generic_gates = qss.ParallelGates( - qiskit.circuit.library.RXGate(4.56), - qiskit.circuit.library.CXGate(), - *generic_gates, - label="label-1" - ) - resolved_gate = qss.custom_gates.custom_resolver(parallel_generic_gates) - assert parallel_generic_gates != parallel_gates - assert resolved_gate == parallel_gates - assert resolved_gate.label == "label-1" - - generic_parallel_gates = qiskit.circuit.Gate( - parallel_gates.name, parallel_gates.num_qubits, [], label="label-2" - ) - generic_parallel_gates.definition = parallel_generic_gates.definition - resolved_gate = qss.custom_gates.custom_resolver(generic_parallel_gates) - assert generic_parallel_gates != parallel_gates - assert resolved_gate == parallel_gates - assert resolved_gate.label == "label-2" - - assert qss.custom_gates.custom_resolver(qiskit.circuit.library.CXGate()) is None - assert qss.custom_gates.custom_resolver(qiskit.circuit.library.RXGate(2)) is None - assert qss.custom_gates.custom_resolver(qiskit.circuit.Gate("??", 1, [])) is None diff --git a/qiskit-superstaq/qiskit_superstaq/serialization.py b/qiskit-superstaq/qiskit_superstaq/serialization.py index 628a7b40c..db44b3826 100644 --- a/qiskit-superstaq/qiskit_superstaq/serialization.py +++ b/qiskit-superstaq/qiskit_superstaq/serialization.py @@ -1,21 +1,50 @@ +from __future__ import annotations + import io import json import re import warnings -from typing import Dict, List, Sequence, Set, Tuple, TypeVar, Union +from typing import Callable, Dict, List, Sequence, TypeVar, Union import general_superstaq as gss import numpy as np import numpy.typing as npt -import qiskit.qasm2 import qiskit.qpy -from qiskit.converters.ast_to_dag import AstInterpreter +import qiskit_ibm_provider import qiskit_superstaq as qss T = TypeVar("T") RealArray = Union[int, float, List["RealArray"]] +# Custom gate types to resolve when deserializing circuits +# MSGate included as a workaround for https://github.com/Qiskit/qiskit/issues/11378 +_custom_gates_by_name: Dict[str, type[qiskit.circuit.Instruction]] = { + "acecr": qss.custom_gates.AceCR, + "parallel": qss.custom_gates.ParallelGates, + "stripped_cz": qss.custom_gates.StrippedCZGate, + "zzswap": qss.custom_gates.ZZSwapGate, + "ix": qss.custom_gates.iXGate, + "ixdg": qss.custom_gates.iXdgGate, + "iccx": qss.custom_gates.iCCXGate, + "iccxdg": qss.custom_gates.iCCXdgGate, + "ms": qiskit.circuit.library.MSGate, +} + +# Custom resolvers, necessary when `gate != type(gate)(*gate.params)` +# MSGate included as a workaround for https://github.com/Qiskit/qiskit/issues/11378 +_custom_resolvers: Dict[ + type[qiskit.circuit.Instruction], + Callable[[qiskit.circuit.Instruction], qiskit.circuit.Instruction], +] = { + qss.custom_gates.ParallelGates: lambda gate: qss.custom_gates.ParallelGates( + *[_resolve_gate(inst.operation) for inst in gate.definition], label=gate.label + ), + qiskit.circuit.library.MSGate: lambda gate: qiskit.circuit.library.MSGate( + gate.num_qubits, gate.params[0], label=gate.label + ), +} + def json_encoder(val: object) -> Dict[str, Union[str, RealArray]]: """Convert (real or complex) arrays to a JSON-serializable format. @@ -68,58 +97,6 @@ def to_json(val: object) -> str: return json.dumps(val, default=json_encoder) -def _assign_unique_inst_names(circuit: qiskit.QuantumCircuit) -> qiskit.QuantumCircuit: - """This function rewrites the input circuit with new instruction names. - - QPY requires unique custom gates to have unique `.name` attributes (including parameterized - gates differing by just their `.params` attributes). This function does so by appending a - unique (consecutive) "_{index}" string to the name of any custom instruction which shares - a name with a non-equivalent prior instruction in the circuit. - - Args: - circuit: The `qiskit.QuantumCircuit` to be rewritten. - - Returns: - A copy of the input circuit with unique custom instruction names. - """ - - unique_insts_by_name: Dict[str, List[qiskit.circuit.Instruction]] = {} - insts_to_update: List[Tuple[int, int]] = [] - unique_inst_ids: Set[int] = set() - - qiskit_gates = set(AstInterpreter.standard_extension) | {"measure"} - - new_circuit = circuit.copy() - for inst, _, _ in new_circuit: - if inst.name in qiskit_gates or id(inst) in unique_inst_ids: - continue - - if not isinstance(inst, qiskit.qasm2.parse._DefinedGate): - inst._define() - - # save id() in case instruction instance is used more than once - unique_inst_ids.add(id(inst)) - - if inst.name in unique_insts_by_name: - index = 0 - for other in unique_insts_by_name[inst.name]: - if inst == other: - break - index += 1 - - if index == len(unique_insts_by_name[inst.name]): - unique_insts_by_name[inst.name].append(inst) - if index > 0: - insts_to_update.append((inst, index)) - else: - unique_insts_by_name[inst.name] = [inst] - - for inst, index in insts_to_update: - inst.name += f"_{index}" - - return new_circuit - - def serialize_circuits( circuits: Union[qiskit.QuantumCircuit, Sequence[qiskit.QuantumCircuit]] ) -> str: @@ -132,12 +109,16 @@ def serialize_circuits( A string representing the serialized circuit(s). """ if isinstance(circuits, qiskit.QuantumCircuit): - circuits = [_assign_unique_inst_names(circuits)] + circuits = [_prepare_circuit(circuits)] else: - circuits = [_assign_unique_inst_names(circuit) for circuit in circuits] + circuits = [_prepare_circuit(circuit) for circuit in circuits] + # Use `qiskit_ibm_provider.qpy` for serialization, which is a delayed copy of `qiskit.qpy`. + # Deserialization can't be done with a QPY version older than that used for serialization, so + # this prevents us from having to force users to update Qiskit the moment we do (this is what + # Qiskit itself does for circuit submission) buf = io.BytesIO() - qiskit.qpy.dump(circuits, buf) + qiskit_ibm_provider.qpy.dump(circuits, buf) return gss.serialization.bytes_to_str(buf.getvalue()) @@ -167,8 +148,8 @@ def deserialize_circuits(serialized_circuits: str) -> List[qiskit.QuantumCircuit # If the circuit was serialized with a newer version of QPY, that's probably what caused # this error. In this case we should just tell the user to update. raise ValueError( - "Circuits failed to deserialize. This is likely because your version of " - f"qiskit-terra ({qiskit.__version__}) is out of date. Consider updating it." + "Circuits failed to deserialize. This is likely because your version of Qiskit " + f"({qiskit.__version__}) is out of date. Consider updating it." ) else: # Otherwise there is probably a more complicated issue. @@ -177,14 +158,230 @@ def deserialize_circuits(serialized_circuits: str) -> List[qiskit.QuantumCircuit "report at https://github.com/Infleqtion/client-superstaq/issues containing " "the following information (as well as any other relevant context):\n\n" f"qiskit-superstaq version: {qss.__version__}\n" - f"qiskit-terra version: {qiskit.__version__}\n" + f"qiskit version: {qiskit.__version__}\n" f"error: {e!r}" ) - for circuit in circuits: - for pc, (inst, qargs, cargs) in enumerate(circuit._data): - new_inst = qss.custom_gates.custom_resolver(inst) - if new_inst is not None: - circuit._data[pc] = qiskit.circuit.CircuitInstruction(new_inst, qargs, cargs) + return [_resolve_circuit(circuit) for circuit in circuits] + + +def _is_qiskit_gate(gate: qiskit.circuit.Instruction) -> bool: + """Returns True if `gate` will be correctly resolved by QPY.""" + base_class = getattr(gate, "base_class", type(gate)) + + return ( + issubclass(base_class, qiskit.circuit.Instruction) + and base_class.__module__.startswith("qiskit.") + and base_class + not in ( + qiskit.circuit.Instruction, + qiskit.circuit.Gate, + qiskit.circuit.ControlledGate, + ) + and not issubclass( # Qiskit mishandles these gates + base_class, + ( + qiskit.circuit.library.MSGate, # https://github.com/Qiskit/qiskit/issues/11378 + qiskit.circuit.library.MCXGate, # https://github.com/Qiskit/qiskit/issues/11377 + qiskit.circuit.library.MCU1Gate, + qiskit.circuit.library.MCPhaseGate, + ), + ) + and ( + hasattr(qiskit.circuit.library, base_class.__name__) + or hasattr(qiskit.circuit, base_class.__name__) + or hasattr(qiskit.extensions, base_class.__name__) + or hasattr(qiskit.extensions.quantum_initializer, base_class.__name__) + or hasattr(qiskit.circuit.controlflow, base_class.__name__) + ) + ) + + +def _prepare_circuit(circuit: qiskit.QuantumCircuit) -> qiskit.QuantumCircuit: + """Rewrites the input circuit in anticipation of QPY serialization. + + This is intended to be run prior to serialization as a workaround for various known QPY issues, + namely: + * https://github.com/Qiskit/qiskit/issues/11378 (mishandling of `MSGate`) + * https://github.com/Qiskit/qiskit/issues/11377 (mishandling of multi-controlled gates) + * https://github.com/Qiskit/qiskit/issues/8941 (incorrect definitions for gates sharing a name) + * https://github.com/Qiskit/qiskit/issues/8794 (serialization error for non-default ctrl_state) + * https://github.com/Qiskit/qiskit/issues/8549 (incorrect gate names in deserialized circuit) + + Most significantly (#8941 above), QPY requires unique custom gates to have unique `.name` + attributes (including parameterized gates differing by just their `.params` attributes). This + routine ensures this by wrapping unequal gates with the same name into uniquely-named temporary + instructions. The original circuit can then be recovered using the `_resolve_circuit` function + below. + + Args: + circuit: The `qiskit.QuantumCircuit` to be rewritten. + + Returns: + A copy of the input circuit with unique custom instruction names. + """ + old_gates_by_name = {} + new_gates_by_name = {} + + def _update_gate(gate: qiskit.circuit.Instruction) -> qiskit.circuit.Instruction: + # Control flow operations contain nested circuit blocks; prepare them first + if isinstance(gate, qiskit.circuit.ControlFlowOp): + gate = gate.replace_blocks([_prepare_circuit(block) for block in gate.blocks]) + + # Check if this is a gate QPY already handles correctly + if _is_qiskit_gate(gate): + return gate + + if gate.name not in old_gates_by_name: + new_gate = _prepare_gate(gate) + old_gates_by_name[gate.name] = [gate] + new_gates_by_name[gate.name] = [new_gate] + return new_gate + + for i, other in enumerate(old_gates_by_name[gate.name]): + if gate is other or gate == other: + return new_gates_by_name[gate.name][i] + + # Workaround for https://github.com/Qiskit/qiskit/issues/8941: wrap gate in a temporary + # instruction to prevent `.definition` from being overwritten + new_gate = _wrap_gate(gate) + old_gates_by_name[gate.name].append(gate) + new_gates_by_name[gate.name].append(new_gate) + return new_gate + + new_circuit = circuit.copy_empty_like() + for inst in circuit: + new_inst = inst.replace(operation=_update_gate(inst.operation)) + new_circuit.append(new_inst) + + return new_circuit + + +def _prepare_gate( + gate: qiskit.circuit.Instruction, force_wrapper: bool = False +) -> qiskit.circuit.Instruction: + # Check if this is a gate QPY already handles + if _is_qiskit_gate(gate): + return gate + + # Workaround for https://github.com/Qiskit/qiskit/issues/8794 + if isinstance(gate, qiskit.circuit.ControlledGate): + if gate.definition is not None and not gate._definition: + gate._define() + + if isinstance(gate, tuple(_custom_gates_by_name.values())) and not isinstance( + gate, tuple(_custom_resolvers) + ): + return gate + + if isinstance(gate, qiskit.circuit.ControlledGate): + return qiskit.circuit.ControlledGate( + name=gate._name, + num_qubits=gate.num_qubits, + params=gate.params, + label=gate.label, + num_ctrl_qubits=gate.num_ctrl_qubits, + definition=_prepare_circuit(gate._definition), + ctrl_state=gate.ctrl_state, + base_gate=_prepare_gate(gate.base_gate), + ) + + if isinstance(gate, qiskit.circuit.Gate): + new_gate = qiskit.circuit.Gate( + gate.name, + num_qubits=gate.num_qubits, + params=gate.params, + label=gate.label, + ) + + else: + new_gate = qiskit.circuit.Instruction( + gate.name, + num_qubits=gate.num_qubits, + num_clbits=gate.num_clbits, + params=gate.params, + label=gate.label, + ) + + if gate.definition: + new_gate.definition = _prepare_circuit(gate.definition) + + return new_gate + + +def _wrap_gate(gate: qiskit.circuit.Instruction) -> qiskit.circuit.Instruction: + """Wrap `gate` in a uniquely=name instruction containing only that gate in its `.definition`. + + This functions as a workaround for https://github.com/Qiskit/qiskit/issues/8941. + """ + + name = f"__superstaq_wrapper_{id(gate)}" + circuit = qiskit.QuantumCircuit(gate.num_qubits, gate.num_clbits, name=name) + circuit.append(_prepare_gate(gate), range(gate.num_qubits), range(gate.num_clbits)) + new_gate = circuit.to_instruction(label=gate.name) + + # For backwards compatibility + compat_name = "parallel_gates" if isinstance(gate, qss.ParallelGates) else gate.name + new_gate.definition.name = compat_name + new_gate.params.extend(gate.params) + + return new_gate + + +def _resolve_circuit(circuit: qiskit.QuantumCircuit) -> qiskit.QuantumCircuit: + """Reverse of the transformation performed by `_prepare_circuit`. + + This is intended to be run after deserialization in order to recover the names and types of + instructions in the original circuit. + """ + new_circuit = circuit.copy_empty_like() + for inst in circuit: + inst = inst.replace(operation=_resolve_gate(inst.operation)) + new_circuit.append(inst) + return new_circuit + + +def _resolve_gate(gate: qiskit.circuit.Instruction) -> qiskit.circuit.Instruction: + if gate.name.startswith(r"__superstaq_wrapper_"): + return _resolve_gate(gate.definition[0].operation) + + elif isinstance(gate, qiskit.circuit.ControlFlowOp): + return gate.replace_blocks([_resolve_circuit(block) for block in gate.blocks]) + + elif type(gate) is qiskit.circuit.ControlledGate: + gate.base_gate = _resolve_gate(gate.base_gate) + gate.name = gate._name.rsplit("_", 1)[0] # https://github.com/Qiskit/qiskit/issues/8549 + + if gate.definition is not None and gate._definition is not None: + gate.definition = _resolve_circuit(gate._definition) + + elif type(gate) in (qiskit.circuit.Instruction, qiskit.circuit.Gate): + if gate.definition: + gate.definition = _resolve_circuit(gate.definition) + + else: + return gate + + return _resolve_custom_gate(gate) + + +def _resolve_custom_gate(gate: qiskit.circuit.Instruction) -> qiskit.circuit.Instruction: + for name, trial_class in _custom_gates_by_name.items(): + if gate.name == name or gate.name.startswith(f"{name}_"): + try: + if resolver := _custom_resolvers.get(trial_class): + trial_gate = resolver(gate) + elif issubclass(trial_class, qiskit.circuit.ControlledGate): + trial_gate = trial_class( + *gate.params, ctrl_state=gate.ctrl_state, label=gate.label + ) + else: + trial_gate = trial_class(*gate.params, label=gate.label) + + except Exception: + continue + + if trial_gate.definition == gate.definition: + return trial_gate - return circuits + return gate diff --git a/qiskit-superstaq/qiskit_superstaq/serialization_test.py b/qiskit-superstaq/qiskit_superstaq/serialization_test.py index 6e16df453..80fbdcc73 100644 --- a/qiskit-superstaq/qiskit_superstaq/serialization_test.py +++ b/qiskit-superstaq/qiskit_superstaq/serialization_test.py @@ -1,4 +1,6 @@ # pylint: disable=missing-function-docstring,missing-class-docstring +from __future__ import annotations + import io import json import warnings @@ -49,32 +51,6 @@ def test_to_json() -> None: qss.serialization.to_json(qiskit.QuantumCircuit()) -def test_assign_unique_inst_names() -> None: - inst_0 = qss.ZZSwapGate(0.1) - inst_1 = qss.ZZSwapGate(0.2) - inst_2 = qss.ZZSwapGate(0.1) - - circuit = qiskit.QuantumCircuit(4) - circuit.append(inst_0, [0, 1]) - circuit.append(inst_1, [1, 2]) - circuit.append(inst_2, [2, 0]) - circuit.append(inst_1, [0, 1]) - circuit.rxx(1.1, 0, 1) - circuit.rxx(1.2, 1, 2) - - expected_inst_names = [ - "zzswap", - "zzswap_1", - "zzswap", - "zzswap_1", - "rxx", - "rxx", - ] - - new_circuit = qss.serialization._assign_unique_inst_names(circuit) - assert [inst.name for inst, _, _ in new_circuit] == expected_inst_names - - def test_circuit_serialization() -> None: circuit_0 = qiskit.QuantumCircuit(3) circuit_0.cx(2, 1) @@ -139,7 +115,7 @@ def test_warning_suppression() -> None: # QPY encodes qiskit.__version__ into the serialized circuit, so mocking a newer version string # during serialization will cause a QPY version UserWarning during deserialization - with mock.patch("qiskit.qpy.interface.__version__", newer_version): + with mock.patch("qiskit_ibm_provider.qpy.interface.__version__", newer_version): serialized_circuit = qss.serialization.serialize_circuits(circuit) # Check that a warning would normally be thrown @@ -158,7 +134,9 @@ def test_deserialization_errors() -> None: circuit.x(0) # Mock a circuit serialized with a newer version of QPY: - with mock.patch("qiskit.qpy.common.QPY_VERSION", qiskit.qpy.common.QPY_VERSION + 1): + with mock.patch( + "qiskit_ibm_provider.qpy.common.QPY_VERSION", qiskit.qpy.common.QPY_VERSION + 1 + ): serialized_circuit = qss.serialize_circuits(circuit) # Remove a few bytes to force a deserialization error @@ -166,11 +144,11 @@ def test_deserialization_errors() -> None: gss.serialization.str_to_bytes(serialized_circuit)[:-4] ) - with pytest.raises(ValueError, match="your version of qiskit-terra"): + with pytest.raises(ValueError, match="your version of Qiskit"): _ = qss.deserialize_circuits(serialized_circuit) # Mock a circuit serialized with an older of QPY: - with mock.patch("qiskit.qpy.common.QPY_VERSION", 3): + with mock.patch("qiskit_ibm_provider.qpy.common.QPY_VERSION", 3): serialized_circuit = qss.serialize_circuits(circuit) with pytest.raises(ValueError, match="Please contact"): @@ -192,3 +170,185 @@ def test_circuit_from_qasm_with_gate_defs() -> None: before = qiskit.transpile(circuit_from_qasm, basis_gates=["cx", "u"]) after = qiskit.transpile(new_circuit, basis_gates=["cx", "u"]) assert before == after + + +# Gate classes and corresponding numbers of parameters +test_gates = { + qiskit.circuit.library.Measure: 0, + qiskit.circuit.library.Reset: 0, + qiskit.circuit.library.DCXGate: 0, + qiskit.circuit.library.ECRGate: 0, + qiskit.circuit.library.HGate: 0, + qiskit.circuit.library.GlobalPhaseGate: 1, + qiskit.circuit.library.PhaseGate: 1, + qiskit.circuit.library.RC3XGate: 0, + qiskit.circuit.library.RCCXGate: 0, + qiskit.circuit.library.RGate: 2, + qiskit.circuit.library.RVGate: 3, + qiskit.circuit.library.RXGate: 1, + qiskit.circuit.library.RXXGate: 1, + qiskit.circuit.library.RYGate: 1, + qiskit.circuit.library.RYYGate: 1, + qiskit.circuit.library.RZGate: 1, + qiskit.circuit.library.RZXGate: 1, + qiskit.circuit.library.RZZGate: 1, + qiskit.circuit.library.SGate: 0, + qiskit.circuit.library.SXGate: 0, + qiskit.circuit.library.SXdgGate: 0, + qiskit.circuit.library.SdgGate: 0, + qiskit.circuit.library.SwapGate: 0, + qiskit.circuit.library.TGate: 0, + qiskit.circuit.library.TdgGate: 0, + qiskit.circuit.library.U1Gate: 1, + qiskit.circuit.library.U2Gate: 2, + qiskit.circuit.library.U3Gate: 3, + qiskit.circuit.library.UGate: 3, + qiskit.circuit.library.XGate: 0, + qiskit.circuit.library.XXMinusYYGate: 2, + qiskit.circuit.library.XXPlusYYGate: 2, + qiskit.circuit.library.YGate: 0, + qiskit.circuit.library.ZGate: 0, + qiskit.circuit.library.iSwapGate: 0, + qiskit.circuit.library.C3SXGate: 0, + qiskit.circuit.library.C3XGate: 0, + qiskit.circuit.library.C4XGate: 0, + qiskit.circuit.library.CCXGate: 0, + qiskit.circuit.library.CCZGate: 0, + qiskit.circuit.library.CHGate: 0, + qiskit.circuit.library.CXGate: 0, + qiskit.circuit.library.CYGate: 0, + qiskit.circuit.library.CZGate: 0, + qiskit.circuit.library.CSGate: 0, + qiskit.circuit.library.CSXGate: 0, + qiskit.circuit.library.CSdgGate: 0, + qiskit.circuit.library.CSwapGate: 0, + qiskit.circuit.library.CPhaseGate: 1, + qiskit.circuit.library.CRXGate: 1, + qiskit.circuit.library.CRYGate: 1, + qiskit.circuit.library.CRZGate: 1, + qiskit.circuit.library.CU1Gate: 1, + qiskit.circuit.library.CU3Gate: 3, + qiskit.circuit.library.CUGate: 4, + qss.AceCR: 2, + qss.AQTiCCXGate: 0, + qss.StrippedCZGate: 1, + qss.ZZSwapGate: 1, + qss.custom_gates.iXGate: 0, + qss.custom_gates.iXdgGate: 0, + qss.custom_gates.iCCXGate: 0, + qss.custom_gates.iCCXdgGate: 0, +} + + +def test_completeness() -> None: + """Makes sure `qss.serialization._custom_gates_by_name` and tests cover all custom gates.""" + for attr_name in dir(qss.custom_gates): + attr = getattr(qss.custom_gates, attr_name) + if isinstance(attr, type) and issubclass(attr, qiskit.circuit.Instruction): + assert issubclass( + attr, tuple(qss.serialization._custom_gates_by_name.values()) + ), f"'{attr_name}' not covered in `qss.serialization._custom_gates_by_name`." + + if attr is not qss.ParallelGates: + assert attr in test_gates, f"'{attr_name}' missing from `test_gates`." + + +@pytest.mark.parametrize("base_class", test_gates, ids=lambda g: g.name) +def test_gate_preparation_and_resolution(base_class: type[qiskit.circuit.Instruction]) -> None: + num_params = test_gates[base_class] + + gate = base_class(*np.random.uniform(-2 * np.pi, 2 * np.pi, num_params)) + assert qss.serialization._resolve_gate(qss.serialization._prepare_gate(gate)) == gate + assert qss.serialization._resolve_gate(qss.serialization._wrap_gate(gate)) == gate + + +def _check_serialization(*gates: qiskit.circuit.Instruction) -> None: + num_qubits = sum(g.num_qubits for g in gates) + 1 + circuit = qiskit.QuantumCircuit(num_qubits, max((g.num_clbits for g in gates), default=0)) + + for gate in gates: + circuit.append(gate, range(gate.num_qubits), range(gate.num_clbits)) + + if isinstance(gate, qiskit.circuit.Gate) and not isinstance( + gate, qiskit.circuit.ControlledGate + ): + circuit.append(gate.control(1, ctrl_state=0), range(gate.num_qubits + 1)) + + # Make sure resolution recurses into component gates + if all(isinstance(gate, qiskit.circuit.Gate) for gate in gates): + parallel_gates = qss.ParallelGates(*gates) + circuit.append(qss.ParallelGates(*gates), range(parallel_gates.num_qubits)) + + # Make sure resolution recurses into control-flow operations + circuit.for_loop([0, 1], body=circuit.copy(), qubits=circuit.qubits, clbits=circuit.clbits) + + # Make sure resolution recurses into sub-operations + subcircuit = circuit.copy() + subcircuit.append(subcircuit.to_instruction(), subcircuit.qubits, subcircuit.clbits) + circuit.append(subcircuit, circuit.qubits, circuit.clbits) + + assert circuit == qss.deserialize_circuits(qss.serialize_circuits(circuit))[0] + + +@pytest.mark.parametrize("base_class", test_gates, ids=lambda g: g.name) +def test_gate_serialization(base_class: type[qiskit.circuit.Instruction]) -> None: + num_params = test_gates[base_class] + params = np.random.uniform(-2 * np.pi, 2 * np.pi, (2, num_params)) + + # Construct two different gates to test https://github.com/Qiskit/qiskit/issues/8941 workaround + gate1 = base_class(*params[0]) + if issubclass(base_class, qiskit.circuit.ControlledGate) and base_class != qss.AQTiCCXGate: + gate2 = base_class(*params[1], ctrl_state=gate1.ctrl_state // 2) + else: + gate2 = base_class(*params[1]) + + _check_serialization(gate1, gate2) + + +def test_qiskit_gate_workarounds() -> None: + """Tests workarounds for qiskit gates which are not handled correctly by QPY.""" + _check_serialization( + qiskit.circuit.library.MSGate(2, 1.1), + qiskit.circuit.library.MSGate(2, 2.2), + qiskit.circuit.library.MSGate(3, 2.2), + ) + + circuit = qiskit.QuantumCircuit(5) + circuit.append(qiskit.circuit.library.MCXGate(3, ctrl_state=1), range(4)) + circuit.append(qiskit.circuit.library.MCXGrayCode(4, ctrl_state=2), range(5)) + circuit.append(qiskit.circuit.library.MCXRecursive(3, ctrl_state=3), range(4)) + circuit.append(qiskit.circuit.library.MCXVChain(3, ctrl_state=4), range(5)) + circuit.append(qiskit.circuit.library.MCU1Gate(1.1, 3, ctrl_state=5), range(4)) + circuit.append(qiskit.circuit.library.MCU1Gate(2.2, 3, ctrl_state=6), range(4)) + circuit.append(qiskit.circuit.library.MCPhaseGate(1.1, 3), range(4)) + circuit.append(qiskit.circuit.library.MCPhaseGate(2.2, 3), range(4)) + + subcircuit = circuit.copy() + subcircuit.append(circuit, circuit.qubits) + circuit.append(subcircuit, circuit.qubits) + + assert qss.deserialize_circuits(qss.serialize_circuits(circuit))[0] == circuit + + +def test_different_gates_with_same_name() -> None: + gate1 = qss.ZZSwapGate(1.2) + gate2 = qss.AceCR(1.2, sandwich_rx_rads=np.pi / 2).copy(gate1.name) + gate3 = gate1.copy() + gate3.definition = gate2.definition + + circuit = qiskit.QuantumCircuit(2) + circuit.append(gate1, [0, 1]) + circuit.append(gate2, [0, 1]) + circuit.append(gate3, [0, 1]) + + expected_gate2 = qiskit.circuit.Gate(gate2.name, gate2.num_qubits, gate2.params) + expected_gate2.definition = gate2.definition + expected_gate3 = qiskit.circuit.Gate(gate3.name, gate3.num_qubits, gate3.params) + expected_gate3.definition = gate3.definition + + expected_circuit = qiskit.QuantumCircuit(2) + expected_circuit.append(gate1, [0, 1]) + expected_circuit.append(expected_gate2, [0, 1]) + expected_circuit.append(expected_gate3, [0, 1]) + + assert qss.deserialize_circuits(qss.serialize_circuits(circuit))[0] == expected_circuit diff --git a/qiskit-superstaq/requirements.txt b/qiskit-superstaq/requirements.txt index f9814f2cb..86e32403c 100644 --- a/qiskit-superstaq/requirements.txt +++ b/qiskit-superstaq/requirements.txt @@ -1,3 +1,4 @@ general-superstaq~=0.5.3 matplotlib~=3.0 -qiskit>=0.43.3, <0.45.0 +qiskit>=0.44.1 +qiskit-ibm-provider>=0.7.2