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