diff --git a/cirq-superstaq/cirq_superstaq/compiler_output.py b/cirq-superstaq/cirq_superstaq/compiler_output.py index 01bf79cbb..5be5c789b 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. @@ -135,214 +130,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 1a925f788..f7bb47f60 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 23d44b1ae..b52f3e26d 100644 --- a/cirq-superstaq/cirq_superstaq/service.py +++ b/cirq-superstaq/cirq_superstaq/service.py @@ -32,7 +32,7 @@ import uuid import warnings from collections import defaultdict -from collections.abc import Callable, Iterable, Mapping, Sequence +from collections.abc import Iterable, Mapping, Sequence from typing import TYPE_CHECKING, Any, Generic, Literal, Union, cast, overload import cirq @@ -269,13 +269,18 @@ 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 @@ -289,7 +294,14 @@ def _map_compile_request_to_client_result( 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)) + return cast( + "CssCompileResultT_co", + css.compiler_output.CompilerOutput.read_json( + json_dict=json_dict, + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, + ), + ) def _resolve_target(self, target: str | None) -> str: target = target or self.default_target @@ -658,17 +670,14 @@ def aqt_compile( "target": target, } - options_dict: dict[str, object] - options_dict = {**kwargs} - - if num_eca_circuits is not None: - gss.validation.validate_integer_param(num_eca_circuits) - options_dict["num_eca_circuits"] = int(num_eca_circuits) - if random_seed is not None: - gss.validation.validate_integer_param(random_seed) - options_dict["random_seed"] = int(random_seed) - if atol is not None: - options_dict["atol"] = float(atol) + options_dict: dict[str, object] = gss.validation.get_validated_aqt_options( + num_eca_circuits=num_eca_circuits, + random_seed=random_seed, + atol=atol, + gateset=gateset, + **kwargs, + ) + if gate_defs is not None: gate_defs_cirq = {} for key, val in gate_defs.items(): @@ -676,8 +685,7 @@ def aqt_compile( val = _to_matrix_gate(val) gate_defs_cirq[key] = val options_dict["gate_defs"] = gate_defs_cirq - if gateset is not None: - options_dict["gateset"] = gateset + if pulses or variables: options_dict["aqt_configs"] = { "pulses": self._qtrl_config_to_yaml_str(pulses), @@ -692,9 +700,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( @@ -816,9 +823,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( @@ -949,7 +955,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( @@ -996,7 +1002,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/cirq-superstaq/cirq_superstaq/service_test.py b/cirq-superstaq/cirq_superstaq/service_test.py index 51812716d..6cf29c4f4 100644 --- a/cirq-superstaq/cirq_superstaq/service_test.py +++ b/cirq-superstaq/cirq_superstaq/service_test.py @@ -550,6 +550,7 @@ def test_service_aqt_compile_single(mock_post_request: mock.MagicMock) -> None: expected_options = { "aqt_configs": {}, "atol": 1e-3, + "gateset": {"CZ3": [[5, 6]], "X90": [[5], [6]], "EFX90": [[5], [6]]}, "gate_defs": { "CZ3": css.CZ3, "CZ3/T5C4": None, @@ -557,7 +558,6 @@ def test_service_aqt_compile_single(mock_post_request: mock.MagicMock) -> None: "CS2": cirq.MatrixGate(cirq.unitary(cirq.CZ**0.49)), "CS3": cirq.MatrixGate(cirq.unitary(css.CZ3**0.5), qid_shape=(3, 3)), }, - "gateset": {"CZ3": [[5, 6]], "X90": [[5], [6]], "EFX90": [[5], [6]]}, } mock_post_request.assert_called_with( "/aqt_compile", diff --git a/general-superstaq/general_superstaq/__init__.py b/general-superstaq/general_superstaq/__init__.py index 66b512b04..7d2b96b14 100644 --- a/general-superstaq/general_superstaq/__init__.py +++ b/general-superstaq/general_superstaq/__init__.py @@ -18,7 +18,7 @@ from general_superstaq.compiler_output import BaseCompilerOutput from general_superstaq.job import Job from general_superstaq.resource_estimate import ResourceEstimate -from general_superstaq.service import JaqalService, Service +from general_superstaq.service import JaqalService, QasmService, Service from general_superstaq.superstaq_exceptions import ( SuperstaqException, SuperstaqServerException, @@ -45,6 +45,7 @@ "BaseCompilerOutput", "JaqalService", "Job", + "QasmService", "ResourceEstimate", "Service", "SuperstaqException", diff --git a/general-superstaq/general_superstaq/compiler_output.py b/general-superstaq/general_superstaq/compiler_output.py index 312302bfd..0a005fb02 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") @@ -121,6 +133,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's AQT compilation endpoint. + + 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.""" @@ -168,59 +392,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..7adfcbb3e 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,354 @@ 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] 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 62e6236f5..ea64a6c9b 100644 --- a/general-superstaq/general_superstaq/service.py +++ b/general-superstaq/general_superstaq/service.py @@ -24,6 +24,7 @@ from general_superstaq.superstaq_client import _SuperstaqClient, _SuperstaqClientV3 if TYPE_CHECKING: + import numpy as np import numpy.typing as npt from _typeshed import SupportsItems @@ -584,6 +585,327 @@ def submit_atom_picture(self, bitmap: npt.ArrayLike) -> str: return f"Submitted request for atom picture with ID: {request_id}" +class QasmService(Service): + """This class contains services relating to Superstaq and OpenQasm input.""" + + def compile( + self, + qasm_strs: str | Sequence[str], + target: str, + **kwargs: Any, + ) -> gss.compiler_output.CompilerOutput: + """Compiles the given `qasm_strs` to the target device's native gateset. + + Args: + qasm_strs: The OpenQasm string(s) to compile. + target: String of target device. + kwargs: Other desired compile options. + + Returns: + A `CompilerOutput` object whose .circuit(s) attribute contains optimized compiled + circuit(s). + """ + target = gss.validation.validate_target(target) + if target.startswith("aqt_"): + return self.aqt_compile(qasm_strs, **kwargs) + if target.startswith("qscout_"): + return self.qscout_compile(qasm_strs, **kwargs) + + circuits_is_list = not isinstance(qasm_strs, str) + qasm_circuits = qasm_strs if circuits_is_list else [qasm_strs] + options = {**self._client.client_kwargs, **kwargs} + request_json = { + "qasm_strs": json.dumps(qasm_circuits), + "target": target, + "options": json.dumps(options), # TODO: need broader serialization support + } + json_dict = self._client.compile(request_json) + return gss.compiler_output.CompilerOutput.read_json( + json_dict, circuits_is_list=circuits_is_list + ) + + def ibmq_compile( + self, + qasm_strs: str | Sequence[str], + target: str, + *, + dynamical_decoupling: bool = True, + dd_strategy: str = "adaptive", + **kwargs: Any, + ) -> gss.compiler_output.CompilerOutput: + """Compiles and optimizes the given `qasm_strs` to the target IBMQ device. + + Qiskit Terra must be installed to correctly deserialize pulse schedules for pulse-enabled + targets. + + Superstaq currently supports the following dynamical decoupling strategies: + + * "standard": Places a single DD sequence in each idle window. + + * "syncopated": Places DD pulses at fixed time intervals, alternating between pulses on + neighboring qubits in order to mitigate parasitic ZZ coupling errors. + + * "adaptive" (default): Dynamically spaces DD pulses across idle windows with awareness of + neighboring qubits to achieve the parasitic ZZ coupling mitigation of the "syncopated" + strategy with fewer pulses and less discretization error. + + See https://superstaq.readthedocs.io/en/latest/optimizations/ibm/ibmq_dd_strategies_qss.html + for an example of each strategy. + + Args: + qasm_strs: The OpenQasm string(s) to compile. + target: String of target IBMQ device. + dynamical_decoupling: Applies dynamical decoupling optimization to circuit(s). + dd_strategy: Method to use for placing dynamical decoupling operations; should be either + "standard", "syncopated", or "adaptive" (default). See above. + kwargs: Other desired compile options. + + Returns: + A `CompilerOutput` object whose .circuit(s) attribute contains optimized compiled + circuit(s). + + Raises: + ValueError: If `target` is not a valid IBMQ target. + """ + target = gss.validation.validate_target(target) + if not target.startswith("ibmq_"): + raise ValueError(f"{target!r} is not a valid IBMQ target.") + + options = {"dynamical_decoupling": dynamical_decoupling, "dd_strategy": dd_strategy} + kwargs.update(options) + + return self.compile(qasm_strs, target=target, **kwargs) + + def cq_compile( + self, + qasm_strs: str | Sequence[str], + target: str = "cq_sqale_qpu", + *, + grid_shape: tuple[int, int] | None = None, + control_radius: float = 1.0, + stripped_cz_rads: float = 0.0, + **kwargs: Any, + ) -> gss.compiler_output.CompilerOutput: + """Compiles and optimizes the given `qasm_strs` to the target CQ device. + + Args: + qasm_strs: The OpenQasm string(s) to compile. + target: String of target CQ device. + grid_shape: Optional fixed dimensions for the rectangular qubit grid (by default the + actual qubit layout will be pulled from the hardware provider). + control_radius: The radius with which qubits remain connected + (ie 1.0 indicates nearest neighbor connectivity). + stripped_cz_rads: The angle in radians of the stripped cz gate. + kwargs: Other desired `cq_compile()` options. + + Returns: + A `CompilerOutput` object whose .circuit(s) attribute contains optimized compiled + circuit(s). + + Raises: + ValueError: If `target` is not a valid CQ target. + """ + target = gss.validation.validate_target(target) + if not target.startswith("cq_"): + raise ValueError(f"{target!r} is not a valid CQ target.") + + return self.compile( + qasm_strs, + grid_shape=grid_shape, + control_radius=control_radius, + stripped_cz_rads=stripped_cz_rads, + target=target, + **kwargs, + ) + + def aqt_compile( + self, + qasm_strs: str | Sequence[str], + target: str = "aqt_keysight_qpu", + *, + num_eca_circuits: int | None = None, + random_seed: int | None = None, + atol: float | None = None, + gate_defs: Mapping[str, str | npt.NDArray[np.number[Any]] | None] | None = None, + gateset: Mapping[str, Sequence[Sequence[int]]] | None = None, + pulses: object = None, + variables: object = None, + **kwargs: Any, + ) -> gss.compiler_output.CompilerOutput: + """Compiles and optimizes the input OpenQasm str(s) for the Advanced Quantum Testbed (AQT). + + AQT is a superconducting transmon quantum computing testbed at Lawrence Berkeley National + Laboratory. More information can be found at https://aqt.lbl.gov. + + Specifying a nonzero value for `num_eca_circuits` enables compilation with Equivalent + Circuit Averaging (ECA). See https://arxiv.org/abs/2111.04572 for a description of ECA. + + Args: + qasm_strs: The OpenQasm string(s) to compile. + target: String name of the target AQT device. + num_eca_circuits: Optional number of logically equivalent random OpenQasm string to + generate from each input OpenQasm string for Equivalent Circuit Averaging (ECA). + random_seed: Optional seed used for approximate synthesis and ECA. + atol: An optional tolerance to use for approximate gate synthesis. + gate_defs: An optional dictionary mapping names in `qtrl` configs to operations, where + each operation can be either a unitary matrix or None. More specific associations + take precedence, for example `{"SWAP": , "SWAP/C5C4": }` implies + `` for all "SWAP" calibrations except "SWAP/C5C4" (which will instead be + mapped to `` applied to qubits 4 and 5). Setting any calibration to None + will disable that calibration. + gateset: Which gates to use for compilation. Should be a dictionary with entries in the + for `gate_name: [[1, 2], [3, 4]`, where the keys refer to specific gates, and the + values indicate which qubit(s) they act upon. + pulses: Qtrl `PulseManager` or file path for pulse configuration. + variables: Qtrl `VariableManager` or file path for variable configuration. + kwargs: Other desired compile options. + + Returns: + Object whose .circuit(s) attribute contains optimized OpenQasm strings. + + Raises: + ValueError: If `target` is not a valid AQT target. + """ + target = gss.validation.validate_target(target) + if not target.startswith("aqt_"): + raise ValueError("Using `aqt_compile()` requires a valid AQT target.") + + aqt_options = gss.validation.get_validated_aqt_options( + num_eca_circuits=num_eca_circuits, + random_seed=random_seed, + atol=atol, + gateset=gateset, + **kwargs, + ) + if pulses or variables: + aqt_options["aqt_configs"] = { + "pulses": self._qtrl_config_to_yaml_str(pulses), + "variables": self._qtrl_config_to_yaml_str(variables), + } + if gate_defs is not None: + aqt_options["gate_defs"] = gate_defs + + options = {**self._client.client_kwargs, **aqt_options} + circuits_is_list = not isinstance(qasm_strs, str) + qasm_circuits = qasm_strs if circuits_is_list else [qasm_strs] + + json_dict = self._client.aqt_compile( + { + "qasm_strs": json.dumps(qasm_circuits), + "options": json.dumps(options), + "target": target, + } + ) + return gss.compiler_output.CompilerOutput.read_json( + json_dict, + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, + ) + + def qscout_compile( + self, + qasm_strs: str | Sequence[str], + target: str = "qscout_peregrine_qpu", + *, + num_eca_circuits: int | None = None, + mirror_swaps: bool = False, + base_entangling_gate: str = "xx", + num_qubits: int | None = None, + error_rates: SupportsItems[tuple[int, ...], float] | None = None, + atol: float = 1e-8, + atol_map: SupportsItems[tuple[int, ...], float] | None = None, + keep_qubit_order: bool = False, + random_seed: int | None = None, + **kwargs: Any, + ) -> gss.compiler_output.CompilerOutput: + """Compiles and optimizes the given OpenQasm for a QSCOUT `target`. + + Specifying a nonzero value for `num_eca_circuits` enables compilation with Equivalent + Circuit Averaging (ECA). See [1] for a description of ECA. + + References: + [1] A. Hashim, et al., Optimized fermionic SWAP networks with equivalent circuit + averaging for QAOA. Phys. Rev. Research 4, 033028 (2022). + https://arxiv.org/abs/2111.04572 + + Args: + qasm_strs: The OpenQasm string(s) to compile. + target: String of target representing target device. + num_eca_circuits: Optional number of logically equivalent random OpenQasm strings to + generate from each input OpenQasm string for Equivalent Circuit Averaging (ECA). + mirror_swaps: Whether to use mirror swapping to reduce two-qubit gate overhead. + base_entangling_gate: The base entangling gate to use: ("xx", "zz", "sxx", or "szz"). + Compilation with the "xx" and "zz" entangling bases will use arbitrary + parameterized two-qubit interactions, while the "sxx" and "szz" bases will only use + fixed maximally-entangling rotations. Defaults to "xx". + num_qubits: An optional number of qubits that should be initialized in the returned + OpenQasm string(s) (by default this will be determined from the input `qasm_strs`). + error_rates: Optional dictionary assigning relative error rates to pairs of physical + qubits, in the form `{: , ...}` where `` + is a tuple physical qubit indices (ints) and `` is a relative error rate + for gates acting on those qubits (for example `{(0, 1): 0.3, (1, 2): 0.2}`). If + provided, Superstaq will attempt to map the circuit to minimize the total error on + each qubit. Omitted qubit pairs are assumed to be error-free. + atol: Optional tolerance (trace distance bound) used for approximate compilation. + Superstaq will elide gates which can be approximated within the given tolerance by + identity operations. + atol_map: Optional dictionary assigning compilation tolerances to physical qubits, in + the form `{: , ...}` where `` is a tuple of + physical qubit indices (ints) and `` is an absolute tolerance (trace distance + bound) for gates acting on those qubits (for example `{(0, 1): 0.3, (1, 2): 0.2}`). + If provided, these tolerances will override `atol` for gates on the given qubits. + Omitted qubit pairs default to `atol`. + keep_qubit_order: If `True`, do not reorder input qubits when compiling with ECA. + random_seed: Used to seed any stochastic compilation passes (especially for ECA). + kwargs: Other desired `/qscout_compile` options. + + Returns: + Object whose .circuit(s) attribute contains optimized OpenQasm strings. + + Raises: + ValueError: If `base_entangling_gate` is not a valid gate option. + ValueError: If `target` is not a valid QSCOUT target. + ValueError: If provided `num_qubits` is less than the register size required by + `qasm_strs`. + """ + target = gss.validation.validate_target(target) + if not target.startswith("qscout_"): + raise ValueError("Using `qscout_compile()` requires a valid QSCOUT target.") + + circuits_is_list = not isinstance(qasm_strs, str) + qasm_circuits = [qasm_strs] if isinstance(qasm_strs, str) else qasm_strs + inferred_num_qubits = gss.validation.get_validated_assembly_qubits( + qasm_circuits, circuit_type="qasm_strs" + ) + qscout_options = gss.validation.get_validated_qscout_options( + inferred_num_qubits, + num_eca_circuits=num_eca_circuits, + mirror_swaps=mirror_swaps, + base_entangling_gate=base_entangling_gate, + num_qubits=num_qubits, + error_rates=error_rates, + atol=atol, + atol_map=atol_map, + keep_qubit_order=keep_qubit_order, + random_seed=random_seed, + **kwargs, + ) + options = {**self._client.client_kwargs, **qscout_options} + + json_dict = self._client.qscout_compile( + { + "qasm_strs": json.dumps(qasm_circuits), + "options": json.dumps(options), + "target": target, + } + ) + return gss.compiler_output.CompilerOutput.read_json( + json_dict, + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, + ) + + class JaqalService(BaseService): """This class contains services relating to Superstaq and Jaqal input.""" @@ -662,9 +984,11 @@ def qscout_compile( raise ValueError("Using `qscout_compile()` requires a valid QSCOUT target.") jaqal_programs = [jaqal_programs] if isinstance(jaqal_programs, str) else jaqal_programs - inferred_num_qubits = gss.validation.get_validated_jaqal_qubits(jaqal_programs) + inferred_num_qubits = gss.validation.get_validated_assembly_qubits( + jaqal_programs, circuit_type="jaqal_strs" + ) - options = gss.validation.get_validated_qscout_options( + qscout_options = gss.validation.get_validated_qscout_options( inferred_num_qubits, num_eca_circuits=num_eca_circuits, mirror_swaps=mirror_swaps, @@ -677,6 +1001,7 @@ def qscout_compile( random_seed=random_seed, **kwargs, ) + options = {**self._client.client_kwargs, **qscout_options} json_dict = self._client.qscout_compile( { @@ -685,4 +1010,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/general-superstaq/general_superstaq/service_test.py b/general-superstaq/general_superstaq/service_test.py index 309e493c4..402c94504 100644 --- a/general-superstaq/general_superstaq/service_test.py +++ b/general-superstaq/general_superstaq/service_test.py @@ -18,6 +18,7 @@ import secrets import tempfile import textwrap +from collections.abc import Callable, Sequence from unittest import mock import pytest @@ -529,3 +530,189 @@ def test_aces( mock_post.return_value.json = lambda: [1] * 51 assert service.process_aces("id1") == [1] * 51 + + +@pytest.mark.parametrize( + ("compile_call", "target"), + [ + pytest.param( + lambda s, c, t: s.compile(c, target=t), + "ss_unconstrained_simulator", + id="compile", + ), + pytest.param(lambda s, c, t: s.compile(c, target=t), "aqt_keysight_qpu", id="aqt_compile"), + pytest.param( + lambda s, c, t: s.compile(c, target=t), + "qscout_peregrine_qpu", + id="qscout_compile", + ), + pytest.param( + lambda s, c, t: s.cq_compile(c, target=t), "cq_sqale_simulator", id="cq_compile" + ), + pytest.param( + lambda s, c, t: s.ibmq_compile(c, target=t), + "ibmq_fez_qpu", + id="ibmq_compile", + ), + ], +) +def test_qasm_service_compile( + compile_call: Callable[ + [gss.Service, str | Sequence[str], str], gss.compiler_output.CompilerOutput + ], + target: str, +) -> None: + qasm_service = gss.QasmService(api_key="key", remote_host="http://example.com") + + mock_client = mock.MagicMock(spec=gss.superstaq_client._SuperstaqClient) + mock_client.api_version = "v0.2.0" + mock_client.client_kwargs = {} + qasm_service._client = mock_client + + input_circuit = textwrap.dedent( + """\ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[1]; + h q[0]; + t q[0]; + """ + ) + compiled_circuit = input_circuit + json_dict: dict[str, str | list[str]] = { + "qasm_strs": json.dumps([compiled_circuit]), + "initial_logical_to_physicals": json.dumps([[(0, 1)]]), + "final_logical_to_physicals": json.dumps([[(0, 13)]]), + } + + if target.startswith("qscout_"): + mock_client.qscout_compile.return_value = json_dict + elif target.startswith("aqt_"): + mock_client.aqt_compile.return_value = json_dict + else: + mock_client.compile.return_value = json_dict + + qasm_compiler_output = compile_call(qasm_service, input_circuit, target) + assert isinstance(qasm_compiler_output, gss.compiler_output.CompilerOutput) + + assert not qasm_compiler_output.has_multiple_circuits() + assert qasm_compiler_output.circuit == compiled_circuit + assert not qasm_compiler_output.jaqal_program + assert not qasm_compiler_output.jaqal_programs + assert qasm_compiler_output.initial_logical_to_physical == {0: 1} + assert qasm_compiler_output.final_logical_to_physical == {0: 13} + + json_dict = { + "qasm_strs": json.dumps([compiled_circuit, compiled_circuit]), + "initial_logical_to_physicals": json.dumps([[(0, 1)], [(0, 1)]]), + "final_logical_to_physicals": json.dumps([[(0, 13)], [(0, 13)]]), + } + + if target.startswith("qscout_"): + mock_client.qscout_compile.return_value = json_dict + elif target.startswith("aqt_"): + mock_client.aqt_compile.return_value = json_dict + else: + mock_client.compile.return_value = json_dict + + qasm_compiler_output = compile_call(qasm_service, [input_circuit] * 2, target) + assert isinstance(qasm_compiler_output, gss.compiler_output.CompilerOutput) + + assert qasm_compiler_output.has_multiple_circuits() + assert qasm_compiler_output.circuits == [compiled_circuit] * 2 + assert not qasm_compiler_output.jaqal_program + assert not qasm_compiler_output.jaqal_programs + assert qasm_compiler_output.initial_logical_to_physicals == [{0: 1}] * 2 + assert qasm_compiler_output.final_logical_to_physicals == [{0: 13}] * 2 + + +def test_qasm_service_exceptions() -> None: + qasm_service = gss.QasmService(api_key="key") + with pytest.raises(ValueError, match=r"a valid AQT target"): + qasm_service.aqt_compile(qasm_strs="", target="ss_unconstrained_simulator") + with pytest.raises(ValueError, match=r"a valid IBMQ target"): + qasm_service.ibmq_compile(qasm_strs="", target="ss_unconstrained_simulator") + with pytest.raises(ValueError, match=r"a valid CQ target"): + qasm_service.cq_compile(qasm_strs="", target="ss_unconstrained_simulator") + with pytest.raises(ValueError, match=r"a valid QSCOUT target"): + qasm_service.qscout_compile(qasm_strs="", target="ss_unconstrained_simulator") + + +@mock.patch( + "general_superstaq.superstaq_client._SuperstaqClient.post_request", + return_value={ + "qasm_strs": json.dumps(["OPENQASM 2.0;"]), + "initial_logical_to_physicals": json.dumps([[]]), + "final_logical_to_physicals": json.dumps([[]]), + }, +) +def test_qasm_service_aqt_compile_single(mock_post_request: mock.MagicMock) -> None: + service = gss.QasmService(api_key="key", remote_host="http://example.com") + out = service.aqt_compile("OPENQASM 2.0;", test_options="yes") + mock_post_request.assert_called_once_with( + "/aqt_compile", + { + "qasm_strs": '["OPENQASM 2.0;"]', + "options": '{"test_options": "yes"}', + "target": "aqt_keysight_qpu", + }, + ) + + alt_out = service.compile("OPENQASM 2.0;", target="aqt_keysight_qpu", test_options="yes") + + for output in [out, alt_out]: + assert output.circuit == "OPENQASM 2.0;" + assert output.initial_logical_to_physical == {} + assert output.final_logical_to_physical == {} + assert not hasattr(output, "circuits") + assert not hasattr(output, "initial_logical_to_physicals") + assert not hasattr(output, "final_logical_to_physicals") + + with pytest.raises(ValueError, match=r"Unable to serialize configuration"): + _ = service.aqt_compile("OPENQASM 2.0;", atol=1e-2, pulses=123, variables=456) + + out = service.aqt_compile( + "OPENQASM 2.0;", + atol=1e-3, + aqt_configs={}, + gate_defs={}, + gateset={"X90": [[0], [1]]}, + ) + expected_options = ( + '{"aqt_configs": {}, "atol": 0.001, "gateset": {"X90": [[0], [1]]}, "gate_defs": {}}' + ) + mock_post_request.assert_called_with( + "/aqt_compile", + { + "qasm_strs": '["OPENQASM 2.0;"]', + "options": expected_options, + "target": "aqt_keysight_qpu", + }, + ) + assert out.circuit == "OPENQASM 2.0;" + assert not hasattr(out, "circuits") + + +@mock.patch( + "general_superstaq.superstaq_client._SuperstaqClient.post_request", + return_value={ + "qasm_strs": json.dumps(["OPENQASM 2.0;"]), + "initial_logical_to_physicals": json.dumps([[]]), + "final_logical_to_physicals": json.dumps([[]]), + }, +) +def test_service_aqt_compile_eca(mock_post_request: mock.MagicMock) -> None: + service = gss.QasmService(api_key="key", remote_host="http://example.com") + out = service.aqt_compile("OPENQASM 2.0;", num_eca_circuits=1, random_seed=1234, atol=1e-2) + mock_post_request.assert_called_once() + assert out.circuits == ["OPENQASM 2.0;"] + assert out.initial_logical_to_physicals == [{}] + assert out.final_logical_to_physicals == [{}] + assert not hasattr(out, "circuit") + assert not hasattr(out, "initial_logical_to_physical") + assert not hasattr(out, "final_logical_to_physical") + + out = service.aqt_compile(["OPENQASM 2.0;"], num_eca_circuits=1, random_seed=1234, atol=1e-2) + assert out.circuits == [["OPENQASM 2.0;"]] + assert out.initial_logical_to_physicals == [[{}]] + assert out.final_logical_to_physicals == [[{}]] diff --git a/general-superstaq/general_superstaq/validation.py b/general-superstaq/general_superstaq/validation.py index 1d1ebfda9..da4ecd33e 100644 --- a/general-superstaq/general_superstaq/validation.py +++ b/general-superstaq/general_superstaq/validation.py @@ -103,23 +103,34 @@ def validate_target(target: str) -> str: return target -def get_validated_jaqal_qubits(jaqal_programs: Sequence[str]) -> int: - """Gets the maximum number of qubits that should be initialized for all `jaqal_programs`. +def get_validated_assembly_qubits(string_programs: Sequence[str], *, circuit_type: str) -> int: + """Gets the maximum number of qubits that should be initialized for all `string_programs`. Args: - jaqal_programs: The Jaqal programs to infer qubit count from. + string_programs: The Jaqal programs to infer qubit count from. + circuit_type: The kind of circuit that `string_programs` correspond to. Returns: - The max qubit register size needed for all `jaqal_programs`. + The max qubit register size needed for all `string_programs`. Raises: - ValueError: If no qubit count could be inferred from `jaqal_programs`. + ValueError: If no pattern is defined for `circuit_type`. + ValueError: If no qubit count could be inferred from `string_programs`. """ - pattern = re.compile(r"^\s*register\b.*?\[(\d+)\]", re.MULTILINE) - register_sizes = (int(m.group(1)) for jp in jaqal_programs for m in [pattern.search(jp)] if m) + if circuit_type == "jaqal_strs": + pattern = re.compile(r"^\s*register\b.*?\[(\d+)\]", re.MULTILINE) + elif circuit_type == "qasm_strs": + pattern = re.compile(r"^\s*qreg\b.*?\[(\d+)\]", re.MULTILINE) + else: + raise ValueError(f"Unsupported circuit type provided: '{circuit_type}'.") + register_sizes = ( + int(m.group(1)) for circuit_str in string_programs for m in pattern.finditer(circuit_str) + ) inferred_num_qubits = max(register_sizes, default=None) if inferred_num_qubits is None: - raise ValueError("Could not determine number of qubits from Jaqal program register(s).") + raise ValueError( + "Could not determine maximum number of qubits from input circuit register(s)." + ) return inferred_num_qubits @@ -231,6 +242,43 @@ def _validate_ibm_channel(ibm_channel: str) -> str: return ibm_channel +def get_validated_aqt_options( + num_eca_circuits: int | None = None, + random_seed: int | None = None, + atol: float | None = None, + gateset: Mapping[str, Sequence[Sequence[int]]] | None = None, + **kwargs: object, +) -> dict[str, object]: + """Generates an options dictionary packaging the input args into a format compatible for + `/aqt_compile`. + + Args: + num_eca_circuits: Optional number of logically equivalent random circuits to generate + from each input circuit for Equivalent Circuit Averaging (ECA). + random_seed: Optional seed used for approximate synthesis and ECA. + atol: An optional tolerance to use for approximate gate synthesis. + gateset: Which gates to use for compilation. Should be a dictionary with entries in the + for `gate_name: [[1, 2], [3, 4]`, where the keys refer to specific gates, and the + values indicate which qubit(s) they act upon. + kwargs: Other desired compile options. + + Returns: + A validated options dictionary packaging provided `args`. + """ + options: dict[str, object] = {**kwargs} + if num_eca_circuits is not None: + validate_integer_param(num_eca_circuits, parameter_name="num_eca_circuits") + options["num_eca_circuits"] = int(num_eca_circuits) + if random_seed is not None: + validate_integer_param(random_seed, parameter_name="random_seed") + options["random_seed"] = int(random_seed) + if atol is not None: + options["atol"] = float(atol) + if gateset is not None: + options["gateset"] = gateset + return options + + def get_validated_qscout_options( inferred_num_qubits: int, *, diff --git a/general-superstaq/general_superstaq/validation_test.py b/general-superstaq/general_superstaq/validation_test.py index 2c244f174..792a44a41 100644 --- a/general-superstaq/general_superstaq/validation_test.py +++ b/general-superstaq/general_superstaq/validation_test.py @@ -140,7 +140,7 @@ def test_validate_qubo() -> None: gss.validation.validate_qubo({(): 123}) -def test_validate_jaqal_qubits() -> None: +def test_validate_assembly_qubits() -> None: jaqal_program = textwrap.dedent( """\ register baseregister[1] @@ -169,17 +169,24 @@ def test_validate_jaqal_qubits() -> None: """ ) assert ( - gss.validation.get_validated_jaqal_qubits([jaqal_program]) - == gss.validation.get_validated_jaqal_qubits([jaqal_program] * 3) + gss.validation.get_validated_assembly_qubits([jaqal_program], circuit_type="jaqal_strs") + == gss.validation.get_validated_assembly_qubits( + [jaqal_program] * 3, circuit_type="jaqal_strs" + ) == 1 ) assert ( - gss.validation.get_validated_jaqal_qubits([jaqal_program_alt]) - == gss.validation.get_validated_jaqal_qubits( - [jaqal_program, jaqal_program_alt, jaqal_program] + gss.validation.get_validated_assembly_qubits([jaqal_program_alt], circuit_type="jaqal_strs") + == gss.validation.get_validated_assembly_qubits( + [jaqal_program, jaqal_program_alt, jaqal_program], circuit_type="jaqal_strs" ) == 4 ) - with pytest.raises(ValueError, match=r"Could not determine number"): + with pytest.raises(ValueError, match=r"Could not determine"): missing_qubit_jaqal = "\n".join(jaqal_program.split("\n")[1:]) - _ = gss.validation.get_validated_jaqal_qubits([missing_qubit_jaqal]) + _ = gss.validation.get_validated_assembly_qubits( + [missing_qubit_jaqal], circuit_type="jaqal_strs" + ) + + with pytest.raises(ValueError, match=r"Unsupported circuit type"): + _ = gss.validation.get_validated_assembly_qubits([], circuit_type="foo_strs") diff --git a/qiskit-superstaq/qiskit_superstaq/compiler_output.py b/qiskit-superstaq/qiskit_superstaq/compiler_output.py index 28f78af3b..f6f758e08 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]: @@ -144,227 +139,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 40e1af926..07ec0445d 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"): @@ -287,10 +287,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( @@ -364,19 +361,15 @@ def aqt_compile( if not self.name.startswith("aqt_"): raise ValueError(f"{self.name!r} is not a valid AQT target.") - options: dict[str, Any] = {**kwargs} - if num_eca_circuits is not None: - gss.validation.validate_integer_param(num_eca_circuits) - options["num_eca_circuits"] = int(num_eca_circuits) - if random_seed is not None: - gss.validation.validate_integer_param(random_seed) - options["random_seed"] = int(random_seed) - if atol is not None: - options["atol"] = float(atol) + options = gss.validation.get_validated_aqt_options( + num_eca_circuits=num_eca_circuits, + random_seed=random_seed, + atol=atol, + gateset=gateset, + **kwargs, + ) if gate_defs is not None: options["gate_defs"] = gate_defs - if gateset is not None: - options["gateset"] = gateset if pulses or variables: options["aqt_configs"] = { "pulses": self._provider._qtrl_config_to_yaml_str(pulses), @@ -393,9 +386,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( @@ -450,8 +442,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( @@ -564,9 +555,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( @@ -610,8 +600,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 f10f1b2b9..f950d1083 100644 --- a/qiskit-superstaq/qiskit_superstaq/superstaq_backend_test.py +++ b/qiskit-superstaq/qiskit_superstaq/superstaq_backend_test.py @@ -294,11 +294,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", @@ -306,9 +306,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 8970a3f88..66fac3ffe 100644 --- a/qiskit-superstaq/qiskit_superstaq/superstaq_provider.py +++ b/qiskit-superstaq/qiskit_superstaq/superstaq_provider.py @@ -28,7 +28,7 @@ import uuid import warnings -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any, Generic, cast, overload import general_superstaq as gss @@ -183,13 +183,18 @@ 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, @@ -205,7 +210,14 @@ def _map_compile_request_to_client_result( return cast( "QssCompileResultT_co", qss.SuperstaqJobV3(client=self._client, job_id=job_id) ) - return cast("QssCompileResultT_co", legacy_parser(json_dict)) + return cast( + "QssCompileResultT_co", + qss.compiler_output.CompilerOutput.read_json( + json_dict=json_dict, + circuits_is_list=circuits_is_list, + num_eca_circuits=num_eca_circuits, + ), + ) 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,