diff --git a/pytket/docs/changelog.rst b/pytket/docs/changelog.rst index 105d5b7a0c..f04ab1ed69 100644 --- a/pytket/docs/changelog.rst +++ b/pytket/docs/changelog.rst @@ -1,6 +1,20 @@ Changelog ================================== +0.16.0 (Unreleased) +------------------- + +Minor new features: + +* New :py:meth:``backends.Backend.run_circuit`` and :py:meth:``backends.Backend.run_circuits`` + methods. + +API changes: + +* The deprecated ``get_shots``, ``get_counts`` and ``get_state`` methods + of the ``Backend`` class are removed. Use ``run_circuits`` and the homonym + methods of the :py:class:`backends.backendresult.BackendResult` class instead. + 0.15.0 (September 2021) ----------------------- diff --git a/pytket/pytket/backends/backend.py b/pytket/pytket/backends/backend.py index 50e3cab293..1b3550c09d 100644 --- a/pytket/pytket/backends/backend.py +++ b/pytket/pytket/backends/backend.py @@ -21,7 +21,6 @@ List, Optional, Sequence, - Tuple, Union, Any, cast, @@ -30,18 +29,16 @@ from importlib import import_module from types import ModuleType -import numpy as np from typing_extensions import Literal -from pytket.circuit import BasisOrder, Bit, Circuit, OpType # type: ignore +from pytket.circuit import Bit, Circuit, OpType # type: ignore from pytket.passes import BasePass # type: ignore from pytket.predicates import Predicate # type: ignore from pytket.utils.outcomearray import OutcomeArray -from pytket.utils.results import KwargTypes, counts_from_shot_table +from pytket.utils.results import KwargTypes from .backend_exceptions import ( CircuitNotValidError, - InvalidResultType, CircuitNotRunError, ) from .backendinfo import BackendInfo @@ -254,14 +251,6 @@ def _check_handle_type(self, reshandle: ResultHandle) -> None: ) ) - @property - def persistent_handles(self) -> bool: - """ - Whether the backend produces `ResultHandle` objects that can be reused with - other instances of the backend class. - """ - return self._persistent_handles - def process_circuit( self, circuit: Circuit, @@ -320,95 +309,6 @@ def process_circuits( """ ... - @overload - @staticmethod - def _get_n_shots_as_list( - n_shots: Union[None, int, Sequence[Optional[int]]], - n_circuits: int, - optional: Literal[False], - ) -> List[int]: - ... - - @overload - @staticmethod - def _get_n_shots_as_list( - n_shots: Union[None, int, Sequence[Optional[int]]], - n_circuits: int, - optional: Literal[True], - set_zero: Literal[True], - ) -> List[int]: - ... - - @overload - @staticmethod - def _get_n_shots_as_list( - n_shots: Union[None, int, Sequence[Optional[int]]], - n_circuits: int, - optional: bool = True, - set_zero: bool = False, - ) -> Union[List[Optional[int]], List[int]]: - ... - - @staticmethod - def _get_n_shots_as_list( - n_shots: Union[None, int, Sequence[Optional[int]]], - n_circuits: int, - optional: bool = True, - set_zero: bool = False, - ) -> Union[List[Optional[int]], List[int]]: - """ - Convert any admissible n_shots value into List[Optional[int]] format. - - This validates the n_shots argument for process_circuits. If a single - value is passed, this value is broadcast to the number of circuits. - Additional boolean flags control how the argument is validated. - Raises an exception if n_shots is in an invalid format. - - :param n_shots: The argument to be validated. - :type n_shots: Union[None, int, Sequence[Optional[int]]] - :param n_circuits: Length of the converted argument returned. - :type n_circuits: int - :param optional: Whether n_shots can be None (default: True). - :type optional: bool - :param set_zero: Whether None values should be set to 0 (default: False). - :type set_zero: bool - :return: a list of length `n_circuits`, the converted argument - """ - - n_shots_list: List[Optional[int]] = [] - - def validate_n_shots(n: Optional[int]) -> bool: - return optional or (n is not None and n > 0) - - if set_zero and not optional: - ValueError("set_zero cannot be true when optional is false") - - if hasattr(n_shots, "__iter__"): - assert not isinstance(n_shots, int) - assert n_shots is not None - - if not all(map(validate_n_shots, n_shots)): - raise ValueError( - "n_shots values are required for all circuits for this backend" - ) - n_shots_list = list(n_shots) - else: - assert n_shots is None or isinstance(n_shots, int) - - if not validate_n_shots(n_shots): - raise ValueError("Parameter n_shots is required for this backend") - # convert n_shots to a list - n_shots_list = [n_shots] * n_circuits - - if len(n_shots_list) != n_circuits: - raise ValueError("The length of n_shots and circuits must match") - - if set_zero: - # replace None with 0 - n_shots_list = list(map(lambda n: n or 0, n_shots_list)) - - return n_shots_list - @abstractmethod def circuit_status(self, handle: ResultHandle) -> CircuitStatus: """ @@ -431,30 +331,6 @@ def pop_result(self, handle: ResultHandle) -> Optional[ResultCache]: """ return self._cache.pop(handle, None) - @property - def characterisation(self) -> Optional[dict]: - """Retrieve the characterisation targeted by the backend if it exists. - - :return: The characterisation that this backend targets if it exists. The - characterisation object contains device-specific information such as gate - error rates. - :rtype: Optional[dict] - """ - raise NotImplementedError( - "Backend does not support retrieving characterisation." - ) - - @property - def backend_info(self) -> Optional[BackendInfo]: - """Retrieve all Backend properties in a BackendInfo object, including - device architecture, supported gate set, gate errors and other hardware-specific - information. - - :return: The BackendInfo describing this backend if it exists. - :rtype: Optional[BackendInfo] - """ - raise NotImplementedError("Backend does not provide any device properties.") - def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult: """Return a BackendResult corresponding to the handle. @@ -487,13 +363,55 @@ def get_results( """ return [self.get_result(handle, **kwargs) for handle in handles] - def _process( - self, circuit: Circuit, **kwargs: KwargTypes - ) -> Tuple[BackendResult, ResultHandle]: - handle = self.process_circuit(circuit, **kwargs) # type: ignore - result = self.get_result(handle, **kwargs) - self.pop_result(handle) - return result, handle + def run_circuit( + self, + circuit: Circuit, + n_shots: Optional[int] = None, + valid_check: bool = True, + **kwargs: KwargTypes, + ) -> BackendResult: + """ + Submits a circuit to the backend and returns results + + :param circuit: Circuit to be executed + :param n_shots: Passed on to :py:meth:`Backend.process_circuit` + :param valid_check: Passed on to :py:meth:`Backend.process_circuit` + :return: Result + + This is a convenience method equivalent to calling + :py:meth:`Backend.process_circuit` followed by :py:meth:`Backend.get_result`. + Any additional keyword arguments are passed on to + :py:meth:`Backend.process_circuit` and :py:meth:`Backend.get_result`. + """ + return self.run_circuits( + [circuit], n_shots=n_shots, valid_check=valid_check, **kwargs + )[0] + + def run_circuits( + self, + circuits: Sequence[Circuit], + n_shots: Optional[Union[int, Sequence[int]]] = None, + valid_check: bool = True, + **kwargs: KwargTypes, + ) -> List[BackendResult]: + """ + Submits circuits to the backend and returns results + + :param circuits: Sequence of Circuits to be executed + :param n_shots: Passed on to :py:meth:`Backend.process_circuits` + :param valid_check: Passed on to :py:meth:`Backend.process_circuits` + :return: List of results + + This is a convenience method equivalent to calling + :py:meth:`Backend.process_circuits` followed by :py:meth:`Backend.get_results`. + Any additional keyword arguments are passed on to + :py:meth:`Backend.process_circuits` and :py:meth:`Backend.get_results`. + """ + handles = self.process_circuits(circuits, n_shots, valid_check, **kwargs) + results = self.get_results(handles, **kwargs) + for h in handles: + self.pop_result(h) + return results def cancel(self, handle: ResultHandle) -> None: """ @@ -506,158 +424,67 @@ def cancel(self, handle: ResultHandle) -> None: raise NotImplementedError("Backend does not support job cancellation.") @property - def supports_shots(self) -> bool: - """Does this backend support shot result retrieval via `get_shots`.""" - return self._supports_shots - - def get_shots( - self, - circuit: Circuit, - n_shots: Optional[int] = None, - basis: BasisOrder = BasisOrder.ilo, - valid_check: bool = True, - **kwargs: KwargTypes, - ) -> np.ndarray: - """Obtain the table of shots from an experiment. Accepts a - :py:class:`~pytket.circuit.Circuit` to be run and immediately returned. This - will fail if the circuit does not match the device's requirements. - - If the `postprocess` keyword argument is set to True, and the backend supports - the feature (see :py:meth:`supports_contextual_optimisation`), then contextual - optimisations are applied before running the circuit and retrieved results will - have any necessary classical postprocessing applied. This is not enabled by - default. + def characterisation(self) -> Optional[dict]: + """Retrieve the characterisation targeted by the backend if it exists. - :param circuit: The circuit to run - :type circuit: Circuit - :param n_shots: Number of shots to generate from the circuit. Defaults to None - :type n_shots: Optional[int], optional - :param basis: Toggle between ILO (increasing lexicographic order of bit ids) and - DLO (decreasing lexicographic order) for column ordering. Defaults to - BasisOrder.ilo. - :type basis: BasisOrder, optional - :param valid_check: Explicitly check that the circuit satisfies all of the - required predicates before running. Defaults to True - :type valid_check: bool, optional - :raises NotImplementedError: If backend implementation does not support shot - table retrieval - :return: Table of shot results. Each row is a single shot, with columns ordered - by classical bit order (according to `basis`). Entries are 0 or 1 - corresponding to qubit basis states. - :rtype: np.ndarray - """ - result, _ = self._process( - circuit, n_shots=n_shots, valid_check=valid_check, **kwargs - ) - c_bits = ( - sorted(result.c_bits.keys(), reverse=(basis is not BasisOrder.ilo)) - if result.c_bits - else None + :return: The characterisation that this backend targets if it exists. The + characterisation object contains device-specific information such as gate + error rates. + :rtype: Optional[dict] + """ + raise NotImplementedError( + "Backend does not support retrieving characterisation." ) - return result.get_shots(c_bits) @property - def supports_counts(self) -> bool: - """Does this backend support counts result retrieval via `get_counts`.""" - return self._supports_counts + def backend_info(self) -> Optional[BackendInfo]: + """Retrieve all Backend properties in a BackendInfo object, including + device architecture, supported gate set, gate errors and other hardware-specific + information. - def get_counts( - self, - circuit: Circuit, - n_shots: Optional[int] = None, - basis: BasisOrder = BasisOrder.ilo, - valid_check: bool = True, - **kwargs: KwargTypes, - ) -> Dict[Tuple[int, ...], int]: - """Obtain a summary of results, accumulating the shots for each result from an - experiment. Accepts a - :py:class:`~pytket.circuit.Circuit` to be run and immediately returned. This - will fail if the circuit does not match the device's requirements. + :return: The BackendInfo describing this backend if it exists. + :rtype: Optional[BackendInfo] + """ + raise NotImplementedError("Backend does not provide any device properties.") - If the `postprocess` keyword argument is set to True, and the backend supports - the feature (see :py:meth:`supports_contextual_optimisation`), then contextual - optimisatioons are applied before running the circuit and retrieved results will - have any necessary classical postprocessing applied. This is not enabled by - default. + @property + def persistent_handles(self) -> bool: + """ + Whether the backend produces `ResultHandle` objects that can be reused with + other instances of the backend class. + """ + return self._persistent_handles - :param circuit: The circuit to run - :type circuit: Circuit - :param n_shots: Number of shots to generate from the circuit. Defaults to None - :type n_shots: Optional[int], optional - :param basis: Toggle between ILO (increasing lexicographic order of bit ids) and - DLO (decreasing lexicographic order) for column ordering. Defaults to - BasisOrder.ilo. - :type basis: BasisOrder, optional - :param valid_check: Explicitly check that the circuit satisfies all of the - required predicates before running. Defaults to True - :type valid_check: bool, optional - :raises NotImplementedError: If backend implementation does not support counts - retrieval - :return: Dictionary mapping observed readouts to the number of times observed. - :rtype: Dict[Tuple[int, ...], int] + @property + def supports_shots(self) -> bool: """ + Does this backend support shot result retrieval via + :py:meth:`backendresult.BackendResult.get_shots`. + """ + return self._supports_shots - result, _ = self._process( - circuit, n_shots=n_shots, valid_check=valid_check, **kwargs - ) - c_bits = ( - sorted(result.c_bits.keys(), reverse=(basis is not BasisOrder.ilo)) - if result.c_bits - else None - ) - try: - return result.get_counts(c_bits) - except InvalidResultType: - shots = self.get_shots( - circuit, n_shots=n_shots, basis=basis, valid_check=valid_check, **kwargs - ) - return counts_from_shot_table(shots) + @property + def supports_counts(self) -> bool: + """ + Does this backend support counts result retrieval via + :py:meth:`backendresult.BackendResult.get_counts`. + """ + return self._supports_counts @property def supports_state(self) -> bool: - """Does this backend support statevector retrieval via `get_state`.""" + """ + Does this backend support statevector retrieval via + :py:meth:`backendresult.BackendResult.get_state`. + """ return self._supports_state - def get_state( - self, - circuit: Circuit, - basis: BasisOrder = BasisOrder.ilo, - valid_check: bool = True, - ) -> np.ndarray: - """Obtain a statevector from a simulation. Accepts a - :py:class:`~pytket.circuit.Circuit` to be run and immediately returned. This - will fail if the circuit does not match the simulator's requirements. - - :param circuit: The circuit to run - :type circuit: Circuit - :param basis: Toggle between ILO-BE (increasing lexicographic order of bit ids, - big-endian) and DLO-BE (decreasing lexicographic order, big-endian) for - ordering the coefficients. Defaults to BasisOrder.ilo. - :type basis: BasisOrder, optional - :param valid_check: Explicitly check that the circuit satisfies all of the - required predicates before running. Defaults to True - :type valid_check: bool, optional - :raises NotImplementedError: If backend implementation does not support - statevector retrieval - :return: A big-endian statevector for the circuit in the encoding given by - `basis`; e.g. :math:`[a_{00}, a_{01}, a_{10}, a_{11}]` where :math:`a_{01}` - is the amplitude of the :math:`\\left|01\\right>` state (in ILO, this means - qubit q[0] is in state :math:`\\left|0\\right>` and q[1] is in state - :math:`\\left|1\\right>`, and the reverse in DLO) - :rtype: np.ndarray - """ - - result, _ = self._process(circuit, valid_check=valid_check) - q_bits = ( - sorted(result.q_bits.keys(), reverse=(basis is not BasisOrder.ilo)) - if result.q_bits - else None - ) - return result.get_state(q_bits) - @property def supports_unitary(self) -> bool: - """Does this backend support unitary retrieval via `get_unitary`.""" + """ + Does this backend support unitary retrieval via + :py:meth:`backendresult.BackendResult.get_unitary`. + """ return self._supports_unitary @property @@ -723,3 +550,92 @@ def __extension_version__(self) -> Optional[str]: return self._get_extension_module().__extension_version__ # type: ignore except AttributeError: return None + + @overload + @staticmethod + def _get_n_shots_as_list( + n_shots: Union[None, int, Sequence[Optional[int]]], + n_circuits: int, + optional: Literal[False], + ) -> List[int]: + ... + + @overload + @staticmethod + def _get_n_shots_as_list( + n_shots: Union[None, int, Sequence[Optional[int]]], + n_circuits: int, + optional: Literal[True], + set_zero: Literal[True], + ) -> List[int]: + ... + + @overload + @staticmethod + def _get_n_shots_as_list( + n_shots: Union[None, int, Sequence[Optional[int]]], + n_circuits: int, + optional: bool = True, + set_zero: bool = False, + ) -> Union[List[Optional[int]], List[int]]: + ... + + @staticmethod + def _get_n_shots_as_list( + n_shots: Union[None, int, Sequence[Optional[int]]], + n_circuits: int, + optional: bool = True, + set_zero: bool = False, + ) -> Union[List[Optional[int]], List[int]]: + """ + Convert any admissible n_shots value into List[Optional[int]] format. + + This validates the n_shots argument for process_circuits. If a single + value is passed, this value is broadcast to the number of circuits. + Additional boolean flags control how the argument is validated. + Raises an exception if n_shots is in an invalid format. + + :param n_shots: The argument to be validated. + :type n_shots: Union[None, int, Sequence[Optional[int]]] + :param n_circuits: Length of the converted argument returned. + :type n_circuits: int + :param optional: Whether n_shots can be None (default: True). + :type optional: bool + :param set_zero: Whether None values should be set to 0 (default: False). + :type set_zero: bool + :return: a list of length `n_circuits`, the converted argument + """ + + n_shots_list: List[Optional[int]] = [] + + def validate_n_shots(n: Optional[int]) -> bool: + return optional or (n is not None and n > 0) + + if set_zero and not optional: + ValueError("set_zero cannot be true when optional is false") + + if hasattr(n_shots, "__iter__"): + assert not isinstance(n_shots, int) + assert n_shots is not None + + if not all(map(validate_n_shots, n_shots)): + raise ValueError( + "n_shots values are required for all circuits for this backend" + ) + n_shots_list = list(n_shots) + else: + assert n_shots is None or isinstance(n_shots, int) + + if not validate_n_shots(n_shots): + raise ValueError("Parameter n_shots is required for this backend") + # convert n_shots to a list + n_shots_list = [n_shots] * n_circuits + + if len(n_shots_list) != n_circuits: + raise ValueError("The length of n_shots and circuits must match") + + if set_zero: + # replace None with 0 + n_shots_list = list(map(lambda n: n or 0, n_shots_list)) + + return n_shots_list diff --git a/pytket/pytket/utils/expectations.py b/pytket/pytket/utils/expectations.py index 7193f665a0..e3a4246883 100644 --- a/pytket/pytket/utils/expectations.py +++ b/pytket/pytket/utils/expectations.py @@ -101,19 +101,17 @@ def get_pauli_expectation_value( state_circuit = backend.get_compiled_circuit(state_circuit) if backend.supports_expectation: return backend.get_pauli_expectation_value(state_circuit, pauli) # type: ignore - handle = backend.process_circuit(state_circuit) - state = backend.get_state(handle) - backend.pop_result(handle) + state = backend.run_circuit(state_circuit).get_state() return complex(pauli.state_expectation(state)) measured_circ = state_circuit.copy() append_pauli_measurement(pauli, measured_circ) measured_circ = backend.get_compiled_circuit(measured_circ) if backend.supports_counts: - counts = backend.get_counts(measured_circ, n_shots=n_shots) + counts = backend.run_circuit(measured_circ, n_shots=n_shots).get_counts() return expectation_from_counts(counts) elif backend.supports_shots: - shot_table = backend.get_shots(measured_circ, n_shots=n_shots) + shot_table = backend.run_circuit(measured_circ, n_shots=n_shots).get_shots() return expectation_from_shots(shot_table) else: raise ValueError("Backend does not support counts or shots") @@ -161,7 +159,8 @@ def get_operator_expectation_value( backend.expectation_allows_nonhermitian or all(z.imag == 0 for z in coeffs) ): return backend.get_operator_expectation_value(state_circuit, operator) # type: ignore - state = backend.get_state(state_circuit) + result = backend.run_circuit(state_circuit) + state = result.get_state() return operator.state_expectation(state) energy: complex id_string = QubitPauliString() diff --git a/pytket/tests/backend_test.py b/pytket/tests/backend_test.py index a9cb432bba..c4a66f5605 100644 --- a/pytket/tests/backend_test.py +++ b/pytket/tests/backend_test.py @@ -75,7 +75,8 @@ def test_bell() -> None: c.CX(0, 1) assert np.allclose(c.get_statevector(), sv) b = TketSimBackend() - assert np.allclose(b.get_state(c), sv) + r = b.run_circuit(c) + assert np.allclose(r.get_state(), sv) # Check that the "get_compiled_circuit" result is still the same. # (The circuit could change, due to optimisations). c = b.get_compiled_circuit(c) @@ -90,13 +91,15 @@ def test_basisorder() -> None: b = TketSimBackend() c = Circuit(2) c.X(1) - assert (b.get_state(c) == np.asarray([0, 1, 0, 0])).all() - assert (b.get_state(c, basis=BasisOrder.dlo) == np.asarray([0, 0, 1, 0])).all() + r = b.run_circuit(c) + assert (r.get_state() == np.asarray([0, 1, 0, 0])).all() + assert (r.get_state(basis=BasisOrder.dlo) == np.asarray([0, 0, 1, 0])).all() assert (c.get_statevector() == np.asarray([0, 1, 0, 0])).all() b = TketSimShotBackend() c.measure_all() - assert b.get_shots(c, n_shots=4, seed=4).shape == (4, 2) - assert b.get_counts(c, n_shots=4, seed=4) == {(0, 1): 4} + r = b.run_circuit(c, n_shots=4, seed=4) + assert r.get_shots().shape == (4, 2) + assert r.get_counts() == {(0, 1): 4} @pytest.mark.filterwarnings("ignore::PendingDeprecationWarning") @@ -112,8 +115,9 @@ def test_swaps() -> None: bs = TketSimBackend() c, c1 = bs.get_compiled_circuits((c, c1)) - s = bs.get_state(c) - s1 = bs.get_state(c1) + [r, r1] = bs.run_circuits([c, c1]) + s = r.get_state() + s1 = r1.get_state() assert np.allclose(s, s1) assert np.allclose(s_direct, s) assert np.allclose(s1_direct, s) @@ -160,15 +164,16 @@ def test_swaps_basisorder() -> None: b = TketSimBackend() c, c1 = b.get_compiled_circuits((c, c1)) - s_ilo = b.get_state(c1, basis=BasisOrder.ilo) - correct_ilo = b.get_state(c, basis=BasisOrder.ilo) + r, r1 = b.run_circuits((c, c1)) + s_ilo = r1.get_state(basis=BasisOrder.ilo) + correct_ilo = r.get_state(basis=BasisOrder.ilo) assert np.allclose(s_ilo, correct_ilo) assert np.allclose(s_ilo_direct, s_ilo) assert np.allclose(correct_ilo_direct, s_ilo) - s_dlo = b.get_state(c1, basis=BasisOrder.dlo) - correct_dlo = b.get_state(c, basis=BasisOrder.dlo) + s_dlo = r1.get_state(basis=BasisOrder.dlo) + correct_dlo = r.get_state(basis=BasisOrder.dlo) assert np.allclose(s_dlo, correct_dlo) qbs = c.qubits @@ -440,7 +445,8 @@ def test_tket_sim_backend_equivalence_with_circuit_functions() -> None: states = [circ.get_statevector(), compiled_circ.get_statevector()] unitaries = [circ.get_unitary(), compiled_circ.get_unitary()] - states.append(backend.get_state(compiled_circ)) + result = backend.run_circuit(compiled_circ) + states.append(result.get_state()) # paranoia: get_state should not alter the circuit states.append(compiled_circ.get_statevector()) diff --git a/pytket/tests/utils_test.py b/pytket/tests/utils_test.py index bc4d4cfabe..00745837a6 100644 --- a/pytket/tests/utils_test.py +++ b/pytket/tests/utils_test.py @@ -541,7 +541,7 @@ def unitary_from_states(circ: Circuit, back: Backend) -> np.ndarray: if val == "1": basis_circ.X(qb) basis_circ.append(circ) - outar[:, i] = back.get_state(basis_circ) + outar[:, i] = back.run_circuit(basis_circ).get_state() return outar @@ -582,7 +582,8 @@ def test_symbolic_conversion(circ: Circuit) -> None: back = TketSimBackend() circ = back.get_compiled_circuit(circ, 1) - simulated_state = back.get_state(circ) + result = back.run_circuit(circ) + simulated_state = result.get_state() assert np.allclose(numeric_state.T, simulated_state, atol=1e-10) simulated_unitary = unitary_from_states(circ, back)