From d800e69298f59add7ba2a3e0d902e8ce59d19db4 Mon Sep 17 00:00:00 2001 From: CalMacCQ <93673602+CalMacCQ@users.noreply.github.com> Date: Fri, 8 Dec 2023 09:43:44 +0000 Subject: [PATCH 1/2] Remove redundant type declarations from docstrings (#297) * remove :type from api_wrappers docstrings * remove all instances of :type and :rtype * fix some issues with return types * fix formatting of QuantinuumBackend.process_circuits kwargs * update intersphinx syntax * update sphinx * add extension to requiremnets * use ~=6.2 for sphinx * use subsubsection heading * consistent capitalisation * fix link --- .github/workflows/docs/conf.py | 8 +-- .github/workflows/docs/requirements.txt | 3 +- .../quantinuum/backends/api_wrappers.py | 20 +------- .../quantinuum/backends/leakage_gadget.py | 7 --- .../quantinuum/backends/quantinuum.py | 49 ++----------------- 5 files changed, 11 insertions(+), 76 deletions(-) diff --git a/.github/workflows/docs/conf.py b/.github/workflows/docs/conf.py index 0a399612..b4528f5a 100644 --- a/.github/workflows/docs/conf.py +++ b/.github/workflows/docs/conf.py @@ -38,10 +38,10 @@ pytketdoc_base = "https://tket.quantinuum.com/api-docs/" intersphinx_mapping = { - "https://docs.python.org/3/": None, - pytketdoc_base: None, - "https://qiskit.org/documentation/": None, - "http://docs.qulacs.org/en/latest/": None, + "python": ("https://docs.python.org/3/", None), + "pytket": (pytketdoc_base, None), + "qiskit": ("https://qiskit.org/documentation/", None), + "qulacs": ("http://docs.qulacs.org/en/latest/", None), } autodoc_member_order = "groupwise" diff --git a/.github/workflows/docs/requirements.txt b/.github/workflows/docs/requirements.txt index d190a280..b344c427 100644 --- a/.github/workflows/docs/requirements.txt +++ b/.github/workflows/docs/requirements.txt @@ -1,4 +1,5 @@ -sphinx >= 4.3.2, <6.2.0 +sphinx ~= 6.2 sphinx_book_theme >= 1.0.1, <2.0 +sphinx-autodoc-typehints sphinx-copybutton enum-tools[sphinx] diff --git a/pytket/extensions/quantinuum/backends/api_wrappers.py b/pytket/extensions/quantinuum/backends/api_wrappers.py index 815c25c6..9047cca3 100644 --- a/pytket/extensions/quantinuum/backends/api_wrappers.py +++ b/pytket/extensions/quantinuum/backends/api_wrappers.py @@ -99,20 +99,14 @@ def __init__( :param token_store: JWT Token store, defaults to None A new MemoryCredentialStorage will be initialised if None is provided. - :type token_store: CredentialStorage, optional :param api_url: _description_, defaults to DEFAULT_API_URL - :type api_url: Optional[str], optional :param api_version: API version, defaults to 1 - :type api_version: int, optional :param use_websocket: Whether to use websocket to retrieve, defaults to True - :type use_websocket: bool, optional :param support_mfa: Whether to wait for the user to input the auth code, defaults to True - :type support_mfa: bool, optional :param session: Session for HTTP requests, defaults to None A new requests.Session will be initialised if None is provided - :type session: requests.Session, optional """ self.online = True @@ -341,9 +335,7 @@ def retrieve_job_status( Retrieves job status from device. :param job_id: unique id of job - :type job_id: str :param use_websocket: use websocket to minimize interaction - :type use_websocket: bool :return: (dict) output from API @@ -370,9 +362,7 @@ def retrieve_job( Retrieves job from device. :param job_id: unique id of job - :type job_id: str :param use_websocket: use websocket to minimize interaction - :type use_websocket: bool :return: (dict) output from API @@ -471,7 +461,6 @@ def status(self, machine: str) -> str: Check status of machine. :param machine: machine name - :type machine: str :return: (str) status of machine @@ -491,7 +480,6 @@ def cancel(self, job_id: str) -> dict: Cancels job. :param job_id: job ID to cancel - :type job_id: str :return: (dict) output from API @@ -531,7 +519,6 @@ def __init__(self, machine_list: Optional[list] = None): "n_shots": 10000, "batching": True, } - :type machine_list: list """ if machine_list == None: machine_list = [ @@ -575,22 +562,19 @@ def _get_machine_list(self) -> Optional[list]: return self.machine_list def full_login(self) -> None: - """No login offline with the offline API - :return: None""" + """No login offline with the offline API""" return None def login(self) -> str: """No login offline with the offline API, this function will always - return an empty api token - :return: empty api token""" + return an empty api token""" return "" def _submit_job(self, body: Dict) -> None: """The function will take the submitted job and store it for later :param body: submitted job - :type body: dict :return: None """ diff --git a/pytket/extensions/quantinuum/backends/leakage_gadget.py b/pytket/extensions/quantinuum/backends/leakage_gadget.py index 53e1210e..78a3316c 100644 --- a/pytket/extensions/quantinuum/backends/leakage_gadget.py +++ b/pytket/extensions/quantinuum/backends/leakage_gadget.py @@ -31,13 +31,9 @@ def get_leakage_gadget_circuit( :param circuit_qubit: Generated circuit detects whether leakage errors have occurred in this qubit. - :type circuit_qubit: Qubit :param postselection_qubit: Measured qubit to detect leakage error. - :type postselection_qubit: Qubit :param postselection_bit: Leakage detection result is written to this bit. - :type postselection_bit: Bit :return: Circuit for detecting leakage errors for specified ids. - :rtype: Circuit """ c = Circuit() c.add_qubit(circuit_qubit) @@ -63,13 +59,10 @@ def get_detection_circuit(circuit: Circuit, n_device_qubits: int) -> Circuit: additional Bit are written to a new register "leakage_detection_bit". :param circuit: Circuit to have leakage detection added. - :type circuit: Circuit :param n_device_qubits: Total number of qubits supported by the device being compiled to. - :type n_device_qubits: int :return: Circuit with leakage detection circuitry added. - :rtype: Circuit """ n_qubits: int = circuit.n_qubits if n_qubits == 0: diff --git a/pytket/extensions/quantinuum/backends/quantinuum.py b/pytket/extensions/quantinuum/backends/quantinuum.py index 50c28e07..a7e31c3d 100644 --- a/pytket/extensions/quantinuum/backends/quantinuum.py +++ b/pytket/extensions/quantinuum/backends/quantinuum.py @@ -223,7 +223,7 @@ class QuantinuumBackend(Backend): """ Interface to a Quantinuum device. More information about the QuantinuumBackend can be found on this page - https://tket.quantinuum.com/extensions/pytket-quantinuum/api/index.html + https://tket.quantinuum.com/extensions/pytket-quantinuum/index.html """ _supports_shots = True @@ -246,22 +246,15 @@ def __init__( """Construct a new Quantinuum backend. :param device_name: Name of device, e.g. "H1-1" - :type device_name: str :param label: Job labels used if Circuits have no name, defaults to "job" - :type label: Optional[str], optional :param simulator: Only applies to simulator devices, options are "state-vector" or "stabilizer", defaults to "state-vector" :param group: string identifier of a collection of jobs, can be used for usage tracking. - :type group: Optional[str], optional :param provider: select a provider for federated authentication. We currently only support 'microsoft', which enables the microsoft Device Flow. - :type provider: Optional[str], optional - :type simulator: str, optional :param api_handler: Instance of API handler, defaults to DEFAULT_API_HANDLER - :type api_handler: QuantinuumAPI :param compilation_config: Optional compilation configuration - :type compilation_config: QuantinuumBackendCompilationConfig Supported kwargs: @@ -326,9 +319,7 @@ def _available_devices( e.g. [{'name': 'H1', 'n_qubits': 6}] :param api_handler: Instance of API handler - :type api_handler: QuantinuumAPI :return: Dictionaries of machine name and number of qubits. - :rtype: List[Dict[str, Any]] """ id_token = api_handler.login() if api_handler.online: @@ -373,9 +364,7 @@ def available_devices( See :py:meth:`pytket.backends.Backend.available_devices`. :param api_handler: Instance of API handler, defaults to DEFAULT_API_HANDLER - :type api_handler: Optional[QuantinuumAPI] :return: A list of BackendInfo objects for each available Backend. - :rtype: List[BackendInfo] """ api_handler = kwargs.get("api_handler", DEFAULT_API_HANDLER) @@ -403,11 +392,8 @@ def device_state( :param device_name: Name of the device. - :type device_name: str :param api_handler: Instance of API handler, defaults to DEFAULT_API_HANDLER - :type api_handler: QuantinuumAPI :return: String of state, e.g. "online" - :rtype: str """ res = requests.get( f"{api_handler.url}machine/{device_name}", @@ -469,9 +455,7 @@ def default_compilation_pass(self, optimisation_level: int = 2) -> BasePass: :param optimisation_level: Allows values of 0,1 or 2, with higher values prompting more computationally heavy optimising compilation that can lead to reduced gate count in circuits. - :type optimisation_level: int :return: Compilation pass for compiling circuits to Quantinuum devices - :rtype: BasePass """ assert optimisation_level in range(3) passlist = [ @@ -554,9 +538,7 @@ def get_jobid(handle: ResultHandle) -> str: """Return the corresponding Quantinuum Job ID from a ResultHandle. :param handle: result handle. - :type handle: ResultHandle :return: Quantinuum API Job ID string. - :rtype: str """ return cast(str, handle[0]) @@ -618,44 +600,30 @@ def submit_program( """Submit a program directly to the backend. :param program: program (encoded as string) - :type program: str :param language: language - :type language: Language :param n_shots: Number of shots - :type n_shots: int :param name: Job name, defaults to None - :type name: Optional[str], optional :param noisy_simulation: Boolean flag to specify whether the simulator should perform noisy simulation with an error model defaults to True - :type noisy_simulation: bool :param group: String identifier of a collection of jobs, can be used for usage tracking. Overrides the instance variable `group`, defaults to None - :type group: Optional[str], optional :param wasm_file_handler: ``WasmFileHandler`` object for linked WASM module, defaults to None - :type wasm_file_handler: Optional[WasmFileHandler], optional :param no_opt: if true, requests that the backend perform no optimizations - :type no_opt: bool, defaults to False :param allow_2q_gate_rebase: if true, allow rebasing of the two-qubit gates to a higher-fidelity alternative gate at the discretion of the backend - :type allow_2q_gate_rebase: bool, defaults to False :param pytket_pass: ``pytket.passes.BasePass`` intended to be applied by the backend (beta feature, may be ignored), defaults to None - :type pytket_pass: Optional[BasePass], optional :param options: Items to add to the "options" dictionary of the request body - :type options: Optional[Dict[str, Any]], optional :param request_options: Extra options to add to the request body as a json-style dictionary, defaults to None - :type request_options: Optional[Dict[str, Any]], optional :param results_selection: Ordered list of register names and indices used to construct final :py:class:`BackendResult`. If None, all all results are used in lexicographic order. - :type results_selection: Optional[List[Tuple[str, int]]] :raises WasmUnsupported: WASM submitted to backend that does not support it. :raises QuantinuumAPIError: API error. :raises ConnectionError: Connection to remote API failed :return: ResultHandle for submitted job. - :rtype: ResultHandle """ body: Dict[str, Any] = { @@ -731,7 +699,8 @@ def process_circuits( """ See :py:meth:`pytket.backends.Backend.process_circuits`. - Supported kwargs: + Supported kwargs + ^^^^^^^^^^^^^^^^ * `postprocess`: apply end-of-circuit simplifications and classical postprocessing to improve fidelity of results (bool, default False) @@ -918,9 +887,7 @@ def start_batch( :param max_batch_cost: Maximum cost to be used for the batch, if a job exceeds the batch max it will be rejected. - :type max_batch_cost: int :return: Handle for submitted circuit. - :rtype: ResultHandle """ self._check_batchable() @@ -955,12 +922,9 @@ def add_to_batch( documentation on remaining parameters. :param batch_start_job: Handle of first circuit submitted to batch. - :type batch_start_job: ResultHandle :param batch_end: Boolean flag to signal the final circuit of batch, defaults to False - :type batch_end: bool, optional :return: Handle for submitted circuit. - :rtype: ResultHandle """ self._check_batchable() @@ -1046,11 +1010,9 @@ def get_partial_result( Retrieve partial results for a given job, regardless of its current state. :param handle: handle to results - :type handle: ResultHandle :return: A tuple containing the results and circuit status. If no results are available, the first element is None. - :rtype: Tuple[Optional[BackendResult], CircuitStatus] """ handle = self._update_result_handle(handle) job_id = self.get_jobid(handle) @@ -1143,18 +1105,13 @@ def cost( :param circuit: Circuit to calculate runtime estimate for. Must be valid for backend. - :type circuit: Circuit :param n_shots: Number of shots. - :type n_shots: int :param syntax_checker: Optional. Name of the syntax checker to use to get cost. For example for the "H1-1" device that would be "H1-1SC". For most devices this is automatically inferred, default=None. - :type syntax_checker: str :param use_websocket: Optional. Boolean flag to use a websocket connection. - :type use_websocket: bool :raises ValueError: Circuit is not valid, needs to be compiled. :return: Cost in HQC to execute the shots. - :rtype: float """ if not self.valid_circuit(circuit): raise ValueError( From 0f7b5fc70298047d24dd569d16e31203349d0ae6 Mon Sep 17 00:00:00 2001 From: Alec Edgington <54802828+cqc-alec@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:50:07 +0000 Subject: [PATCH 2/2] Make `QuantinuumBackend.cost()` safe (#298) --- docs/changelog.rst | 8 +++ .../quantinuum/backends/quantinuum.py | 53 +++++++++++++------ tests/integration/backend_test.py | 51 ++++++++++++++---- 3 files changed, 87 insertions(+), 25 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f94a59ec..5cd63821 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ Changelog ~~~~~~~~~ +Unreleased +---------- + +* ``QuantinuumBackend.cost()`` now raises an error if the ``syntax_checker`` + argument doesn't correspond to the device's reported syntax checker or if it + specifies a device that isn't a syntax checker; and the method returns 0 if + called on syntax-checker backends. + 0.26.0 (November 2023) ---------------------- diff --git a/pytket/extensions/quantinuum/backends/quantinuum.py b/pytket/extensions/quantinuum/backends/quantinuum.py index a7e31c3d..78f14ebe 100644 --- a/pytket/extensions/quantinuum/backends/quantinuum.py +++ b/pytket/extensions/quantinuum/backends/quantinuum.py @@ -1094,11 +1094,14 @@ def cost( **kwargs: QuumKwargTypes, ) -> Optional[float]: """ - Return the cost in HQC to complete this `circuit` with `n_shots` - repeats. - If the backend is not a syntax checker (backend name does not end with - "SC"), it is automatically appended - to check against the relevant syntax checker. + Return the cost in HQC to process this `circuit` with `n_shots` + repeats on this backend. + + The cost is obtained by sending the circuit to a "syntax-checker" + backend, which incurs no cost itself but reports what the cost would be + for the actual backend (``self``). + + If ``self`` is a syntax checker then the cost will be zero. See :py:meth:`QuantinuumBackend.process_circuits` for the supported kwargs. @@ -1119,20 +1122,38 @@ def cost( + " Try running `backend.get_compiled_circuit` first" ) + if self._MACHINE_DEBUG: + return 0.0 + + assert self.backend_info is not None + + if self.backend_info.get_misc("system_type") == "syntax checker": + return 0.0 + try: - syntax_checker = ( - syntax_checker - or cast(BackendInfo, self.backend_info).misc["syntax_checker"] - ) + syntax_checker_name = self.backend_info.misc["syntax_checker"] + if syntax_checker is not None and syntax_checker != syntax_checker_name: + raise ValueError( + f"Device {self._device_name}'s syntax checker is " + "{syntax_checker_name} but a different syntax checker " + "({syntax_checker}) was specified. You should omit the " + "`syntax_checker` argument to ensure the correct one is " + "used." + ) except KeyError: - raise NoSyntaxChecker( - "Could not find syntax checker for this backend," - " try setting one explicitly with the ``syntax_checker`` parameter" - ) + if syntax_checker is not None: + syntax_checker_name = syntax_checker + else: + raise NoSyntaxChecker( + "Could not find syntax checker for this backend, " + "try setting one explicitly with the ``syntax_checker`` " + "parameter (it will normally have a name ending in 'SC')." + ) + backend = QuantinuumBackend(syntax_checker_name, api_handler=self.api_handler) + assert backend.backend_info is not None + if backend.backend_info.get_misc("system_type") != "syntax checker": + raise ValueError(f"Device {backend._device_name} is not a syntax checker.") - backend = QuantinuumBackend( - cast(str, syntax_checker), api_handler=self.api_handler - ) try: handle = backend.process_circuit(circuit, n_shots, kwargs=kwargs) # type: ignore except DeviceNotAvailable as e: diff --git a/tests/integration/backend_test.py b/tests/integration/backend_test.py index 11abe4ef..51d87546 100644 --- a/tests/integration/backend_test.py +++ b/tests/integration/backend_test.py @@ -326,19 +326,52 @@ def test_cost_estimate( c = b.get_compiled_circuit(c) estimate = None if b._device_name.endswith("SC"): - with pytest.raises(NoSyntaxChecker) as e: - _ = b.cost(c, n_shots) - assert "Could not find syntax checker" in str(e.value) - estimate = b.cost(c, n_shots, syntax_checker=b._device_name, no_opt=False) + estimate = b.cost(c, n_shots) + assert estimate == 0.0 else: # All other real hardware backends should have the # "syntax_checker" misc property set, so there should be no # need of providing it explicitly. estimate = b.cost(c, n_shots, no_opt=False) - if estimate is None: - pytest.skip("API is flaky, sometimes returns None unexpectedly.") - assert isinstance(estimate, float) - assert estimate > 0.0 + if estimate is None: + pytest.skip("API is flaky, sometimes returns None unexpectedly.") + assert isinstance(estimate, float) + assert estimate > 0.0 + + +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +@pytest.mark.parametrize( + "authenticated_quum_backend", + [ + {"device_name": name} + for name in [ + *pytest.ALL_QUANTUM_HARDWARE_NAMES, # type: ignore + ] + ], + indirect=True, +) +@pytest.mark.timeout(120) +def test_cost_estimate_wrong_syntax_checker( + authenticated_quum_backend: QuantinuumBackend, +) -> None: + b = authenticated_quum_backend + c = Circuit(1).PhasedX(0.5, 0.5, 0).measure_all() + with pytest.raises(ValueError): + _ = b.cost(c, 10, syntax_checker="H6-2SC") + + +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +@pytest.mark.parametrize( + "authenticated_quum_backend", [{"device_name": "H1-1E"}], indirect=True +) +@pytest.mark.timeout(120) +def test_cost_estimate_bad_syntax_checker( + authenticated_quum_backend: QuantinuumBackend, +) -> None: + b = authenticated_quum_backend + c = Circuit(1).PhasedX(0.5, 0.5, 0).measure_all() + with pytest.raises(ValueError): + _ = b.cost(c, 10, syntax_checker="H2-1E") @pytest.mark.skipif(skip_remote_tests, reason=REASON) @@ -730,7 +763,7 @@ def test_wasm( @pytest.mark.skipif(skip_remote_tests, reason=REASON) @pytest.mark.parametrize( - "authenticated_quum_backend", [{"device_name": "H1-1SC"}], indirect=True + "authenticated_quum_backend", [{"device_name": "H1-1E"}], indirect=True ) @pytest.mark.timeout(120) def test_wasm_costs(