diff --git a/cirq-superstaq/cirq_superstaq/compiler_output.py b/cirq-superstaq/cirq_superstaq/compiler_output.py index 5f965a5df..79f1032c0 100644 --- a/cirq-superstaq/cirq_superstaq/compiler_output.py +++ b/cirq-superstaq/cirq_superstaq/compiler_output.py @@ -14,20 +14,15 @@ from __future__ import annotations -import importlib.util -import json -import warnings -from typing import Any +from typing import TYPE_CHECKING, Any import cirq import general_superstaq as gss import cirq_superstaq as css -try: +if TYPE_CHECKING: import qtrl.sequence_utils.readout -except ModuleNotFoundError: - pass def active_qubit_indices(circuit: cirq.AbstractCircuit) -> list[int]: @@ -41,7 +36,7 @@ def active_qubit_indices(circuit: cirq.AbstractCircuit) -> list[int]: circuit: The input quantum circuit. Returns: - A list of active qubit indicies. + A list of active qubit indices. Raises: TypeError: If qubit indices are requested for non-line qubits. @@ -136,214 +131,35 @@ def __init__( jaqal_programs=jaqal_programs, ) - -def read_json(json_dict: dict[str, Any], circuits_is_list: bool) -> CompilerOutput: - """Reads out returned JSON from Superstaq API's IBMQ compilation endpoint. - - Args: - json_dict: A JSON dictionary matching the format returned by /ibmq_compile endpoint - circuits_is_list: A bool flag that controls whether the returned object has a .circuits - attribute (if `True`) or a .circuit attribute (`False`). - - Returns: - A `CompilerOutput` object with the compiled circuit(s). If included in the server response, - the returned object also stores the corresponding pulse gate circuit(s) in its - .pulse_gate_circuit(s) attribute (provided qiskit-superstaq is available locally). - """ - compiled_circuits = css.serialization.deserialize_circuits(json_dict["cirq_circuits"]) - initial_logical_to_physicals: list[dict[cirq.Qid, cirq.Qid]] = list( - map(dict, cirq.read_json(json_text=json_dict["initial_logical_to_physicals"])) - ) - final_logical_to_physicals: list[dict[cirq.Qid, cirq.Qid]] = list( - map(dict, cirq.read_json(json_text=json_dict["final_logical_to_physicals"])) - ) - - pulse_gate_circuits = None - - if "pulse_gate_circuits" in json_dict: - pulse_gate_circuits = css.serialization.deserialize_qiskit_circuits( - json_dict["pulse_gate_circuits"], - circuits_is_list, - pulse_start_times=json_dict.get("pulse_start_times"), + @staticmethod + def _get_deserialized_content( + json_dict: dict[str, Any], + circuits_is_list: bool, + ) -> tuple[ + list[cirq.Circuit], + list[object] | None, + list[dict[cirq.Qid, cirq.Qid]], + list[dict[cirq.Qid, cirq.Qid]], + ]: + compiled_circuits = css.serialization.deserialize_circuits(json_dict["cirq_circuits"]) + initial_logical_to_physicals_list: list[dict[cirq.Qid, cirq.Qid]] = list( + map(dict, cirq.read_json(json_text=json_dict["initial_logical_to_physicals"])) ) - - if circuits_is_list: - return CompilerOutput( - compiled_circuits, - initial_logical_to_physicals, - final_logical_to_physicals, - pulse_gate_circuits=pulse_gate_circuits, + final_logical_to_physicals_list: list[dict[cirq.Qid, cirq.Qid]] = list( + map(dict, cirq.read_json(json_text=json_dict["final_logical_to_physicals"])) ) - return CompilerOutput( - compiled_circuits[0], - initial_logical_to_physicals[0], - final_logical_to_physicals[0], - pulse_gate_circuits=None if pulse_gate_circuits is None else pulse_gate_circuits[0], - ) - -def read_json_aqt( - json_dict: dict[str, Any], circuits_is_list: bool, num_eca_circuits: int | None = None -) -> CompilerOutput: - """Reads out returned JSON from Superstaq API's AQT compilation endpoint. - - Args: - json_dict: A JSON dictionary matching the format returned by aqt_compile endpoint. - circuits_is_list: A bool flag that controls whether the returned object has a .circuits - attribute (if `True`) or a .circuit attribute (`False`). - num_eca_circuits: Number of logically equivalent random circuits to generate for each - input circuit. - - Returns: - A `CompilerOutput` object with the compiled circuit(s). If `qtrl` is available locally, - the returned object also stores the pulse sequence in the .seq attribute. - """ - compiled_circuits: list[cirq.Circuit] | list[list[cirq.Circuit]] - compiled_circuits = css.serialization.deserialize_circuits(json_dict["cirq_circuits"]) - - initial_logical_to_physicals_list: list[dict[cirq.Qid, cirq.Qid]] = list( - map(dict, cirq.read_json(json_text=json_dict["initial_logical_to_physicals"])) - ) - initial_logical_to_physicals: ( - list[dict[cirq.Qid, cirq.Qid]] | list[list[dict[cirq.Qid, cirq.Qid]]] - ) = initial_logical_to_physicals_list - - final_logical_to_physicals_list: list[dict[cirq.Qid, cirq.Qid]] = list( - map(dict, cirq.read_json(json_text=json_dict["final_logical_to_physicals"])) - ) - final_logical_to_physicals: ( - list[dict[cirq.Qid, cirq.Qid]] | list[list[dict[cirq.Qid, cirq.Qid]]] - ) = final_logical_to_physicals_list - - seq = None - - if "state_jp" in json_dict: - if not importlib.util.find_spec("qtrl"): - warnings.warn( - "This output only contains compiled circuits. The `qtrl` package must be installed " - "in order to deserialize compiled pulse sequences.", - stacklevel=2, + pulse_gate_circuits = None + if "pulse_gate_circuits" in json_dict: + pulse_gate_circuits = gss.serialization.deserialize_qiskit_circuits( + json_dict["pulse_gate_circuits"], + circuits_is_list, + pulse_start_times=json_dict.get("pulse_start_times"), ) - else: # pragma: no cover, b/c qtrl is not open source so it is not in cirq-superstaq reqs - - def _sequencer_from_state(state: dict[str, Any]) -> qtrl.sequencer.Sequence: - seq = qtrl.sequencer.Sequence(n_elements=1) - seq.__setstate__(state) - seq.compile() - return seq - - state = gss.serialization.deserialize(json_dict["state_jp"]) - - if "readout_jp" in json_dict: - readout_state = gss.serialization.deserialize(json_dict["readout_jp"]) - readout_seq = _sequencer_from_state(readout_state) - if "readout_qubits" in json_dict: - readout_qubits = json.loads(json_dict["readout_qubits"]) - readout_seq._readout = qtrl.sequence_utils.readout._ReadoutInfo( - readout_seq, readout_qubits, n_readouts=len(compiled_circuits) - ) - - state["_readout"] = readout_seq - - seq = _sequencer_from_state(state) - - if num_eca_circuits is not None: - compiled_circuits = [ - compiled_circuits[i : i + num_eca_circuits] - for i in range(0, len(compiled_circuits), num_eca_circuits) - ] - initial_logical_to_physicals = [ - initial_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(initial_logical_to_physicals_list), num_eca_circuits) - ] - final_logical_to_physicals = [ - final_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(final_logical_to_physicals_list), num_eca_circuits) - ] - - if circuits_is_list: - return CompilerOutput( + return ( compiled_circuits, - initial_logical_to_physicals, - final_logical_to_physicals, - seq=seq, + pulse_gate_circuits, + initial_logical_to_physicals_list, + final_logical_to_physicals_list, ) - - return CompilerOutput( - compiled_circuits[0], - initial_logical_to_physicals[0], - final_logical_to_physicals[0], - seq=seq, - ) - - -def read_json_qscout( - json_dict: dict[str, Any], circuits_is_list: bool, num_eca_circuits: int | None = None -) -> CompilerOutput: - """Reads out returned JSON from Superstaq API's QSCOUT compilation endpoint. - - Args: - json_dict: A JSON dictionary matching the format returned by qscout_compile endpoint. - circuits_is_list: A bool flag that controls whether the returned object has a .circuits - attribute (if `True`) or a .circuit attribute (`False`). - num_eca_circuits: Optional number of logically equivalent random circuits to generate for - each input circuit. - - Returns: - A `CompilerOutput` object with the compiled circuit(s) and a list of jaqal programs - represented as strings. - """ - compiled_circuits: list[cirq.Circuit] | list[list[cirq.Circuit]] - compiled_circuits = css.serialization.deserialize_circuits(json_dict["cirq_circuits"]) - - initial_logical_to_physicals_list: list[dict[cirq.Qid, cirq.Qid]] = list( - map(dict, cirq.read_json(json_text=json_dict["initial_logical_to_physicals"])) - ) - initial_logical_to_physicals: ( - list[dict[cirq.Qid, cirq.Qid]] | list[list[dict[cirq.Qid, cirq.Qid]]] - ) = initial_logical_to_physicals_list - - final_logical_to_physicals_list: list[dict[cirq.Qid, cirq.Qid]] = list( - map(dict, cirq.read_json(json_text=json_dict["final_logical_to_physicals"])) - ) - final_logical_to_physicals: ( - list[dict[cirq.Qid, cirq.Qid]] | list[list[dict[cirq.Qid, cirq.Qid]]] - ) = final_logical_to_physicals_list - - jaqal_programs: list[str] = json_dict["jaqal_programs"] - - if num_eca_circuits: - compiled_circuits = [ - compiled_circuits[i : i + num_eca_circuits] - for i in range(0, len(compiled_circuits), num_eca_circuits) - ] - initial_logical_to_physicals = [ - initial_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(initial_logical_to_physicals_list), num_eca_circuits) - ] - final_logical_to_physicals = [ - final_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(final_logical_to_physicals_list), num_eca_circuits) - ] - jaqal_programs = [ - gss.compiler_output._jaqal_programs_to_subcircuits( - jaqal_programs[i : i + num_eca_circuits] - ) - for i in range(0, len(jaqal_programs), num_eca_circuits) - ] - - if circuits_is_list: - return CompilerOutput( - compiled_circuits, - initial_logical_to_physicals, - final_logical_to_physicals, - jaqal_programs=jaqal_programs, - ) - - return CompilerOutput( - compiled_circuits[0], - initial_logical_to_physicals[0], - final_logical_to_physicals[0], - jaqal_programs=jaqal_programs, - ) diff --git a/cirq-superstaq/cirq_superstaq/compiler_output_test.py b/cirq-superstaq/cirq_superstaq/compiler_output_test.py index 28b133270..10ff78a3e 100644 --- a/cirq-superstaq/cirq_superstaq/compiler_output_test.py +++ b/cirq-superstaq/cirq_superstaq/compiler_output_test.py @@ -117,7 +117,7 @@ def test_read_json() -> None: "final_logical_to_physicals": cirq.to_json([list(final_logical_to_physical.items())]), } - out = css.compiler_output.read_json(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert out.initial_logical_to_physical == initial_logical_to_physical assert out.final_logical_to_physical == final_logical_to_physical @@ -127,7 +127,7 @@ def test_read_json() -> None: assert out.jaqal_program is None assert out.jaqal_programs is None - out = css.compiler_output.read_json(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit] assert out.final_logical_to_physicals == [final_logical_to_physical] assert out.initial_logical_to_physicals == [initial_logical_to_physical] @@ -151,7 +151,7 @@ def test_read_json_ibmq() -> None: "final_logical_to_physicals": cirq.to_json([list(final_logical_to_physical.items())]), } - out = css.compiler_output.read_json(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert out.pulse_gate_circuit == qiskit.QuantumCircuit() assert out.initial_logical_to_physical == initial_logical_to_physical @@ -161,7 +161,7 @@ def test_read_json_ibmq() -> None: assert not hasattr(out, "initial_logical_to_physicals") assert not hasattr(out, "final_logical_to_physicals") - out = css.compiler_output.read_json(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit] assert out.pulse_gate_circuits == [qiskit.QuantumCircuit()] assert out.final_logical_to_physicals == [final_logical_to_physical] @@ -191,7 +191,7 @@ def test_read_json_pulse_gate_circuits() -> None: "pulse_start_times": [[0, 10]], } - out = css.compiler_output.read_json(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit pulse_output = out.pulse_gate_circuit @@ -206,7 +206,7 @@ def test_read_json_pulse_gate_circuits() -> None: "pulse_gate_circuits": qss.serialization.serialize_circuits([qc_pulse, qc_pulse]), "pulse_start_times": [[0, 10], [0, 100]], } - out = css.compiler_output.read_json(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit, circuit] pulse_output = out.pulse_gate_circuits @@ -218,7 +218,7 @@ def test_read_json_pulse_gate_circuits() -> None: mock.patch.dict("sys.modules", {"qiskit_superstaq": None}), pytest.warns(UserWarning, match=r"qiskit-superstaq is required"), ): - out = css.compiler_output.read_json(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert out.pulse_gate_circuit is None @@ -227,7 +227,7 @@ def test_read_json_pulse_gate_circuits() -> None: UserWarning, match=r"Your compiled pulse gate circuits could not be deserialized.", ): - out = css.compiler_output.read_json(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit, circuit] assert out.pulse_gate_circuits is None @@ -250,7 +250,7 @@ def test_read_json_aqt() -> None: } with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert out.initial_logical_to_physical == initial_logical_to_physical @@ -260,7 +260,7 @@ def test_read_json_aqt() -> None: assert not hasattr(out, "final_logical_to_physicals") with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit] assert out.final_logical_to_physicals == [final_logical_to_physical] @@ -270,7 +270,7 @@ def test_read_json_aqt() -> None: assert not hasattr(out, "final_logical_to_physical") with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert out.seq is None @@ -286,7 +286,7 @@ def test_read_json_aqt() -> None: } with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit, circuit] assert out.initial_logical_to_physicals == [ @@ -300,7 +300,7 @@ def test_read_json_aqt() -> None: # no sequence returned json_dict.pop("state_jp") - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.seq is None @@ -319,7 +319,7 @@ def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires q "final_logical_to_physicals": cirq.to_json([list(final_logical_to_physical.items())]), } - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert isinstance(out.seq, qtrl.sequencer.Sequence) assert pickle.dumps(out.seq) == pickle.dumps(seq) @@ -329,7 +329,7 @@ def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires q # Serialized readout attribute for aqt_zurich_qpu: json_dict["readout_jp"] = state_str json_dict["readout_qubits"] = "[4, 5, 6, 7]" - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert isinstance(out.seq, qtrl.sequencer.Sequence) assert isinstance(out.seq._readout, qtrl.sequencer.Sequence) @@ -341,7 +341,7 @@ def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires q assert not hasattr(out, "circuits") # Multiple circuits: - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit] assert pickle.dumps(out.seq) == pickle.dumps(seq) assert not hasattr(out, "circuit") @@ -356,7 +356,7 @@ def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires q ), "final_logical_to_physicals": cirq.to_json(2 * [list(final_logical_to_physical.items())]), } - out = css.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit, circuit] assert pickle.dumps(out.seq) == pickle.dumps(seq) assert isinstance(out.seq, qtrl.sequencer.Sequence) @@ -407,7 +407,7 @@ def test_read_json_qscout() -> None: "jaqal_programs": [jaqal_program], } - out = css.compiler_output.read_json_qscout(json_dict, circuits_is_list=False) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert out.initial_logical_to_physical == initial_logical_to_physical assert out.final_logical_to_physical == final_logical_to_physical @@ -424,7 +424,7 @@ def test_read_json_qscout() -> None: "final_logical_to_physicals": cirq.to_json(2 * [list(final_logical_to_physical.items())]), "jaqal_programs": [jaqal_program, jaqal_program], } - out = css.compiler_output.read_json_qscout(json_dict, circuits_is_list=True) + out = css.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit, circuit] assert out.final_logical_to_physicals == [final_logical_to_physical, final_logical_to_physical] assert out.initial_logical_to_physicals == [ @@ -436,7 +436,9 @@ def test_read_json_qscout() -> None: assert out.jaqal_programs == [jaqal_program, jaqal_program] assert out.jaqal_program == jaqal_program_as_subcircuits - out = css.compiler_output.read_json_qscout(json_dict, circuits_is_list=True, num_eca_circuits=1) + out = css.compiler_output.CompilerOutput.read_json( + json_dict, circuits_is_list=True, num_eca_circuits=1 + ) assert out.circuits == [[circuit], [circuit]] assert out.initial_logical_to_physicals == [ [initial_logical_to_physical], @@ -448,7 +450,7 @@ def test_read_json_qscout() -> None: ] assert out.jaqal_programs == [jaqal_program, jaqal_program] - out = css.compiler_output.read_json_qscout( + out = css.compiler_output.CompilerOutput.read_json( json_dict, circuits_is_list=False, num_eca_circuits=2 ) assert out.circuits == [circuit, circuit] diff --git a/cirq-superstaq/cirq_superstaq/job.py b/cirq-superstaq/cirq_superstaq/job.py index 64998d536..8e491e9a5 100644 --- a/cirq-superstaq/cirq_superstaq/job.py +++ b/cirq-superstaq/cirq_superstaq/job.py @@ -181,7 +181,7 @@ def status(self, index: int | None = None) -> str: self._refresh_job() return self._overall_status - gss.validation.validate_integer_param(index, min_val=0, parameter_name="index") + gss.validation.validate_integer_param(index, min_val=0) job_ids = self._job_id.split(",") requested_job_id = job_ids[index] @@ -377,7 +377,7 @@ def pulse_gate_circuits(self, index: int | None = None) -> Any: ] deserialized_circuits = [] for serialized_circuit in serialized_circuits: - deserialized_circuit = css.serialization.deserialize_qiskit_circuits( + deserialized_circuit = gss.serialization.deserialize_qiskit_circuits( serialized_circuit, circuits_is_list=False ) if deserialized_circuit is None: @@ -387,7 +387,7 @@ def pulse_gate_circuits(self, index: int | None = None) -> Any: else: gss.validation.validate_integer_param(index, min_val=0, parameter_name="index") serialized_circuit = self._job[job_ids[index]]["pulse_gate_circuits"] - return css.serialization.deserialize_qiskit_circuits( + return gss.serialization.deserialize_qiskit_circuits( serialized_circuit, circuits_is_list=False ) diff --git a/cirq-superstaq/cirq_superstaq/serialization.py b/cirq-superstaq/cirq_superstaq/serialization.py index d8d656bd0..a98d2ffde 100644 --- a/cirq-superstaq/cirq_superstaq/serialization.py +++ b/cirq-superstaq/cirq_superstaq/serialization.py @@ -14,8 +14,6 @@ from __future__ import annotations -import importlib.util -import warnings from collections.abc import Sequence import cirq @@ -62,65 +60,3 @@ def deserialize_circuits(serialized_circuits: str) -> list[cirq.Circuit]: if isinstance(circuits, cirq.Circuit): return [circuits] return circuits - - -def deserialize_qiskit_circuits( - serialized_qiskit_circuits: str, - circuits_is_list: bool, - pulse_start_times: Sequence[Sequence[int]] | None = None, -) -> list[object] | None: - """Deserializes `qiskit.QuantumCircuit` objects, if possible; otherwise warns the user. - - Args: - serialized_qiskit_circuits: Qiskit circuits serialized via `qss.serialize_circuits()`. - circuits_is_list: Whether to refer to "circuits" (plural) or "circuit" (singular) in warning - messages. - pulse_start_times: A list of lists of start times, where each list contains the start times - of every op in the corresponding (serialized) circuit. - - Returns: - A list of deserialized `qiskit.QuantumCircuit` objects, or None if the provided circuits - could not be deserialized. - """ - if importlib.util.find_spec("qiskit_superstaq"): - import qiskit # noqa: PLC0415 - import qiskit_superstaq as qss # noqa: PLC0415 - - try: - pulse_gate_circuits = qss.deserialize_circuits(serialized_qiskit_circuits) - - except Exception as e: - s = "s" if circuits_is_list else "" - warnings.warn( - f"Your compiled pulse gate circuit{s} could not be deserialized. Please " - "make sure your qiskit-superstaq installation is up-to-date (by running " - "`pip install -U qiskit-superstaq`).\n\n" - "If the problem persists, please let us know at superstaq@infleqtion.com, " - "or file a report at https://github.com/Infleqtion/client-superstaq/issues " - "containing the following information (and any other relevant context):\n\n" - f"cirq-superstaq version: {css.__version__}\n" - f"qiskit-superstaq version: {qss.__version__}\n" - f"qiskit version: {qiskit.__version__}\n" - f"error: {e!r}\n\n" - f"You can still access your compiled circuit{s} using the .circuit{s} " - "attribute of this output.", - stacklevel=2, - ) - else: - if pulse_start_times: - for circuit, start_times in zip(pulse_gate_circuits, pulse_start_times): - circuit._op_start_times = start_times - - return pulse_gate_circuits - - else: - s = "s" if circuits_is_list else "" - warnings.warn( - "qiskit-superstaq is required to deserialize compiled pulse gate circuits. You can " - "install it with `pip install qiskit-superstaq`.\n\n" - f"You can still access your compiled circuit{s} using the .circuit{s} attribute of " - "this output.", - stacklevel=2, - ) - - return None diff --git a/cirq-superstaq/cirq_superstaq/service.py b/cirq-superstaq/cirq_superstaq/service.py index 47def9afe..3b702c41d 100644 --- a/cirq-superstaq/cirq_superstaq/service.py +++ b/cirq-superstaq/cirq_superstaq/service.py @@ -32,8 +32,8 @@ import uuid import warnings from collections import defaultdict -from collections.abc import Callable, Iterable, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Generic, Literal, Union, cast, overload +from collections.abc import Iterable, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Generic, Literal, Union, overload import cirq import general_superstaq as gss @@ -48,6 +48,7 @@ if TYPE_CHECKING: import numpy.typing as npt from _typeshed import SupportsItems + from typing_extensions import TypeGuard CssCompileResultT_co = TypeVar( "CssCompileResultT_co", @@ -268,17 +269,30 @@ def __init__( **kwargs, ) + def _is_valid_compile_result( + self, + compile_result: object, + ) -> TypeGuard[CssCompileResultT_co]: + if isinstance(self._client, _SuperstaqClientV3): + return isinstance(compile_result, css.JobV3) + return isinstance(compile_result, css.compiler_output.CompilerOutput) + def _map_compile_request_to_client_result( self, json_dict: dict[str, Any], *, - legacy_parser: Callable[[dict[str, Any]], css.compiler_output.CompilerOutput], + circuits_is_list: bool, + num_eca_circuits: int | None = None, ) -> CssCompileResultT_co: """Maps a compile endpoint's JSON response to the output type expected by the API version. Args: json_dict: The JSON output from a compile endpoint. - legacy_parser: The JSON parsing function to use for the v0.2.0 API. + circuits_is_list: A boolean flag that controls whether the returned object has a + `.circuits` attribute (if `True`) or a `.circuit` attribute (`False`). Note: + relevant only for the v0.2.0 API. + num_eca_circuits: Optional number of logically equivalent random circuits to generate + for each input circuit. Note: relevant only for the v0.2.0 API. Returns: For v0.3.0, compile-like endpoints will return a `css.JobV3`. For v0.2.0, legacy @@ -287,12 +301,20 @@ def _map_compile_request_to_client_result( Raises: TypeError: If `json_dict` is missing a job ID for the v0.3.0 API version. """ + compile_result: css.compiler_output.CompilerOutput | css.JobV3 if isinstance(self._client, gss.superstaq_client._SuperstaqClientV3): job_id = json_dict.get("job_id") if not isinstance(job_id, str): raise TypeError("No valid job id was found in the compile request.") - return cast("CssCompileResultT_co", css.JobV3(client=self._client, job_id=job_id)) - return cast("CssCompileResultT_co", legacy_parser(json_dict)) + compile_result = css.JobV3(client=self._client, job_id=job_id) + else: + compile_result = css.compiler_output.CompilerOutput.read_json( + json_dict=json_dict, + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, + ) + assert self._is_valid_compile_result(compile_result) + return compile_result def _resolve_target(self, target: str | None) -> str: target = target or self.default_target @@ -697,9 +719,8 @@ def aqt_compile( ) return self._map_compile_request_to_client_result( json_dict, - legacy_parser=lambda j_dict: css.compiler_output.read_json_aqt( - j_dict, circuits_is_list, num_eca_circuits - ), + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, ) def qscout_compile( @@ -821,9 +842,8 @@ def qscout_compile( ) return self._map_compile_request_to_client_result( json_dict, - legacy_parser=lambda j_dict: css.compiler_output.read_json_qscout( - j_dict, circuits_is_list, num_eca_circuits - ), + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, ) def cq_compile( @@ -954,7 +974,7 @@ def compile( json_dict = self._client.compile(request_json) return self._map_compile_request_to_client_result( json_dict, - legacy_parser=lambda j_dict: css.compiler_output.read_json(j_dict, circuits_is_list), + circuits_is_list=circuits_is_list, ) def _get_compile_request_json( @@ -1001,7 +1021,7 @@ def submit_dfe( shots: int, **kwargs: Any, ) -> list[str]: - """Executes the circuits neccessary for the DFE protocol. + """Executes the circuits necessary for the DFE protocol. The circuits used to prepare the desired states should not contain final measurements, but can contain mid-circuit measurements (as long as the intended target supports them). For diff --git a/general-superstaq/general_superstaq/compiler_output.py b/general-superstaq/general_superstaq/compiler_output.py index 00a241a20..8f56fa334 100644 --- a/general-superstaq/general_superstaq/compiler_output.py +++ b/general-superstaq/general_superstaq/compiler_output.py @@ -13,9 +13,21 @@ # limitations under the License. from __future__ import annotations +import importlib.util import json +import warnings from collections.abc import Sequence -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +import general_superstaq as gss + +try: + import qtrl.sequence_utils.readout +except ModuleNotFoundError: + pass + +if TYPE_CHECKING: + from typing_extensions import Self C = TypeVar("C") Q = TypeVar("Q") @@ -122,6 +134,218 @@ def jaqal_program(self) -> str | None: return _jaqal_programs_to_subcircuits(self.jaqal_programs) + @classmethod + def read_json_jaqal( + cls, + json_dict: dict[str, Any], + num_eca_circuits: int | None = None, + ) -> Self: + """Reads out the returned JSON from Superstaq API's Jaqal compilation endpoint. + + Args: + json_dict: A JSON dictionary matching the format returned by `/compile` endpoint. + num_eca_circuits: Number of logically equivalent random circuits to generate for each + input circuit. + + Returns: + A `CompilerOutput` object with the compiled Jaqal program(s). + """ + compiled_circuits = json.loads(json_dict["jaqal_strs"]) + + initial_logical_to_physicals_list: list[dict[Q, Q]] = list( + map(dict, json.loads(json_dict["initial_logical_to_physicals"])) + ) + initial_logical_to_physicals: list[dict[Q, Q]] | list[list[dict[Q, Q]]] = ( + initial_logical_to_physicals_list + ) + + final_logical_to_physicals_list: list[dict[Q, Q]] = list( + map(dict, json.loads(json_dict["final_logical_to_physicals"])) + ) + final_logical_to_physicals: list[dict[Q, Q]] | list[list[dict[Q, Q]]] = ( + final_logical_to_physicals_list + ) + + jaqal_programs: list[str] = json_dict.get("jaqal_programs", compiled_circuits) + if num_eca_circuits is not None: + compiled_circuits, initial_logical_to_physicals, final_logical_to_physicals = ( + cls._format_eca_content( + compiled_circuits, + initial_logical_to_physicals_list, + final_logical_to_physicals_list, + num_eca_circuits, + ) + ) + jaqal_programs = [ + _jaqal_programs_to_subcircuits(jaqal_programs[i : i + num_eca_circuits]) + for i in range(0, len(jaqal_programs), num_eca_circuits) + ] + + return cls( + circuits=compiled_circuits, + initial_logical_to_physicals=initial_logical_to_physicals, + final_logical_to_physicals=final_logical_to_physicals, + jaqal_programs=jaqal_programs, + ) + + @staticmethod + def _get_deserialized_content( + json_dict: dict[str, Any], + circuits_is_list: bool, + ) -> tuple[list[C], list[object] | None, list[dict[Q, Q]], list[dict[Q, Q]]]: + compiled_circuits: list[C] = json.loads(json_dict["qasm_strs"]) + initial_logical_to_physicals_list: list[dict[Q, Q]] = list( + map(dict, json.loads(json_dict["initial_logical_to_physicals"])) + ) + final_logical_to_physicals_list: list[dict[Q, Q]] = list( + map(dict, json.loads(json_dict["final_logical_to_physicals"])) + ) + pulse_gate_circuits = None + if "pulse_gate_circuits" in json_dict: # pragma: no cover (requires `qiskit_superstaq`) + pulse_gate_circuits = gss.serialization.deserialize_qiskit_circuits( + json_dict["pulse_gate_circuits"], + circuits_is_list, + pulse_start_times=json_dict.get("pulse_start_times"), + ) + return ( + compiled_circuits, + pulse_gate_circuits, + initial_logical_to_physicals_list, + final_logical_to_physicals_list, + ) + + @staticmethod + def _format_eca_content( + deserialized_circuits: list[C], + initial_logical_to_physicals_list: list[dict[Q, Q]], + final_logical_to_physicals_list: list[dict[Q, Q]], + num_eca_circuits: int, + ) -> tuple[list[list[C]], list[list[dict[Q, Q]]], list[list[dict[Q, Q]]]]: + compiled_circuits = [ + deserialized_circuits[i : i + num_eca_circuits] + for i in range(0, len(deserialized_circuits), num_eca_circuits) + ] + initial_logical_to_physicals = [ + initial_logical_to_physicals_list[i : i + num_eca_circuits] + for i in range(0, len(initial_logical_to_physicals_list), num_eca_circuits) + ] + final_logical_to_physicals = [ + final_logical_to_physicals_list[i : i + num_eca_circuits] + for i in range(0, len(final_logical_to_physicals_list), num_eca_circuits) + ] + return compiled_circuits, initial_logical_to_physicals, final_logical_to_physicals + + @classmethod + def read_json( + cls, + json_dict: dict[str, Any], + *, + circuits_is_list: bool, + num_eca_circuits: int | None = None, + ) -> Self: + """Reads out returned JSON from Superstaq API compilation endpoints. + + Args: + json_dict: The JSON dictionary output from a compile endpoint. + circuits_is_list: A boolean flag that controls whether the returned object has a + `.circuits` attribute (if `True`) or a `.circuit` attribute (`False`). + num_eca_circuits: Optional number of logically equivalent random circuits to generate + for each input circuit. + + Returns: + A `CompilerOutput` object with the compiled circuit(s). If `qtrl` is available locally, + the returned object also stores the pulse sequence in the .seq attribute. Additionally, + if included in the server response, the returned object also stores the corresponding + pulse gate circuit(s) in its .pulse_gate_circuit(s) attribute (provided + `qiskit-superstaq` is available locally). + """ + compiled_circuits: list[C] | list[list[C]] + initial_logical_to_physicals_list: list[dict[Q, Q]] + final_logical_to_physicals_list: list[dict[Q, Q]] + pulse_gate_circuits: list[object] | None + + ( + compiled_circuits, + pulse_gate_circuits, + initial_logical_to_physicals_list, + final_logical_to_physicals_list, + ) = cls._get_deserialized_content(json_dict, circuits_is_list) + + initial_logical_to_physicals: list[dict[Q, Q]] | list[list[dict[Q, Q]]] = ( + initial_logical_to_physicals_list + ) + final_logical_to_physicals: list[dict[Q, Q]] | list[list[dict[Q, Q]]] = ( + final_logical_to_physicals_list + ) + jaqal_programs: list[str] | None = json_dict.get("jaqal_programs") + + seq = None + if "state_jp" in json_dict: + if not importlib.util.find_spec("qtrl"): + warnings.warn( + "This output only contains compiled circuits. The `qtrl` package must be " + "installed in order to deserialize compiled pulse sequences.", + stacklevel=2, + ) + else: # pragma: no cover, b/c qtrl is not open source so it is not in the reqs + + def _sequencer_from_state(state: dict[str, Any]) -> qtrl.sequencer.Sequence: + seq = qtrl.sequencer.Sequence(n_elements=1) + seq.__setstate__(state) + seq.compile() + return seq + + state = gss.serialization.deserialize(json_dict["state_jp"]) + + if "readout_jp" in json_dict: + readout_state = gss.serialization.deserialize(json_dict["readout_jp"]) + readout_seq = _sequencer_from_state(readout_state) + + if "readout_qubits" in json_dict: + readout_qubits = json.loads(json_dict["readout_qubits"]) + readout_seq._readout = qtrl.sequence_utils.readout._ReadoutInfo( + readout_seq, readout_qubits, n_readouts=len(compiled_circuits) + ) + + state["_readout"] = readout_seq + + seq = _sequencer_from_state(state) + + if num_eca_circuits is not None: + compiled_circuits, initial_logical_to_physicals, final_logical_to_physicals = ( + cls._format_eca_content( + compiled_circuits, + initial_logical_to_physicals_list, + final_logical_to_physicals_list, + num_eca_circuits, + ) + ) + + if jaqal_programs is not None: + jaqal_programs = [ + _jaqal_programs_to_subcircuits(jaqal_programs[i : i + num_eca_circuits]) + for i in range(0, len(jaqal_programs), num_eca_circuits) + ] + + if circuits_is_list: + return cls( + compiled_circuits, + initial_logical_to_physicals, + final_logical_to_physicals, + jaqal_programs=jaqal_programs, + pulse_gate_circuits=pulse_gate_circuits, + seq=seq, + ) + + return cls( + compiled_circuits[0], + initial_logical_to_physicals[0], + final_logical_to_physicals[0], + jaqal_programs=jaqal_programs, + pulse_gate_circuits=None if pulse_gate_circuits is None else pulse_gate_circuits[0], + seq=seq, + ) + class CompilerOutput(BaseCompilerOutput[str, int]): """A class that arranges compiled circuit information.""" @@ -170,59 +394,3 @@ def _jaqal_programs_to_subcircuits(jaqal_programs: Sequence[str]) -> str: subcircuits = [jaqal_programs[0]] subcircuits += [jaqal_program.partition(separator)[2] for jaqal_program in jaqal_programs[1:]] return f"\n{separator}".join(subcircuits) - - -def read_json_jaqal( - json_dict: dict[str, Any], num_eca_circuits: int | None = None -) -> CompilerOutput: - """Reads out the returned JSON from Superstaq API's Jaqal compilation endpoint. - - Args: - json_dict: A JSON dictionary matching the format returned by `/compile` endpoint. - num_eca_circuits: Number of logically equivalent random circuits to generate for each - input circuit. - - Returns: - A `CompilerOutput` object with the compiled Jaqal program(s). - """ - compiled_circuits = json.loads(json_dict["jaqal_strs"]) - - initial_logical_to_physicals_list: list[dict[int, int]] = list( - map(dict, json.loads(json_dict["initial_logical_to_physicals"])) - ) - initial_logical_to_physicals: list[dict[int, int]] | list[list[dict[int, int]]] = ( - initial_logical_to_physicals_list - ) - - final_logical_to_physicals_list: list[dict[int, int]] = list( - map(dict, json.loads(json_dict["final_logical_to_physicals"])) - ) - final_logical_to_physicals: list[dict[int, int]] | list[list[dict[int, int]]] = ( - final_logical_to_physicals_list - ) - - jaqal_programs: list[str] = json_dict.get("jaqal_programs", compiled_circuits) - if num_eca_circuits: - compiled_circuits = [ - compiled_circuits[i : i + num_eca_circuits] - for i in range(0, len(compiled_circuits), num_eca_circuits) - ] - initial_logical_to_physicals = [ - initial_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(initial_logical_to_physicals_list), num_eca_circuits) - ] - final_logical_to_physicals = [ - final_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(final_logical_to_physicals_list), num_eca_circuits) - ] - jaqal_programs = [ - _jaqal_programs_to_subcircuits(jaqal_programs[i : i + num_eca_circuits]) - for i in range(0, len(jaqal_programs), num_eca_circuits) - ] - - return CompilerOutput( - circuits=compiled_circuits, - initial_logical_to_physicals=initial_logical_to_physicals, - final_logical_to_physicals=final_logical_to_physicals, - jaqal_programs=jaqal_programs, - ) diff --git a/general-superstaq/general_superstaq/compiler_output_test.py b/general-superstaq/general_superstaq/compiler_output_test.py index e59034ded..3b61bc4af 100644 --- a/general-superstaq/general_superstaq/compiler_output_test.py +++ b/general-superstaq/general_superstaq/compiler_output_test.py @@ -13,7 +13,13 @@ # limitations under the License. from __future__ import annotations +import importlib +import json +import pickle import textwrap +from unittest import mock + +import pytest import general_superstaq as gss @@ -78,3 +84,367 @@ def test_compiler_output_eq() -> None: ) != gss.compiler_output.CompilerOutput( [jaqal_program, jaqal_program_alt], [{0: 0}, {}], [{0: 1}, {}] ) + qasm_program = textwrap.dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg meas[2]; + h q[0]; + cx q[0],q[1]; + barrier q[0],q[1]; + measure q[0] -> meas[0]; + measure q[1] -> meas[1]; + """ + ) + qasm_co = gss.compiler_output.CompilerOutput(qasm_program, {0: 0}, {1: 1}) + assert qasm_co != co + assert not qasm_co.jaqal_programs + assert not qasm_co.jaqal_program + + +def test_read_json_pulse_gate_circuits() -> None: # pragma: no cover, requires `qiskit-superstaq` + qss = pytest.importorskip("qiskit_superstaq", reason="qiskit-superstaq is not installed") + import qiskit # noqa: PLC0415 + + circuit = textwrap.dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg meas[2]; + h q[0]; + cx q[0],q[1]; + barrier q[0],q[1]; + measure q[0] -> meas[0]; + measure q[1] -> meas[1]; + """ + ) + + qc_pulse = qiskit.QuantumCircuit(2) + qc_pulse.h(0) + qc_pulse.cx(0, 1) + + json_dict = { + "qasm_strs": json.dumps([circuit]), + "initial_logical_to_physicals": "[[]]", + "final_logical_to_physicals": "[[]]", + "pulse_gate_circuits": qss.serialization.serialize_circuits(qc_pulse), + "pulse_start_times": [[0, 10]], + } + + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) + assert out.circuit == circuit + + pulse_output = out.pulse_gate_circuit + assert pulse_output == qc_pulse + assert hasattr(pulse_output, "op_start_times") + assert pulse_output.op_start_times == [0, 10] + + json_dict = { + "qasm_strs": json.dumps([circuit, circuit]), + "initial_logical_to_physicals": "[[], []]", + "final_logical_to_physicals": "[[], []]", + "pulse_gate_circuits": qss.serialization.serialize_circuits([qc_pulse, qc_pulse]), + "pulse_start_times": [[0, 10], [0, 100]], + } + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) + assert out.circuits == [circuit, circuit] + + pulse_output = out.pulse_gate_circuits + assert pulse_output == [qc_pulse, qc_pulse] + assert all(hasattr(p_out, "op_start_times") for p_out in pulse_output) + assert pulse_output[1].op_start_times == [0, 100] + + with ( + mock.patch.dict("sys.modules", {"qiskit_superstaq": None}), + pytest.warns(UserWarning, match=r"qiskit-superstaq is required"), + ): + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) + assert out.circuit == circuit + assert out.pulse_gate_circuit is None + + json_dict["pulse_gate_circuits"] = "not-a-serialized-circuit" + with pytest.warns( + UserWarning, + match=r"Your compiled pulse gate circuits could not be deserialized.", + ): + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) + assert out.circuits == [circuit, circuit] + assert out.pulse_gate_circuits is None + + +@mock.patch.dict("sys.modules", {"qtrl": None}) +def test_read_json_aqt() -> None: + importlib.reload(gss.compiler_output) + + circuit = textwrap.dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[4]; + creg meas[4]; + h q[0]; + h q[1]; + h q[2]; + h q[3]; + barrier q[0],q[1],q[2],q[3]; + measure q[0] -> meas[0]; + measure q[1] -> meas[1]; + measure q[2] -> meas[2]; + measure q[3] -> meas[3]; + """ + ) + state_str = gss.serialization.serialize({}) + initial_logical_to_physical = {i: i for i in range(4)} + final_logical_to_physical = {i: 3 - i for i in range(4)} + + json_dict = { + "qasm_strs": json.dumps([circuit]), + "state_jp": state_str, + "initial_logical_to_physicals": json.dumps([list(initial_logical_to_physical.items())]), + "final_logical_to_physicals": json.dumps([list(final_logical_to_physical.items())]), + } + + with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) + + assert out.circuit == circuit + assert out.initial_logical_to_physical == initial_logical_to_physical + assert out.final_logical_to_physical == final_logical_to_physical + assert not hasattr(out, "circuits") + assert not hasattr(out, "initial_logical_to_physicals") + assert not hasattr(out, "final_logical_to_physicals") + + with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) + + assert out.circuits == [circuit] + assert out.final_logical_to_physicals == [final_logical_to_physical] + assert out.initial_logical_to_physicals == [initial_logical_to_physical] + assert not hasattr(out, "circuit") + assert not hasattr(out, "initial_logical_to_physical") + assert not hasattr(out, "final_logical_to_physical") + + with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) + + assert out.circuit == circuit + assert out.seq is None + + # multiple circuits + json_dict = { + "qasm_strs": json.dumps([circuit, circuit]), + "state_jp": state_str, + "initial_logical_to_physicals": json.dumps(2 * [list(initial_logical_to_physical.items())]), + "final_logical_to_physicals": json.dumps(2 * [list(final_logical_to_physical.items())]), + } + + with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) + + assert out.circuits == [circuit, circuit] + assert out.initial_logical_to_physicals == [ + initial_logical_to_physical, + initial_logical_to_physical, + ] + assert out.final_logical_to_physicals == [final_logical_to_physical, final_logical_to_physical] + assert not hasattr(out, "circuit") + assert not hasattr(out, "initial_logical_to_physical") + assert not hasattr(out, "final_logical_to_physical") + + # no sequence returned + json_dict.pop("state_jp") + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) + assert out.seq is None + + +def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires qtrl installation + qtrl = pytest.importorskip("qtrl", reason="qtrl not installed") + seq = qtrl.sequencer.Sequence(n_elements=1) + circuit = textwrap.dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[4]; + creg meas[4]; + h q[0]; + h q[1]; + h q[2]; + h q[3]; + barrier q[0],q[1],q[2],q[3]; + measure q[0] -> meas[0]; + measure q[1] -> meas[1]; + measure q[2] -> meas[2]; + measure q[3] -> meas[3]; + """ + ) + initial_logical_to_physical = {i: i for i in range(4)} + final_logical_to_physical = {i: 3 - i for i in range(4)} + state_str = gss.serialization.serialize(seq.__getstate__()) + json_dict = { + "qasm_strs": json.dumps([circuit]), + "state_jp": state_str, + "initial_logical_to_physicals": json.dumps([list(initial_logical_to_physical.items())]), + "final_logical_to_physicals": json.dumps([list(final_logical_to_physical.items())]), + } + + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) + assert out.circuit == circuit + assert isinstance(out.seq, qtrl.sequencer.Sequence) + assert pickle.dumps(out.seq) == pickle.dumps(seq) + assert not hasattr(out.seq, "_readout") + assert not hasattr(out, "circuits") + + # Serialized readout attribute for `aqt_zurich_qpu`: + json_dict["readout_jp"] = state_str + json_dict["readout_qubits"] = "[4, 5, 6, 7]" + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) + assert out.circuit == circuit + assert isinstance(out.seq, qtrl.sequencer.Sequence) + assert isinstance(out.seq._readout, qtrl.sequencer.Sequence) + assert isinstance(out.seq._readout._readout, qtrl.sequence_utils.readout._ReadoutInfo) + assert out.seq._readout._readout.sequence is out.seq._readout + assert out.seq._readout._readout.qubits == [4, 5, 6, 7] + assert out.seq._readout._readout.n_readouts == 1 + assert pickle.dumps(out.seq._readout) == pickle.dumps(out.seq) == pickle.dumps(seq) + assert not hasattr(out, "circuits") + + # Multiple circuits: + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) + assert out.circuits == [circuit] + assert pickle.dumps(out.seq) == pickle.dumps(seq) + assert not hasattr(out, "circuit") + + json_dict = { + "qasm_strs": json.dumps([circuit, circuit]), + "state_jp": state_str, + "readout_jp": state_str, + "readout_qubits": "[4, 5, 6, 7]", + "initial_logical_to_physicals": json.dumps(2 * [list(initial_logical_to_physical.items())]), + "final_logical_to_physicals": json.dumps(2 * [list(final_logical_to_physical.items())]), + } + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) + assert out.circuits == [circuit, circuit] + assert pickle.dumps(out.seq) == pickle.dumps(seq) + assert isinstance(out.seq, qtrl.sequencer.Sequence) + assert isinstance(out.seq._readout, qtrl.sequencer.Sequence) + assert isinstance(out.seq._readout._readout, qtrl.sequence_utils.readout._ReadoutInfo) + assert out.seq._readout._readout.sequence is out.seq._readout + assert out.seq._readout._readout.qubits == [4, 5, 6, 7] + assert out.seq._readout._readout.n_readouts == 2 + assert not hasattr(out, "circuit") + + +def test_read_json_qscout() -> None: + circuit = textwrap.dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[1]; + creg meas[1]; + h q[0]; + barrier q[0]; + measure q[0] -> meas[0]; + """ + ) + initial_logical_to_physical = {0: 0} + final_logical_to_physical = {0: 13} + + jaqal_program = textwrap.dedent( + """\ + register allqubits[1] + + prepare_all + R allqubits[0] -1.5707963267948966 1.5707963267948966 + Rz allqubits[0] -3.141592653589793 + measure_all + """ + ) + jaqal_program_as_subcircuits = textwrap.dedent( + """\ + register allqubits[1] + + prepare_all + R allqubits[0] -1.5707963267948966 1.5707963267948966 + Rz allqubits[0] -3.141592653589793 + measure_all + + prepare_all + R allqubits[0] -1.5707963267948966 1.5707963267948966 + Rz allqubits[0] -3.141592653589793 + measure_all + """ + ) + + json_dict = { + "qasm_strs": json.dumps([circuit]), + "initial_logical_to_physicals": json.dumps([list(initial_logical_to_physical.items())]), + "final_logical_to_physicals": json.dumps([list(final_logical_to_physical.items())]), + "jaqal_programs": [jaqal_program], + } + + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) + assert out.circuit == circuit + assert out.initial_logical_to_physical == initial_logical_to_physical + assert out.final_logical_to_physical == final_logical_to_physical + assert out.jaqal_program == jaqal_program + assert out.jaqal_programs == [jaqal_program] + assert not hasattr(out, "initial_logical_to_physicals") + assert not hasattr(out, "final_logical_to_physicals") + + json_dict = { + "qasm_strs": json.dumps([circuit, circuit]), + "initial_logical_to_physicals": json.dumps(2 * [list(initial_logical_to_physical.items())]), + "final_logical_to_physicals": json.dumps(2 * [list(final_logical_to_physical.items())]), + "jaqal_programs": [jaqal_program, jaqal_program], + } + out = gss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) + assert out.circuits == [circuit, circuit] + assert out.final_logical_to_physicals == [final_logical_to_physical, final_logical_to_physical] + assert out.initial_logical_to_physicals == [ + initial_logical_to_physical, + initial_logical_to_physical, + ] + assert not hasattr(out, "initial_logical_to_physical") + assert not hasattr(out, "final_logical_to_physical") + assert out.jaqal_programs == [jaqal_program, jaqal_program] + assert out.jaqal_program == jaqal_program_as_subcircuits + + out = gss.compiler_output.CompilerOutput.read_json( + json_dict, circuits_is_list=True, num_eca_circuits=1 + ) + assert out.circuits == [[circuit], [circuit]] + assert out.initial_logical_to_physicals == [ + [initial_logical_to_physical], + [initial_logical_to_physical], + ] + assert out.final_logical_to_physicals == [ + [final_logical_to_physical], + [final_logical_to_physical], + ] + assert out.jaqal_programs == [jaqal_program, jaqal_program] + + out = gss.compiler_output.CompilerOutput.read_json( + json_dict, circuits_is_list=False, num_eca_circuits=2 + ) + assert out.circuits == [circuit, circuit] + assert out.final_logical_to_physicals == [final_logical_to_physical, final_logical_to_physical] + assert out.initial_logical_to_physicals == [ + initial_logical_to_physical, + initial_logical_to_physical, + ] + assert out.jaqal_programs == [jaqal_program_as_subcircuits] + + # Test case for missing jaqal programs + json_dict.pop("jaqal_programs") + out = gss.compiler_output.CompilerOutput.read_json( + json_dict, circuits_is_list=False, num_eca_circuits=2 + ) + assert out.circuits == [circuit, circuit] + assert out.final_logical_to_physicals == [final_logical_to_physical, final_logical_to_physical] + assert out.initial_logical_to_physicals == [ + initial_logical_to_physical, + initial_logical_to_physical, + ] + assert out.jaqal_programs is None diff --git a/general-superstaq/general_superstaq/serialization.py b/general-superstaq/general_superstaq/serialization.py index da9e4740f..ecbe1b4f2 100644 --- a/general-superstaq/general_superstaq/serialization.py +++ b/general-superstaq/general_superstaq/serialization.py @@ -15,9 +15,14 @@ from __future__ import annotations import codecs +import importlib.util import pickle +import warnings +from collections.abc import Sequence from typing import Any +import general_superstaq as gss + def bytes_to_str(bytes_data: bytes) -> str: """Convert arbitrary bytes data into a string. @@ -65,3 +70,65 @@ def deserialize(serialized_obj: str) -> Any: The serialized object. """ return pickle.loads(str_to_bytes(serialized_obj)) # noqa: S301 + + +def deserialize_qiskit_circuits( # pragma: no cover (requires `qiskit_superstaq` install) + serialized_qiskit_circuits: str, + circuits_is_list: bool, + pulse_start_times: Sequence[Sequence[int]] | None = None, +) -> list[object] | None: + """Deserializes `qiskit.QuantumCircuit` objects, if possible; otherwise warns the user. + + Args: + serialized_qiskit_circuits: Qiskit circuits serialized via `qss.serialize_circuits()`. + circuits_is_list: Whether to refer to "circuits" (plural) or "circuit" (singular) in warning + messages. + pulse_start_times: A list of lists of start times, where each list contains the start times + of every op in the corresponding (serialized) circuit. + + Returns: + A list of deserialized `qiskit.QuantumCircuit` objects, or None if the provided circuits + could not be deserialized. + """ + if importlib.util.find_spec("qiskit_superstaq"): + import qiskit # noqa: PLC0415 + import qiskit_superstaq as qss # noqa: PLC0415 + + try: + pulse_gate_circuits = qss.deserialize_circuits(serialized_qiskit_circuits) + + except Exception as e: + s = "s" if circuits_is_list else "" + warnings.warn( + f"Your compiled pulse gate circuit{s} could not be deserialized. Please " + "make sure your qiskit-superstaq installation is up-to-date (by running " + "`pip install -U qiskit-superstaq`).\n\n" + "If the problem persists, please let us know at superstaq@infleqtion.com, " + "or file a report at https://github.com/Infleqtion/client-superstaq/issues " + "containing the following information (and any other relevant context):\n\n" + f"general-superstaq version: {gss.__version__}\n" + f"qiskit-superstaq version: {qss.__version__}\n" + f"qiskit version: {qiskit.__version__}\n" + f"error: {e!r}\n\n" + f"You can still access your compiled circuit{s} using the .circuit{s} " + "attribute of this output.", + stacklevel=2, + ) + else: + if pulse_start_times: + for circuit, start_times in zip(pulse_gate_circuits, pulse_start_times): + circuit._op_start_times = start_times + + return pulse_gate_circuits + + else: + s = "s" if circuits_is_list else "" + warnings.warn( + "qiskit-superstaq is required to deserialize compiled pulse gate circuits. You can " + "install it with `pip install qiskit-superstaq`.\n\n" + f"You can still access your compiled circuit{s} using the .circuit{s} attribute of " + "this output.", + stacklevel=2, + ) + + return None diff --git a/general-superstaq/general_superstaq/service.py b/general-superstaq/general_superstaq/service.py index 0a32e0ddd..ab4eb96e2 100644 --- a/general-superstaq/general_superstaq/service.py +++ b/general-superstaq/general_superstaq/service.py @@ -688,4 +688,6 @@ def qscout_compile( "target": target, } ) - return gss.compiler_output.read_json_jaqal(json_dict, num_eca_circuits=num_eca_circuits) + return gss.compiler_output.CompilerOutput.read_json_jaqal( + json_dict, num_eca_circuits=num_eca_circuits + ) diff --git a/qiskit-superstaq/qiskit_superstaq/compiler_output.py b/qiskit-superstaq/qiskit_superstaq/compiler_output.py index e898a966b..642efd590 100644 --- a/qiskit-superstaq/qiskit_superstaq/compiler_output.py +++ b/qiskit-superstaq/qiskit_superstaq/compiler_output.py @@ -14,21 +14,16 @@ from __future__ import annotations -import importlib.util import json -import warnings -from collections.abc import Mapping -from typing import Any +from typing import TYPE_CHECKING, Any import general_superstaq as gss import qiskit import qiskit_superstaq as qss -try: +if TYPE_CHECKING: import qtrl.sequence_utils.readout -except ModuleNotFoundError: - pass def active_qubit_indices(circuit: qiskit.QuantumCircuit) -> list[int]: @@ -145,227 +140,38 @@ def __init__( jaqal_programs=jaqal_programs, ) - -def read_json( - json_dict: Mapping[str, Any], circuits_is_list: bool, api_version: str = "v0.2.0" -) -> CompilerOutput: - """Reads out returned JSON from Superstaq API's compilation endpoints. - - Args: - json_dict: A JSON dictionary matching the format returned by /compile endpoint. - circuits_is_list: A bool flag that controls whether the returned object has a .circuits - attribute (if `True`) or a .circuit attribute (`False`). - api_version: A string indicating the API version. - - Returns: - A `CompilerOutput` object with the compiled circuit(s) and (if applicable to this target) - corresponding pulse gate circuit(s). - """ - if api_version == "v0.2.0": + @staticmethod + def _get_deserialized_content( + json_dict: dict[str, Any], _circuits_is_list: bool + ) -> tuple[ + list[qiskit.QuantumCircuit], + list[qiskit.QuantumCircuit] | None, + list[dict[int, int]], + list[dict[int, int]], + ]: compiled_circuits = qss.serialization.deserialize_circuits(json_dict["qiskit_circuits"]) - else: - serialized_circuits = json.loads(json_dict["qiskit_circuits"]) - compiled_circuits = [ - qss.serialization.deserialize_circuits(circuit)[0] for circuit in serialized_circuits - ] - - initial_logical_to_physicals: list[dict[int, int]] = list( - map(dict, json.loads(json_dict["initial_logical_to_physicals"])) - ) - final_logical_to_physicals: list[dict[int, int]] = list( - map(dict, json.loads(json_dict["final_logical_to_physicals"])) - ) - - pulse_start_times = json_dict.get("pulse_start_times", []) - for circuit, start_times in zip(compiled_circuits, pulse_start_times): - circuit._op_start_times = start_times - - pulse_gate_circuits = None - - if "pulse_gate_circuits" in json_dict: - pulse_gate_circuits = qss.deserialize_circuits(json_dict["pulse_gate_circuits"]) - - for circuit, start_times in zip(pulse_gate_circuits, pulse_start_times): - circuit._op_start_times = start_times - - if circuits_is_list: - return CompilerOutput( - compiled_circuits, - initial_logical_to_physicals, - final_logical_to_physicals, - pulse_gate_circuits=pulse_gate_circuits, + initial_logical_to_physicals_list: list[dict[int, int]] = list( + map(dict, json.loads(json_dict["initial_logical_to_physicals"])) ) - return CompilerOutput( - compiled_circuits[0], - initial_logical_to_physicals[0], - final_logical_to_physicals[0], - pulse_gate_circuits=None if pulse_gate_circuits is None else pulse_gate_circuits[0], - ) - - -def read_json_aqt( - json_dict: dict[str, str], circuits_is_list: bool, num_eca_circuits: int | None = None -) -> CompilerOutput: - """Reads out the returned JSON from Superstaq API's AQT compilation endpoint. - - Args: - json_dict: A JSON dictionary matching the format returned by /aqt_compile endpoint. - circuits_is_list: Bool flag that controls whether the returned object has a .circuits - attribute (if True) or a .circuit attribute (False). - num_eca_circuits: Optional number of logically equivalent random circuits to generate for - each input circuit. - Returns: - A `CompilerOutput` object with the compiled circuit(s). If `qtrl` is available locally, - the returned object also stores the pulse sequence in the .seq attribute. - """ - compiled_circuits: list[qiskit.QuantumCircuit] | list[list[qiskit.QuantumCircuit]] - compiled_circuits = qss.serialization.deserialize_circuits(json_dict["qiskit_circuits"]) - - initial_logical_to_physicals_list: list[dict[int, int]] = list( - map(dict, json.loads(json_dict["initial_logical_to_physicals"])) - ) - initial_logical_to_physicals: list[dict[int, int]] | list[list[dict[int, int]]] = ( - initial_logical_to_physicals_list - ) - - final_logical_to_physicals_list: list[dict[int, int]] = list( - map(dict, json.loads(json_dict["final_logical_to_physicals"])) - ) - final_logical_to_physicals: list[dict[int, int]] | list[list[dict[int, int]]] = ( - final_logical_to_physicals_list - ) - - seq = None - - if "state_jp" in json_dict: - if not importlib.util.find_spec("qtrl"): - warnings.warn( - "This output only contains compiled circuits. The `qtrl` package must be installed " - "in order to deserialize compiled pulse sequences.", - stacklevel=2, - ) - else: # pragma: no cover, b/c qtrl is not open source so it is not in cirq-superstaq reqs - - def _sequencer_from_state(state: dict[str, Any]) -> qtrl.sequencer.Sequence: - seq = qtrl.sequencer.Sequence(n_elements=1) - seq.__setstate__(state) - seq.compile() - return seq - - state = gss.serialization.deserialize(json_dict["state_jp"]) - - if "readout_jp" in json_dict: - readout_state = gss.serialization.deserialize(json_dict["readout_jp"]) - readout_seq = _sequencer_from_state(readout_state) - - if "readout_qubits" in json_dict: - readout_qubits = json.loads(json_dict["readout_qubits"]) - readout_seq._readout = qtrl.sequence_utils.readout._ReadoutInfo( - readout_seq, readout_qubits, n_readouts=len(compiled_circuits) - ) - - state["_readout"] = readout_seq - - seq = _sequencer_from_state(state) - - if num_eca_circuits is not None: - compiled_circuits = [ - compiled_circuits[i : i + num_eca_circuits] - for i in range(0, len(compiled_circuits), num_eca_circuits) - ] - - initial_logical_to_physicals = [ - initial_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(initial_logical_to_physicals_list), num_eca_circuits) - ] - final_logical_to_physicals = [ - final_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(final_logical_to_physicals_list), num_eca_circuits) - ] - - if circuits_is_list: - return CompilerOutput( - compiled_circuits, - initial_logical_to_physicals, - final_logical_to_physicals, - seq=seq, + final_logical_to_physicals_list: list[dict[int, int]] = list( + map(dict, json.loads(json_dict["final_logical_to_physicals"])) ) - return CompilerOutput( - compiled_circuits[0], - initial_logical_to_physicals[0], - final_logical_to_physicals[0], - seq=seq, - ) - - -def read_json_qscout( - json_dict: dict[str, Any], circuits_is_list: bool, num_eca_circuits: int | None = None -) -> CompilerOutput: - """Reads out the returned JSON from Superstaq API's QSCOUT compilation endpoint. + pulse_start_times = json_dict.get("pulse_start_times", []) + for circuit, start_times in zip(compiled_circuits, pulse_start_times): + circuit._op_start_times = start_times - Args: - json_dict: A JSON dictionary matching the format returned by /qscout_compile endpoint. - circuits_is_list: Bool flag that controls whether the returned object has a .circuits - attribute (if True) or a .circuit attribute (False). - num_eca_circuits: Number of logically equivalent random circuits to generate for each - input circuit. + pulse_gate_circuits = None + if "pulse_gate_circuits" in json_dict: + pulse_gate_circuits = qss.deserialize_circuits(json_dict["pulse_gate_circuits"]) - Returns: - A `CompilerOutput` object with the compiled circuit(s) and a list of - jaqal programs in a string representation. - """ - compiled_circuits: list[qiskit.QuantumCircuit] | list[list[qiskit.QuantumCircuit]] - compiled_circuits = qss.serialization.deserialize_circuits(json_dict["qiskit_circuits"]) - - initial_logical_to_physicals_list: list[dict[int, int]] = list( - map(dict, json.loads(json_dict["initial_logical_to_physicals"])) - ) - initial_logical_to_physicals: list[dict[int, int]] | list[list[dict[int, int]]] = ( - initial_logical_to_physicals_list - ) - - final_logical_to_physicals_list: list[dict[int, int]] = list( - map(dict, json.loads(json_dict["final_logical_to_physicals"])) - ) - final_logical_to_physicals: list[dict[int, int]] | list[list[dict[int, int]]] = ( - final_logical_to_physicals_list - ) - - jaqal_programs: list[str] = json_dict["jaqal_programs"] - - if num_eca_circuits: - compiled_circuits = [ - compiled_circuits[i : i + num_eca_circuits] - for i in range(0, len(compiled_circuits), num_eca_circuits) - ] - initial_logical_to_physicals = [ - initial_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(initial_logical_to_physicals_list), num_eca_circuits) - ] - final_logical_to_physicals = [ - final_logical_to_physicals_list[i : i + num_eca_circuits] - for i in range(0, len(final_logical_to_physicals_list), num_eca_circuits) - ] - jaqal_programs = [ - gss.compiler_output._jaqal_programs_to_subcircuits( - jaqal_programs[i : i + num_eca_circuits] - ) - for i in range(0, len(jaqal_programs), num_eca_circuits) - ] + for circuit, start_times in zip(pulse_gate_circuits, pulse_start_times): + circuit._op_start_times = start_times - if circuits_is_list: - return CompilerOutput( - circuits=compiled_circuits, - initial_logical_to_physicals=initial_logical_to_physicals, - final_logical_to_physicals=final_logical_to_physicals, - jaqal_programs=jaqal_programs, + return ( + compiled_circuits, + pulse_gate_circuits, + initial_logical_to_physicals_list, + final_logical_to_physicals_list, ) - - return CompilerOutput( - compiled_circuits[0], - initial_logical_to_physicals[0], - final_logical_to_physicals[0], - jaqal_programs=jaqal_programs, - ) diff --git a/qiskit-superstaq/qiskit_superstaq/compiler_output_test.py b/qiskit-superstaq/qiskit_superstaq/compiler_output_test.py index d1cb0b92a..62802443f 100644 --- a/qiskit-superstaq/qiskit_superstaq/compiler_output_test.py +++ b/qiskit-superstaq/qiskit_superstaq/compiler_output_test.py @@ -145,7 +145,7 @@ def test_read_json() -> None: "pulse_start_times": [[0, 10, 20]], } - out = qss.compiler_output.read_json(json_dict, circuits_is_list=False) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == qc assert isinstance(out.pulse_gate_circuit, qiskit.QuantumCircuit) assert out.pulse_gate_circuit == qc_pulse @@ -160,7 +160,7 @@ def test_read_json() -> None: "pulse_gate_circuits": qss.serialization.serialize_circuits([qc_pulse, qc_pulse]), "pulse_start_times": [[0, 10, 20], [0, 100, 200]], } - out = qss.compiler_output.read_json(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [qc, qc] assert out.pulse_gate_circuits == [qc_pulse, qc_pulse] assert out.pulse_gate_circuits[1].op_start_times == [0, 100, 200] @@ -168,7 +168,7 @@ def test_read_json() -> None: assert out.jaqal_programs is None json_dict["pulses"] = "oops" - out = qss.compiler_output.read_json(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [qc, qc] @@ -184,7 +184,7 @@ def test_read_json_empty_circuit() -> None: "pulse_start_times": [[]], } - out = qss.compiler_output.read_json(json_dict, circuits_is_list=False) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == qc assert isinstance(out.pulse_gate_circuit, qiskit.QuantumCircuit) assert out.pulse_gate_circuit == qc @@ -197,39 +197,12 @@ def test_read_json_empty_circuit() -> None: "pulse_gate_circuits": qss.serialization.serialize_circuits([qc, qc]), "pulse_start_times": [[], []], } - out = qss.compiler_output.read_json(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [qc, qc] assert out.pulse_gate_circuits == [qc, qc] assert out.pulse_gate_circuits[1].op_start_times == [] -def test_read_jsonV3() -> None: - qc = qiskit.QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - - json_dict = { - "qiskit_circuits": json.dumps([qss.serialization.serialize_circuits(qc)]), - "initial_logical_to_physicals": "[[[0, 0], [1, 1]]]", - "final_logical_to_physicals": "[[[0, 0], [1, 1]]]", - } - out = qss.compiler_output.read_json(json_dict, circuits_is_list=False, api_version="v0.3.0") - assert out.circuit == qc - assert isinstance(out.circuit, qiskit.QuantumCircuit) - assert out.initial_logical_to_physical == {0: 0, 1: 1} - assert out.final_logical_to_physical == {0: 0, 1: 1} - - json_dict = { - "qiskit_circuits": json.dumps([qss.serialization.serialize_circuits(qc)] * 2), - "initial_logical_to_physicals": "[[[0, 0], [1, 1]], [[0, 0], [1, 1]]]", - "final_logical_to_physicals": "[[[0, 0], [1, 1]], [[0, 0], [1, 1]]]", - } - out = qss.compiler_output.read_json(json_dict, circuits_is_list=True, api_version="v0.3.0") - assert out.circuits == [qc, qc] - assert out.initial_logical_to_physicals == [{0: 0, 1: 1}, {0: 0, 1: 1}] - assert out.final_logical_to_physicals == [{0: 0, 1: 1}, {0: 0, 1: 1}] - - @mock.patch.dict("sys.modules", {"qtrl": None}) def test_read_json_aqt() -> None: importlib.reload(qss.compiler_output) @@ -248,13 +221,13 @@ def test_read_json_aqt() -> None: } with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): - out = qss.compiler_output.read_json_aqt(json_dict, circuits_is_list=False) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert not hasattr(out, "circuits") with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): - out = qss.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit] assert not hasattr(out, "circuit") @@ -268,14 +241,14 @@ def test_read_json_aqt() -> None: } with pytest.warns(UserWarning, match=r"deserialize compiled pulse sequences"): - out = qss.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit, circuit] assert not hasattr(out, "circuit") # no sequence returned json_dict.pop("state_jp") - out = qss.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.seq is None @@ -296,7 +269,7 @@ def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires q "state_jp": state_str, } - out = qss.compiler_output.read_json_aqt(json_dict, circuits_is_list=False) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert isinstance(out.seq, qtrl.sequencer.Sequence) assert pickle.dumps(out.seq) == pickle.dumps(seq) @@ -306,7 +279,7 @@ def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires q # Serialized readout attribute for aqt_zurich_qpu: json_dict["readout_jp"] = state_str json_dict["readout_qubits"] = "[4, 5, 6, 7]" - out = qss.compiler_output.read_json_aqt(json_dict, circuits_is_list=False) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert isinstance(out.seq, qtrl.sequencer.Sequence) assert isinstance(out.seq._readout, qtrl.sequencer.Sequence) @@ -318,7 +291,7 @@ def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires q assert not hasattr(out, "circuits") # Multiple circuits: - out = qss.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit] assert pickle.dumps(out.seq) == pickle.dumps(seq) @@ -332,7 +305,7 @@ def test_read_json_with_qtrl() -> None: # pragma: no cover, b/c test requires q "readout_jp": state_str, "readout_qubits": "[4, 5, 6, 7]", } - out = qss.compiler_output.read_json_aqt(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit, circuit] assert pickle.dumps(out.seq) == pickle.dumps(seq) assert isinstance(out.seq, qtrl.sequencer.Sequence) @@ -381,7 +354,7 @@ def test_read_json_qscout() -> None: "jaqal_programs": [jaqal_program], } - out = qss.compiler_output.read_json_qscout(json_dict, circuits_is_list=False) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=False) assert out.circuit == circuit assert out.initial_logical_to_physical == {0: 1} assert out.final_logical_to_physical == {0: 13} @@ -394,20 +367,22 @@ def test_read_json_qscout() -> None: "final_logical_to_physicals": json.dumps([[(0, 13)], [(0, 13)]]), "jaqal_programs": [jaqal_program, jaqal_program], } - out = qss.compiler_output.read_json_qscout(json_dict, circuits_is_list=True) + out = qss.compiler_output.CompilerOutput.read_json(json_dict, circuits_is_list=True) assert out.circuits == [circuit, circuit] assert out.initial_logical_to_physicals == [{0: 1}, {0: 1}] assert out.final_logical_to_physicals == [{0: 13}, {0: 13}] assert out.jaqal_programs == [jaqal_program, jaqal_program] assert out.jaqal_program == jaqal_program_as_subcircuits - out = qss.compiler_output.read_json_qscout(json_dict, circuits_is_list=True, num_eca_circuits=1) + out = qss.compiler_output.CompilerOutput.read_json( + json_dict, circuits_is_list=True, num_eca_circuits=1 + ) assert out.circuits == [[circuit], [circuit]] assert out.initial_logical_to_physicals == [[{0: 1}], [{0: 1}]] assert out.final_logical_to_physicals == [[{0: 13}], [{0: 13}]] assert out.jaqal_programs == [jaqal_program, jaqal_program] - out = qss.compiler_output.read_json_qscout( + out = qss.compiler_output.CompilerOutput.read_json( json_dict, circuits_is_list=False, num_eca_circuits=2 ) assert out.circuits == [circuit, circuit] diff --git a/qiskit-superstaq/qiskit_superstaq/superstaq_backend.py b/qiskit-superstaq/qiskit_superstaq/superstaq_backend.py index f4f2ef59f..2dfcf1839 100644 --- a/qiskit-superstaq/qiskit_superstaq/superstaq_backend.py +++ b/qiskit-superstaq/qiskit_superstaq/superstaq_backend.py @@ -121,8 +121,8 @@ def target(self) -> qiskit.transpiler.Target: gate_durations = [] if duration_info := target_info.get("gate_durations"): - for gate_name, qubit_indicies, duration, unit in duration_info: - gate_durations.append((gate_name, tuple(qubit_indicies), duration, unit)) + for gate_name, qubit_indices, duration, unit in duration_info: + gate_durations.append((gate_name, tuple(qubit_indices), duration, unit)) basis_gateset = ["reset", "measure"] if native_gate_set := target_info.get("native_gate_set"): @@ -288,10 +288,7 @@ def compile( circuits_is_list = not isinstance(circuits, qiskit.QuantumCircuit) json_dict = self._provider._client.compile(request_json) return self._provider._map_compile_request_to_client_result( - json_dict, - legacy_parser=lambda j_dict: qss.compiler_output.read_json( - j_dict, circuits_is_list, api_version=self._provider._client.api_version - ), + json_dict, circuits_is_list=circuits_is_list ) def _get_compile_request_json( @@ -394,9 +391,8 @@ def aqt_compile( ) return self._provider._map_compile_request_to_client_result( json_dict, - legacy_parser=lambda j_dict: qss.compiler_output.read_json_aqt( - j_dict, circuits_is_list, num_eca_circuits - ), + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, ) def ibmq_compile( @@ -451,8 +447,7 @@ def ibmq_compile( circuits_is_list = not isinstance(circuits, qiskit.QuantumCircuit) json_dict = self._provider._client.compile(request_json) return self._provider._map_compile_request_to_client_result( - json_dict, - legacy_parser=lambda j_dict: qss.compiler_output.read_json(j_dict, circuits_is_list), + json_dict, circuits_is_list=circuits_is_list ) def qscout_compile( @@ -565,9 +560,8 @@ def qscout_compile( ) return self._provider._map_compile_request_to_client_result( json_dict, - legacy_parser=lambda j_dict: qss.compiler_output.read_json_qscout( - j_dict, circuits_is_list, num_eca_circuits - ), + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, ) def cq_compile( @@ -611,8 +605,7 @@ def cq_compile( circuits_is_list = not isinstance(circuits, qiskit.QuantumCircuit) json_dict = self._provider._client.compile(request_json) return self._provider._map_compile_request_to_client_result( - json_dict, - legacy_parser=lambda j_dict: qss.compiler_output.read_json(j_dict, circuits_is_list), + json_dict, circuits_is_list=circuits_is_list ) def target_info(self) -> dict[str, Any]: diff --git a/qiskit-superstaq/qiskit_superstaq/superstaq_backend_test.py b/qiskit-superstaq/qiskit_superstaq/superstaq_backend_test.py index 9b5a2914c..fbde70314 100644 --- a/qiskit-superstaq/qiskit_superstaq/superstaq_backend_test.py +++ b/qiskit-superstaq/qiskit_superstaq/superstaq_backend_test.py @@ -295,11 +295,11 @@ def test_ibmq_compile(mock_post: MagicMock) -> None: "final_logical_to_physicals": "[[[0, 4], [1, 5]]]", "pulse_gate_circuits": qss.serialization.serialize_circuits(qc), } - assert backend.compile( - qiskit.QuantumCircuit(), dd_strategy="standard", test_options="yes" - ) == qss.compiler_output.CompilerOutput( - qc, initial_logical_to_physical, final_logical_to_physical, pulse_gate_circuits=qc - ) + out = backend.compile(qiskit.QuantumCircuit(), dd_strategy="standard", test_options="yes") + assert out.initial_logical_to_physical == initial_logical_to_physical + assert out.final_logical_to_physical == final_logical_to_physical + assert out.circuit == qc + assert out.pulse_gate_circuit == qc assert json.loads(mock_post.call_args.kwargs["json"]["options"]) == { "dd_strategy": "standard", @@ -307,9 +307,11 @@ def test_ibmq_compile(mock_post: MagicMock) -> None: "test_options": "yes", } - assert backend.compile([qiskit.QuantumCircuit()]) == qss.compiler_output.CompilerOutput( - [qc], [initial_logical_to_physical], [final_logical_to_physical], pulse_gate_circuits=[qc] - ) + out = backend.compile([qiskit.QuantumCircuit()]) + assert out.initial_logical_to_physicals == [initial_logical_to_physical] + assert out.final_logical_to_physicals == [final_logical_to_physical] + assert out.circuits == [qc] + assert out.pulse_gate_circuits == [qc] assert json.loads(mock_post.call_args.kwargs["json"]["options"]) == { "dd_strategy": "adaptive", "dynamical_decoupling": True, diff --git a/qiskit-superstaq/qiskit_superstaq/superstaq_provider.py b/qiskit-superstaq/qiskit_superstaq/superstaq_provider.py index c53b748fc..93c77330e 100644 --- a/qiskit-superstaq/qiskit_superstaq/superstaq_provider.py +++ b/qiskit-superstaq/qiskit_superstaq/superstaq_provider.py @@ -28,8 +28,8 @@ import uuid import warnings -from collections.abc import Callable, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Generic, cast, overload +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, Any, Generic, overload import general_superstaq as gss import qiskit @@ -42,6 +42,7 @@ import numpy as np import numpy.typing as npt from _typeshed import SupportsItems + from typing_extensions import TypeGuard class SuperstaqProvider(gss.Service, Generic[QssCompileResultT_co]): @@ -182,17 +183,30 @@ def __str__(self) -> str: def __repr__(self) -> str: return f"" + def _is_valid_compile_result( + self, + compile_result: object, + ) -> TypeGuard[QssCompileResultT_co]: + if isinstance(self._client, _SuperstaqClientV3): + return isinstance(compile_result, qss.SuperstaqJobV3) + return isinstance(compile_result, qss.compiler_output.CompilerOutput) + def _map_compile_request_to_client_result( self, json_dict: dict[str, Any], *, - legacy_parser: Callable[[dict[str, Any]], qss.compiler_output.CompilerOutput], + circuits_is_list: bool, + num_eca_circuits: int | None = None, ) -> QssCompileResultT_co: """Maps a compile endpoint's JSON response to the output type expected by the API version. Args: json_dict: The JSON output from a compile endpoint. - legacy_parser: The JSON parsing function to use for the v0.2.0 API. + circuits_is_list: A boolean flag that controls whether the returned object has a + `.circuits` attribute (if `True`) or a `.circuit` attribute (`False`). Note: + relevant only for the v0.2.0 API. + num_eca_circuits: Optional number of logically equivalent random circuits to generate + for each input circuit. Note: relevant only for the v0.2.0 API. Returns: For v0.3.0, compile-like endpoints will return a `qss.SuperstaqJobV3`. For v0.2.0, @@ -201,14 +215,20 @@ def _map_compile_request_to_client_result( Raises: TypeError: If `json_dict` is missing a job ID for the v0.3.0 API version. """ + compile_result: qss.compiler_output.CompilerOutput | qss.SuperstaqJobV3 if isinstance(self._client, gss.superstaq_client._SuperstaqClientV3): job_id = json_dict.get("job_id") if not isinstance(job_id, str): raise TypeError("No valid job id was found in the compile request.") - return cast( - "QssCompileResultT_co", qss.SuperstaqJobV3(client=self._client, job_id=job_id) + compile_result = qss.SuperstaqJobV3(client=self._client, job_id=job_id) + else: + compile_result = qss.compiler_output.CompilerOutput.read_json( + json_dict=json_dict, + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, ) - return cast("QssCompileResultT_co", legacy_parser(json_dict)) + assert self._is_valid_compile_result(compile_result) + return compile_result def get_backend(self, target: str) -> qss.SuperstaqBackend[QssCompileResultT_co]: """Returns a Superstaq backend. diff --git a/qiskit-superstaq/qiskit_superstaq/superstaq_provider_test.py b/qiskit-superstaq/qiskit_superstaq/superstaq_provider_test.py index 50c8c966f..043211e68 100644 --- a/qiskit-superstaq/qiskit_superstaq/superstaq_provider_test.py +++ b/qiskit-superstaq/qiskit_superstaq/superstaq_provider_test.py @@ -50,7 +50,7 @@ def test_provider(fake_superstaq_provider: MockSuperstaqProvider) -> None: def test_provider_args() -> None: with pytest.raises(ValueError, match=r"must be either 'ibm_cloud' or 'ibm_quantum_platform'"): - ss_provider = qss.SuperstaqProvider(api_key="MY_TOKEN", ibmq_channel="foo") + _ = qss.SuperstaqProvider(api_key="MY_TOKEN", ibmq_channel="foo") ss_provider = qss.SuperstaqProvider( api_key="MY_TOKEN", @@ -246,19 +246,19 @@ def test_ibmq_compile(mock_post: MagicMock, fake_superstaq_provider: MockSuperst "pulse_gate_circuits": qss.serialization.serialize_circuits(qc), } - assert fake_superstaq_provider.ibmq_compile( + out = fake_superstaq_provider.ibmq_compile( qiskit.QuantumCircuit(), test_options="yes", target="ibmq_fake_qpu" - ) == qss.compiler_output.CompilerOutput( - qc, initial_logical_to_physical, final_logical_to_physical, pulse_gate_circuits=qc - ) - assert fake_superstaq_provider.ibmq_compile( - [qiskit.QuantumCircuit()], target="ibmq_fake_qpu" - ) == qss.compiler_output.CompilerOutput( - [qc], - [initial_logical_to_physical], - [final_logical_to_physical], - pulse_gate_circuits=[qc], ) + assert out.circuit == qc + assert out.initial_logical_to_physical == initial_logical_to_physical + assert out.final_logical_to_physical == final_logical_to_physical + assert out.pulse_gate_circuit == qc + + out = fake_superstaq_provider.ibmq_compile([qiskit.QuantumCircuit()], target="ibmq_fake_qpu") + assert out.circuits == [qc] + assert out.initial_logical_to_physicals == [initial_logical_to_physical] + assert out.final_logical_to_physicals == [final_logical_to_physical] + assert out.pulse_gate_circuits == [qc] mock_post.return_value.json = lambda: { "qiskit_circuits": qss.serialization.serialize_circuits(qc), @@ -266,26 +266,30 @@ def test_ibmq_compile(mock_post: MagicMock, fake_superstaq_provider: MockSuperst "final_logical_to_physicals": json.dumps([list(final_logical_to_physical.items())]), } - assert fake_superstaq_provider.ibmq_compile( + out = fake_superstaq_provider.ibmq_compile( qiskit.QuantumCircuit(), test_options="yes", target="ibmq_fake_qpu" - ) == qss.compiler_output.CompilerOutput( - qc, initial_logical_to_physical, final_logical_to_physical - ) - assert fake_superstaq_provider.ibmq_compile( - [qiskit.QuantumCircuit()], target="ibmq_fake_qpu" - ) == qss.compiler_output.CompilerOutput( - [qc], [initial_logical_to_physical], [final_logical_to_physical] ) + assert out.circuit == qc + assert out.initial_logical_to_physical == initial_logical_to_physical + assert out.final_logical_to_physical == final_logical_to_physical + + out = fake_superstaq_provider.ibmq_compile([qiskit.QuantumCircuit()], target="ibmq_fake_qpu") + assert out.circuits == [qc] + assert out.initial_logical_to_physicals == [initial_logical_to_physical] + assert out.final_logical_to_physicals == [final_logical_to_physical] + assert json.loads(mock_post.call_args.kwargs["json"]["options"]) == { "dd_strategy": "adaptive", "dynamical_decoupling": True, } - assert fake_superstaq_provider.ibmq_compile( + out = fake_superstaq_provider.ibmq_compile( qiskit.QuantumCircuit(), dd_strategy="standard", test_options="yes", target="ibmq_fake_qpu" - ) == qss.compiler_output.CompilerOutput( - qc, initial_logical_to_physical, final_logical_to_physical ) + assert out.circuit == qc + assert out.initial_logical_to_physical == initial_logical_to_physical + assert out.final_logical_to_physical == final_logical_to_physical + assert json.loads(mock_post.call_args.kwargs["json"]["options"]) == { "dd_strategy": "standard", "dynamical_decoupling": True,