diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 356312f4..b3a71336 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -31,12 +31,7 @@ jobs: cd tests pip install -r test_requirements.txt pytest --cov-report term-missing:skip-covered --cov=qermit --durations=10 - - name: Run mypy - if: github.event_name == 'pull_request' - run: mypy -p qermit - - name: Format check - if: github.event_name == 'pull_request' - run: flake8 qermit/ tests/ --ignore=E501,W503 + linux: name: Build and test (Linux) runs-on: ubuntu-latest @@ -82,3 +77,46 @@ jobs: cd tests pip install -r test_requirements.txt pytest --cov-report term-missing:skip-covered --cov=qermit --durations=10 + + build-docs: + name: Test documentation build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install Qermit + run: pip install . + - name: Build Docs + run: | + cd docs_src + pip install -r requirements.txt + ./build_docs.sh + cd ../manual + ./build_manual.sh + - name: Save documentation + uses: actions/upload-artifact@v2 + with: + name: docs_html + path: docs/ + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: 'docs/' + + formatting-checks: + name: Check typing and formatting + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Build qermit + run: | + pip install -e . -v + cd tests + pip install -r test_requirements.txt + - name: Run mypy + if: github.event_name == 'pull_request' + run: mypy -p qermit + - name: Format check + if: github.event_name == 'pull_request' + run: flake8 qermit/ tests/ --ignore=E501,W503 diff --git a/.gitignore b/.gitignore index 5bf79dfe..8869b613 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ .idea/ .dmypy.json .DS_Store +.venv +build +docs/ diff --git a/_version.py b/_version.py index 6a9beea8..3d187266 100644 --- a/_version.py +++ b/_version.py @@ -1 +1 @@ -__version__ = "0.4.0" +__version__ = "0.5.0" diff --git a/docs_src/CQCLogo.png b/docs_src/CQCLogo.png deleted file mode 100644 index 822c7b29..00000000 Binary files a/docs_src/CQCLogo.png and /dev/null differ diff --git a/docs_src/_static/Quantinuum_logo_black.png b/docs_src/_static/Quantinuum_logo_black.png new file mode 100644 index 00000000..5569581b Binary files /dev/null and b/docs_src/_static/Quantinuum_logo_black.png differ diff --git a/docs_src/_static/Quantinuum_logo_white.png b/docs_src/_static/Quantinuum_logo_white.png new file mode 100644 index 00000000..e896db91 Binary files /dev/null and b/docs_src/_static/Quantinuum_logo_white.png differ diff --git a/docs_src/_static/custom.css b/docs_src/_static/custom.css new file mode 100644 index 00000000..618fe1b1 --- /dev/null +++ b/docs_src/_static/custom.css @@ -0,0 +1,37 @@ +.wy-side-nav-search, +.wy-nav-top { + background: #5A46BE; +} + +.wy-grid-for-nav, +.wy-body-for-nav, +.wy-nav-side, +.wy-side-scroll, +.wy-menu, +.wy-menu-vertical { + background-color: #FFFFFF; +} + +.wy-menu-vertical a:hover { + background-color: #d9d9d9; +} + +.btn-link:visited, +.btn-link, +a:visited, +.a.reference.external, +.a.reference.internal, +.wy-menu-vertical a, +.wy-menu-vertical li, +.wy-menu-vertical ul, +.span.pre, +.sig-param, +.std.std-ref, + +html[data-theme=light] { + --pst-color-inline-code: rgb(199, 37, 78) !important; +} + +.sig-name { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/docs_src/conf.py b/docs_src/conf.py index 7253af09..42dc5027 100644 --- a/docs_src/conf.py +++ b/docs_src/conf.py @@ -18,13 +18,13 @@ # -- Project information ----------------------------------------------------- project = "Qermit" -copyright = "2021, Cambridge Quantum Computing Ltd" -author = "Cambridge Quantum Computing Ltd" +copyright = "2023 Quantinuum" +author = "Quantinuum" # The short X.Y version -version = "0.4.0" +version = "0.5.0" # The full version, including alpha/beta/rc tags -release = "0.4.0" +release = "0.5.0" # -- General configuration --------------------------------------------------- @@ -41,6 +41,7 @@ "sphinx.ext.autosummary", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. @@ -66,16 +67,26 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme" +html_theme = "sphinx_book_theme" + +html_theme_options = { + "repository_url": "https://github.com/CQCL/qermit", + "use_repository_button": True, + "use_issues_button": True, + "logo": { + "image_light": "_static/Quantinuum_logo_black.png", + "image_dark": "_static/Quantinuum_logo_white.png", + }, +} + +html_static_path = ["_static"] +html_css_files = ["custom.css"] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs_src/index.rst b/docs_src/index.rst index d4ad7930..7cceefbd 100644 --- a/docs_src/index.rst +++ b/docs_src/index.rst @@ -1,14 +1,10 @@ Qermit ====== -.. image:: CQCLogo.png - :width: 120px - :align: right - -``qermit`` is a python module for running error-mitigation protocols on quantum computers using ``pytket``, -CQC's python module for interfacing with `CQC `_ tket, a set of quantum programming tools. +``qermit`` is a python module for running error-mitigation protocols on quantum computers using the ``pytket`` +python module for interfacing with tket, a set of quantum programming tools developed by `Quantinuum `_. ``qermit`` functions using the ``pytket`` :py:class:`Backend` class, meaning ``qermit`` supports any of the -`numerous providers `_ ``pytket`` does. +`numerous providers `_ ``pytket`` does. ``qermit`` also provides easy composability of error-mitigation methods, meaning it is practically straightforward to run an experiment with multiple forms of error-mitigation where appropriate. @@ -137,6 +133,8 @@ are several ways to contact us: measurement_reduction.rst spam.rst frame_randomisation.rst + postselection.rst + leakage_detection.rst clifford_noise_characterisation.rst zero_noise_extrapolation.rst probabilistic_error_cancellation.rst diff --git a/docs_src/leakage_detection.rst b/docs_src/leakage_detection.rst new file mode 100644 index 00000000..53c9b5c3 --- /dev/null +++ b/docs_src/leakage_detection.rst @@ -0,0 +1,5 @@ +qermit.leakage_detection +======================== + +.. automethod:: qermit.leakage_detection.leakage_detection.get_leakage_detection_mitres + diff --git a/docs_src/postselection.rst b/docs_src/postselection.rst new file mode 100644 index 00000000..11099a5e --- /dev/null +++ b/docs_src/postselection.rst @@ -0,0 +1,9 @@ +qermit.postselection +==================== + +.. automethod:: qermit.postselection.postselect_mitres.gen_postselect_mitres + +.. autoclass:: qermit.postselection.postselect_manager.PostselectMgr + :members: + :special-members: + diff --git a/docs_src/requirements.txt b/docs_src/requirements.txt index ac634e4d..601e8d8f 100644 --- a/docs_src/requirements.txt +++ b/docs_src/requirements.txt @@ -1,4 +1,4 @@ -sphinx +sphinx > 4.2.0, <6.2.0 sphinx_autodoc_annotation -sphinx_rtd_theme +sphinx_book_theme ~= 1.0.1 jupyter_sphinx diff --git a/manual/_static/Quantinuum_logo_black.png b/manual/_static/Quantinuum_logo_black.png new file mode 100644 index 00000000..5569581b Binary files /dev/null and b/manual/_static/Quantinuum_logo_black.png differ diff --git a/manual/_static/Quantinuum_logo_white.png b/manual/_static/Quantinuum_logo_white.png new file mode 100644 index 00000000..e896db91 Binary files /dev/null and b/manual/_static/Quantinuum_logo_white.png differ diff --git a/manual/_static/custom.css b/manual/_static/custom.css new file mode 100644 index 00000000..618fe1b1 --- /dev/null +++ b/manual/_static/custom.css @@ -0,0 +1,37 @@ +.wy-side-nav-search, +.wy-nav-top { + background: #5A46BE; +} + +.wy-grid-for-nav, +.wy-body-for-nav, +.wy-nav-side, +.wy-side-scroll, +.wy-menu, +.wy-menu-vertical { + background-color: #FFFFFF; +} + +.wy-menu-vertical a:hover { + background-color: #d9d9d9; +} + +.btn-link:visited, +.btn-link, +a:visited, +.a.reference.external, +.a.reference.internal, +.wy-menu-vertical a, +.wy-menu-vertical li, +.wy-menu-vertical ul, +.span.pre, +.sig-param, +.std.std-ref, + +html[data-theme=light] { + --pst-color-inline-code: rgb(199, 37, 78) !important; +} + +.sig-name { + font-size: 1.25rem; +} \ No newline at end of file diff --git a/manual/conf.py b/manual/conf.py index 4c483e74..dde62ca6 100644 --- a/manual/conf.py +++ b/manual/conf.py @@ -4,8 +4,8 @@ # See https://www.sphinx-doc.org/en/master/usage/configuration.html project = "qermit-manual" -copyright = "2020-2021 Cambridge Quantum Computing Ltd" -author = "Cambridge Quantum Computing Ltd" +copyright = "2023 Quantinuum" +author = "Quantinuum" extensions = [ "sphinx.ext.autodoc", @@ -14,4 +14,17 @@ "jupyter_sphinx", ] -html_theme = "sphinx_rtd_theme" +html_theme = "sphinx_book_theme" + +html_theme_options = { + "repository_url": "https://github.com/CQCL/qermit", + "use_repository_button": True, + "use_issues_button": True, + "logo": { + "image_light": "_static/Quantinuum_logo_black.png", + "image_dark": "_static/Quantinuum_logo_white.png", + }, +} + +html_static_path = ["_static"] +html_css_files = ["custom.css"] diff --git a/qermit/__init__.py b/qermit/__init__.py index bca60026..e1197545 100644 --- a/qermit/__init__.py +++ b/qermit/__init__.py @@ -20,12 +20,7 @@ modify the expectation value of some observable (MitEx). """ from qermit.taskgraph.task_graph import TaskGraph # noqa:F401 -from qermit.taskgraph.mittask import ( # noqa:F401 - MitTask, - AnsatzCircuit, - CircuitShots, - ObservableExperiment, -) +from qermit.taskgraph.mittask import MitTask, AnsatzCircuit, CircuitShots, ObservableExperiment # noqa:F401 from qermit.taskgraph.mitres import MitRes # noqa:F401 from qermit.taskgraph.mitex import MitEx # noqa:F401 from qermit.taskgraph.utils import SymbolsDict, MeasurementCircuit, ObservableTracker # noqa:F401 diff --git a/qermit/clifford_noise_characterisation/ccl.py b/qermit/clifford_noise_characterisation/ccl.py index 86ce92cc..9405cdea 100644 --- a/qermit/clifford_noise_characterisation/ccl.py +++ b/qermit/clifford_noise_characterisation/ccl.py @@ -40,6 +40,8 @@ from enum import Enum import warnings from pytket.passes import auto_rebase_pass +from typing import cast +from pytket.unit_id import UnitID ufr_gateset = {OpType.CX, OpType.Rz, OpType.H} @@ -174,10 +176,13 @@ def gen_state_circuits( for i in range(len(all_coms)): com = all_coms[i] if com.op.type == OpType.Rz: - new_circuit.add_barrier(com.qubits) - angle = sample_weighted_clifford_angle(com.op.params[0]) - new_circuit.add_gate(com.op.type, [angle], com.qubits) - new_circuit.add_barrier(com.qubits) + new_circuit.add_barrier(cast(List[UnitID], com.qubits)) + original_angle = com.op.params[0] + if not isinstance(original_angle, float): + raise Exception("Circuit cannot include parameters which are not floats.") + angle = sample_weighted_clifford_angle(original_angle) + new_circuit.add_gate(com.op.type, [angle], cast(List[UnitID], com.qubits)) + new_circuit.add_barrier(cast(List[UnitID], com.qubits)) # Measure gate has special case, but can assume 1 qubit to 1 bit elif com.op.type is OpType.Measure: new_circuit.Measure(com.qubits[0], com.bits[0]) @@ -186,7 +191,7 @@ def gen_state_circuits( new_circuit.add_barrier(com.args) # CX or H gate, add as is else: - new_circuit.add_gate(com.op.type, com.qubits) + new_circuit.add_gate(com.op.type, cast(List[UnitID], com.qubits)) # all circuits accepted and run, some results later discarded if not accepted by Metropolis-Hastings rule state_circuits.append(new_circuit) @@ -219,32 +224,35 @@ def gen_state_circuits( for i in range(len(all_coms)): com = all_coms[i] if com.op.type == OpType.Rz: - new_circuit.add_barrier(com.qubits) + new_circuit.add_barrier(cast(List[UnitID], com.qubits)) # 3 sets of gates int must be in # in clifford_pair_elements means gate has been denominated as Clifford, # but is in some sampled pair so add original angle if i in clifford_pair_elements: - new_circuit.add_gate(com.op.type, com.op.params, com.qubits) - new_circuit.add_barrier(com.qubits) + new_circuit.add_gate(com.op.type, com.op.params, cast(List[UnitID], com.qubits)) + new_circuit.add_barrier(cast(List[UnitID], com.qubits)) # in non_clifford_pair_elements mean gate was denominated to be left non-Clifford, # but its value has been sampled in a pair to now be Clifford # random angle is sampled and returned elif i in non_clifford_pair_elements: - angle = sample_weighted_clifford_angle(com.op.params[0]) - new_circuit.add_gate(com.op.type, [angle], com.qubits) - new_circuit.add_barrier(com.qubits) + original_angle = com.op.params[0] + if not isinstance(original_angle, float): + raise Exception("Circuit cannot include parameters which are not floats.") + angle = sample_weighted_clifford_angle(original_angle) + new_circuit.add_gate(com.op.type, [angle], cast(List[UnitID], com.qubits)) + new_circuit.add_barrier(cast(List[UnitID], com.qubits)) # in cliffords mean it is denominated as Clifford, and hasn't been sampled for a pair # as clifford_pair_elements has already been checked # in this case, cliffords is a dict between Rz index and substitution S power # get power from dict, multiply by 0.5 to get angle, add to circuit elif i in cliffords: - new_circuit.add_gate(com.op.type, [0.5 * cliffords[i]], com.qubits) - new_circuit.add_barrier(com.qubits) + new_circuit.add_gate(com.op.type, [0.5 * cliffords[i]], cast(List[UnitID], com.qubits)) + new_circuit.add_barrier(cast(List[UnitID], com.qubits)) # final case means gate was chosen to retain non-Clifford, and has not been # sampled in any pair, so add original angle. else: - new_circuit.add_gate(com.op.type, com.op.params, com.qubits) - new_circuit.add_barrier(com.qubits) + new_circuit.add_gate(com.op.type, com.op.params, cast(List[UnitID], com.qubits)) + new_circuit.add_barrier(cast(List[UnitID], com.qubits)) # Measure gate has special case, but can assume 1 qubit to 1 bit elif com.op.type is OpType.Measure: new_circuit.Measure(com.qubits[0], com.bits[0]) @@ -253,7 +261,7 @@ def gen_state_circuits( new_circuit.add_barrier(com.args) # CX or H gate, add as is else: - new_circuit.add_gate(com.op.type, com.qubits) + new_circuit.add_gate(com.op.type, cast(List[UnitID], com.qubits)) # all circuits accepted and run, some results later discarded if not accepted by Metropolis-Hastings rule state_circuits.append(new_circuit) @@ -329,7 +337,6 @@ def task( all_close = True attempt = 0 while all_close and attempt < max_state_circuits_attempts: - state_circuits = gen_state_circuits( c_copy, n_non_cliffords, diff --git a/qermit/clifford_noise_characterisation/cdr_post.py b/qermit/clifford_noise_characterisation/cdr_post.py index 15ce35fb..8aa8eca6 100644 --- a/qermit/clifford_noise_characterisation/cdr_post.py +++ b/qermit/clifford_noise_characterisation/cdr_post.py @@ -112,7 +112,6 @@ def cdr_quality_check_task( """ for calibration, original in zip(state_circuit_exp, noisy_expectation): - # The noisy expectation value of the original circuit. original_coefficient = original.to_list()[0]["coefficient"][0] diff --git a/qermit/frame_randomisation/frame_randomisation.py b/qermit/frame_randomisation/frame_randomisation.py index 943825c7..1c2155ed 100644 --- a/qermit/frame_randomisation/frame_randomisation.py +++ b/qermit/frame_randomisation/frame_randomisation.py @@ -36,6 +36,8 @@ class FrameRandomisation(Enum): + + @staticmethod def PauliFrameRandomisation( circuit: Circuit, shots: int, samples: int ) -> List[CircuitShots]: @@ -56,6 +58,7 @@ def PauliFrameRandomisation( pfr_circuits = pfr.sample_circuits(circuit, samples) return [CircuitShots(Circuit=c, Shots=pfr_shots) for c in pfr_circuits] + @staticmethod def UniversalFrameRandomisation( circuit: Circuit, shots: int, samples: int ) -> List[CircuitShots]: diff --git a/qermit/leakage_detection/__init__.py b/qermit/leakage_detection/__init__.py new file mode 100644 index 00000000..2fa3a4cd --- /dev/null +++ b/qermit/leakage_detection/__init__.py @@ -0,0 +1 @@ +from .leakage_detection import get_leakage_detection_mitres # noqa:F401 diff --git a/qermit/leakage_detection/leakage_detection.py b/qermit/leakage_detection/leakage_detection.py new file mode 100644 index 00000000..2d9c5fcf --- /dev/null +++ b/qermit/leakage_detection/leakage_detection.py @@ -0,0 +1,101 @@ +from qermit.postselection import PostselectMgr +from qermit.postselection.postselect_mitres import gen_postselect_task +from qermit import CircuitShots, MitRes, MitTask, TaskGraph +from copy import deepcopy +from typing import List, Tuple, cast +from pytket.extensions.quantinuum.backends.leakage_gadget import get_detection_circuit # type: ignore +from pytket.backends import Backend +from pytket.backends.backendinfo import BackendInfo + + +def gen_add_leakage_gadget_circuit_task(backend: Backend) -> MitTask: + """Generates task adding leakage gadget circuits to given circuts. + + :param backend: Backend on which the circuit will be run. + :type backend: Backend + :return: Task adding leakage gadget circuits to given circuts. + :rtype: MitTask + """ + + if backend.backend_info is None: + raise Exception("This backend has no nodes.") + + n_device_qubits = cast(BackendInfo, backend.backend_info).n_nodes + + def task( + obj, circuit_shots_list: List[CircuitShots] + ) -> Tuple[List[CircuitShots], List[PostselectMgr]]: + """Task adding leakage gadget circuits to given circuts. This reuses + methods from pytket-quantinuum. A list of the corresponding + postselection managers is also created. + + :param circuit_shots_list: List of circuits to which leakage gadget + circuit should be added. + :type circuit_shots_list: List[CircuitShots] + :return: Circuits with gadget added, and list of corresponding + post selection managers. + :rtype: Tuple[List[CircuitShots], List[PostselectMgr]] + """ + + # Add leakage detection gadget to each inputted circuit. + detection_circuit_shots_list = [ + CircuitShots( + Circuit=get_detection_circuit( + circuit=circuit_shots.Circuit, + n_device_qubits=n_device_qubits, + ), + Shots=circuit_shots.Shots, + ) + for circuit_shots in circuit_shots_list + ] + + # For each circuit create a postselection manager. These may be + # different for each circuit, if for example the circuits are of + # different sizes. + postselect_mgr_list = [ + PostselectMgr( + compute_cbits=orig_circuit.Circuit.bits, + postselect_cbits=list( + set(detection_circuit.Circuit.bits).difference( + set(orig_circuit.Circuit.bits) + ) + ), + ) + for orig_circuit, detection_circuit in zip( + circuit_shots_list, detection_circuit_shots_list + ) + ] + + return ( + detection_circuit_shots_list, + postselect_mgr_list, + ) + + return MitTask( + _label="AddLeakageGadget", + _n_in_wires=1, + _n_out_wires=2, + _method=task, + ) + + +def get_leakage_detection_mitres(backend: Backend, **kwargs) -> MitRes: + """Generate MitRes making use of leakage detection and postselection. + + :param backend: Backend on which the circuits are run. + :type backend: Backend + :return: MitRes making use of leakage detection and postselection. + :rtype: MitRes + """ + + _mitres = deepcopy( + kwargs.get("mitres", MitRes(backend, _label="LeakageDetectionMitRes")) + ) + _taskgraph = TaskGraph().from_TaskGraph(_mitres) + _taskgraph.add_wire() + # Prepend task adding leakage detection circuits. + _taskgraph.prepend(gen_add_leakage_gadget_circuit_task(backend)) + # Append task removing shots where leakage is detected + _taskgraph.append(gen_postselect_task()) + + return MitRes(backend).from_TaskGraph(_taskgraph) diff --git a/qermit/mock_backend/mock_quantinuum_backend.py b/qermit/mock_backend/mock_quantinuum_backend.py index 440c6e51..06afa7c1 100644 --- a/qermit/mock_backend/mock_quantinuum_backend.py +++ b/qermit/mock_backend/mock_quantinuum_backend.py @@ -19,76 +19,21 @@ from pytket.extensions.quantinuum import QuantinuumBackend # type: ignore from pytket.extensions.quantinuum.backends.quantinuum import _GATE_SET # type: ignore from pytket.predicates import CompilationUnit # type: ignore -from pytket.extensions.qiskit import AerBackend # type: ignore -import qiskit.providers.aer.noise as noise # type: ignore from pytket import OpType from pytket import Circuit from pytket.backends.resulthandle import ResultHandle from typing import List, Union from pytket.backends.backendresult import BackendResult - - -class NoisyAerBackend(AerBackend): - noisy_gate_set = {OpType.CX, OpType.H, OpType.Rz, OpType.Rz, OpType.Measure} - - def __init__(self, n_qubits: int, prob_1: float, prob_2: float, prob_ro: float): - """AerBacked with simple depolarising and SPAM noise model. - - :param n_qubits: The number of qubits available on the backend. - :type n_qubits: int - :param prob_1: The depolarising noise error rates on single qubit gates. - :type prob_1: float - :param prob_2: The depolarising noise error rates on two qubit gates. - :type prob_2: float - :param prob_ro: Error rates of symmetric uncorrelated SPAM errors. - :type prob_ro: float - """ - - noise_model = self.depolarizing_noise_model(n_qubits, prob_1, prob_2, prob_ro) - super().__init__(noise_model=noise_model) - - def depolarizing_noise_model( - self, - n_qubits: int, - prob_1: float, - prob_2: float, - prob_ro: float, - ) -> noise.NoiseModel: - """Generates noise model, may be passed to `noise_model` parameter of - AerBacked. - - :param n_qubits: Number of qubits noise model applies to. - :type n_qubits: int - :param prob_1: The depolarising noise error rates on single qubit gates. - :type prob_1: float - :param prob_2: The depolarising noise error rates on two qubit gates. - :type prob_2: float - :param prob_ro: Error rates of symmetric uncorrelated SPAM errors. - :type prob_ro: float - :return: Noise model - :rtype: noise.NoiseModel - """ - - noise_model = noise.NoiseModel() - - error_2 = noise.depolarizing_error(prob_2, 2) - for edge in [[i, j] for i in range(n_qubits) for j in range(i)]: - noise_model.add_quantum_error(error_2, ["cx"], [edge[0], edge[1]]) - noise_model.add_quantum_error(error_2, ["cx"], [edge[1], edge[0]]) - - error_1 = noise.depolarizing_error(prob_1, 1) - for node in range(n_qubits): - noise_model.add_quantum_error(error_1, ["h", "rx", "rz"], [node]) - - probabilities = [[1 - prob_ro, prob_ro], [prob_ro, 1 - prob_ro]] - error_ro = noise.ReadoutError(probabilities) - for i in range(n_qubits): - noise_model.add_readout_error(error_ro, [i]) - - return noise_model +from .noisy_aer_backend import NoisyAerBackend class MockQuantinuumBackend(QuantinuumBackend): + """Backend mocking some of the features of QuantinuumBackend. + In particular the gateset and connectivity of the backend is replicated + so that compilation behaviour is reproduced. Some noise (unrelated to + that on the device) is also applied. + """ + gate_set = _GATE_SET gate_set.add(OpType.ZZPhase) @@ -96,12 +41,11 @@ class MockQuantinuumBackend(QuantinuumBackend): name="MockQuantinuumBackend", device_name="mock-quantinuum", version="n/a", - architecture=FullyConnected(10, "node"), + architecture=FullyConnected(10, "q"), gate_set=gate_set, + n_cl_reg=100, ) - noisy_gate_set = {OpType.CX, OpType.H, OpType.Rz, OpType.Rz, OpType.Measure} - def __init__(self): super(MockQuantinuumBackend, self).__init__(device_name="H1-1SC") self.noisy_backend = NoisyAerBackend( @@ -125,7 +69,8 @@ def process_circuit( :param valid_check: Explicitly check that all circuits satisfy all required predicates to run on the backend, defaults to True :type valid_check: bool, optional - :return: Handles to results for each input circuit, as an interable in the same order as the circuits. + :return: Handles to results for each input circuit, as an interable + in the same order as the circuits. :rtype: ResultHandle """ @@ -134,10 +79,12 @@ def process_circuit( noisy_circuit = circuit.copy() cu = CompilationUnit(noisy_circuit) - auto_rebase_pass(gateset=self.noisy_gate_set).apply(cu) - self.noisy_backend.default_compilation_pass(optimisation_level=0).apply(cu) - assert GateSetPredicate(self.noisy_gate_set).verify(cu.circuit) + self.noisy_backend.default_compilation_pass(optimisation_level=0).apply(cu) + auto_rebase_pass(gateset=self.noisy_backend.noisy_gate_set).apply(cu) + assert GateSetPredicate( + self.noisy_backend.noisy_gate_set.union({OpType.Reset, OpType.Barrier}) + ).verify(cu.circuit) handle = self.noisy_backend.process_circuit(cu.circuit, n_shot) self.handle_cu_dict[handle] = cu diff --git a/qermit/mock_backend/noisy_aer_backend.py b/qermit/mock_backend/noisy_aer_backend.py new file mode 100644 index 00000000..b8b78056 --- /dev/null +++ b/qermit/mock_backend/noisy_aer_backend.py @@ -0,0 +1,84 @@ +# Copyright 2019-2023 Quantinuum +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pytket.extensions.qiskit import AerBackend # type: ignore +from pytket import OpType +import qiskit.providers.aer.noise as noise # type: ignore + + +class NoisyAerBackend(AerBackend): + """AerBacked with simple depolarising and SPAM noise model. Depolarising + noise is added to the gateset {OpType.CX, OpType.H, OpType.Rz, + OpType.Rz, OpType.Measure} and circuits should be rebased into + that gateset before running. + """ + + noisy_gate_set = {OpType.CX, OpType.H, OpType.Rz, OpType.Rz, OpType.Measure} + + def __init__(self, n_qubits: int, prob_1: float, prob_2: float, prob_ro: float): + """AerBacked with simple depolarising and SPAM noise model. + + :param n_qubits: The number of qubits available on the backend. + :type n_qubits: int + :param prob_1: The depolarising noise error rates on single qubit gates. + :type prob_1: float + :param prob_2: The depolarising noise error rates on two qubit gates. + :type prob_2: float + :param prob_ro: Error rates of symmetric uncorrelated SPAM errors. + :type prob_ro: float + """ + + super().__init__( + noise_model=self.depolarizing_noise_model(n_qubits, prob_1, prob_2, prob_ro) + ) + + def depolarizing_noise_model( + self, + n_qubits: int, + prob_1: float, + prob_2: float, + prob_ro: float, + ) -> noise.NoiseModel: + """Generates noise model, may be passed to `noise_model` parameter of + AerBacked. + + :param n_qubits: Number of qubits noise model applies to. + :type n_qubits: int + :param prob_1: The depolarising noise error rates on single qubit gates. + :type prob_1: float + :param prob_2: The depolarising noise error rates on two qubit gates. + :type prob_2: float + :param prob_ro: Error rates of symmetric uncorrelated SPAM errors. + :type prob_ro: float + :return: Noise model + :rtype: noise.NoiseModel + """ + + noise_model = noise.NoiseModel() + + error_2 = noise.depolarizing_error(prob_2, 2) + for edge in [[i, j] for i in range(n_qubits) for j in range(i)]: + noise_model.add_quantum_error(error_2, ["cx"], [edge[0], edge[1]]) + noise_model.add_quantum_error(error_2, ["cx"], [edge[1], edge[0]]) + + error_1 = noise.depolarizing_error(prob_1, 1) + for node in range(n_qubits): + noise_model.add_quantum_error(error_1, ["h", "rx", "rz"], [node]) + + probabilities = [[1 - prob_ro, prob_ro], [prob_ro, 1 - prob_ro]] + error_ro = noise.ReadoutError(probabilities) + for i in range(n_qubits): + noise_model.add_readout_error(error_ro, [i]) + + return noise_model diff --git a/qermit/postselection/__init__.py b/qermit/postselection/__init__.py new file mode 100644 index 00000000..322f7e66 --- /dev/null +++ b/qermit/postselection/__init__.py @@ -0,0 +1,2 @@ +from .postselect_manager import PostselectMgr # noqa:F401 +from .postselect_mitres import gen_postselect_mitres # noqa:F401 diff --git a/qermit/postselection/postselect_manager.py b/qermit/postselection/postselect_manager.py new file mode 100644 index 00000000..851bb931 --- /dev/null +++ b/qermit/postselection/postselect_manager.py @@ -0,0 +1,119 @@ +from collections import Counter +from pytket.backends.backendresult import BackendResult +from pytket.utils.outcomearray import OutcomeArray +from pytket.circuit import Bit +from typing import List, Tuple, Dict + + +class PostselectMgr: + """Class for tracking and applying post selection to results. + Includes other methods to analyse the results after post selection. + """ + + def __init__( + self, + compute_cbits: List[Bit], + postselect_cbits: List[Bit], + ): + """Initialisation method. + + :param compute_cbits: Bits in the circuit which are not affected + by post selection. + :type compute_cbits: List[Bit] + :param postselect_cbits: Bits on which the post selection is based. + :type postselect_cbits: List[Bit] + :raises Exception: Raised if a bit is in both compute_cbits + and postselect_cbits. + """ + + intersect = set(compute_cbits).intersection(set(postselect_cbits)) + if intersect: + raise Exception( + f"{intersect} are post select and compute qubits. " + + "They cannot be both." + ) + + self.compute_cbits: List[Bit] = compute_cbits + self.postselect_cbits: List[Bit] = postselect_cbits + + self.cbits: List[Bit] = compute_cbits + postselect_cbits + + def get_postselected_shot(self, shot: Tuple[int, ...]) -> Tuple[int, ...]: + "Removes postselection bits from shot." + return tuple( + [ + bit + for bit, reg in zip(shot, self.cbits) + if reg not in self.postselect_cbits + ] + ) + + def is_postselect_shot(self, shot: Tuple[int, ...]) -> bool: + "Determines if shot survives postselection" + + # TODO: It may be nice to generalise this so that other functions + # besides bit==0 can be used as a means of postselection. + return all( + bit == 0 + for bit, reg in zip(shot, self.cbits) + if reg in self.postselect_cbits + ) + + def dict_to_result(self, result_dict: Dict[Tuple[int, ...], int]) -> BackendResult: + """Convert dictionary to BackendResult. + + :param result_dict: Dictionary to convert. + :type result_dict: Dict[Tuple[int, ...], int] + :return: Corresponding BackendResult. + :rtype: BackendResult + """ + + # Special case where the dictionary is empty. Presently having + # an empty counter results in an error. + if not result_dict: + return BackendResult() + + return BackendResult( + counts=Counter( + { + OutcomeArray.from_readouts([key]): val + for key, val in result_dict.items() + } + ), + c_bits=self.compute_cbits, + ) + + def postselect_result(self, result: BackendResult) -> BackendResult: + """Transforms BackendResult to keep only shots which should be + post selected. + + :param result: Result to be modified. + :type result: BackendResult + :return: Postselected shots. + :rtype: BackendResult + """ + + return self.dict_to_result( + { + self.get_postselected_shot(shot): count + for shot, count in result.get_counts(cbits=self.cbits).items() + if self.is_postselect_shot(shot) + } + ) + + def merge_result(self, result: BackendResult) -> BackendResult: + """Transforms BackendResult so that postselection bits are + removed, but no shots are removed by postselection. + + :param result: Result to be transformed. + :type result: BackendResult + :return: Result with postselection bits removed. + :rtype: BackendResult + """ + + merge_dict: Dict[Tuple[int, ...], int] = {} + for shot, count in result.get_counts(cbits=self.cbits).items(): + postselected_shot = self.get_postselected_shot(shot) + merge_dict[postselected_shot] = merge_dict.get(postselected_shot, 0) + count + + return self.dict_to_result(merge_dict) diff --git a/qermit/postselection/postselect_mitres.py b/qermit/postselection/postselect_mitres.py new file mode 100644 index 00000000..1ab33eed --- /dev/null +++ b/qermit/postselection/postselect_mitres.py @@ -0,0 +1,101 @@ +from .postselect_manager import PostselectMgr +from qermit import CircuitShots, MitRes, MitTask, TaskGraph +from copy import deepcopy +from typing import List, Tuple +from pytket.backends.backendresult import BackendResult +from pytket.backends import Backend + + +def gen_postselect_task() -> MitTask: + """Generates task applying postselection to given results. + + :return: Task applying postselection to given results. + :rtype: MitTask + """ + + def task( + obj, + result_list: List[BackendResult], + postselect_mgr_list: List[PostselectMgr], + ) -> Tuple[List[BackendResult]]: + """Task applying postselection to given results. + + :param result_list: List od results to which postselection should + be applied. + :type result_list: List[BackendResult] + :param postselect_mgr_list: List of postselection managers to apply + to results. + :type postselect_mgr_list: List[PostselectMgr] + :return: List of results after postselection has been applied. + :rtype: Tuple[List[BackendResult]] + """ + + return ( + [ + postselect_mgr.postselect_result(result) + for result, postselect_mgr in zip(result_list, postselect_mgr_list) + ], + ) + + return MitTask( + _label="PostselectResults", + _n_in_wires=2, + _n_out_wires=1, + _method=task, + ) + + +def gen_postselect_mgr_gen_task(postselect_mgr: PostselectMgr) -> MitTask: + """Generates task applying the same post selection manager to all + circuits. + + :param postselect_mgr: Postselection manager to apply. + :type postselect_mgr: PostselectMgr + :return: Task applying the same post selection manager to all circuits. + :rtype: MitTask + """ + + def task( + obj, circ_shots_list: List[CircuitShots] + ) -> Tuple[List[CircuitShots], List[PostselectMgr]]: + """Task applying the same post selection manager to all circuits. + + :param circ_shots_list: List of circuits to which post selection is + applied. + :type circ_shots_list: List[CircuitShots] + :return: List od circuits and corresponding postselection managers. + :rtype: Tuple[List[CircuitShots], List[PostselectMgr]] + """ + + return (circ_shots_list, [postselect_mgr for _ in circ_shots_list]) + + return MitTask( + _label="ConstantNode", + _n_in_wires=1, + _n_out_wires=2, + _method=task, + ) + + +def gen_postselect_mitres( + backend: Backend, postselect_mgr: PostselectMgr, **kwargs +) -> MitRes: + """Generates MitRes running given circuit and applying postselection. + + :param backend: Backend on this circuits are run. + :type backend: Backend + :param postselect_mgr: Postselection manager. + :type postselect_mgr: PostselectMgr + :return: MitRes running given circuit and applying postselection. + :rtype: MitRes + """ + + _mitres = deepcopy( + kwargs.get("mitres", MitRes(backend, _label="PostselectionMitRes")) + ) + _taskgraph = TaskGraph().from_TaskGraph(_mitres) + _taskgraph.add_wire() + _taskgraph.prepend(gen_postselect_mgr_gen_task(postselect_mgr)) + _taskgraph.append(gen_postselect_task()) + + return MitRes(backend).from_TaskGraph(_taskgraph) diff --git a/qermit/probabilistic_error_cancellation/pec_learning_based.py b/qermit/probabilistic_error_cancellation/pec_learning_based.py index 268603dd..1dac4827 100644 --- a/qermit/probabilistic_error_cancellation/pec_learning_based.py +++ b/qermit/probabilistic_error_cancellation/pec_learning_based.py @@ -41,6 +41,7 @@ from pytket.pauli import QubitPauliString # type: ignore from pytket.predicates import CliffordCircuitPredicate # type: ignore +from pytket.unit_id import Qubit import re from typing import List, Tuple, Dict, cast, Union, Any @@ -66,7 +67,7 @@ def str_to_pauli_op(pauli_str: str) -> Op: "Y": Op.create(OpType.Y), "I": Op.create(OpType.noop), } - return switcher.get(pauli_str) + return switcher[pauli_str] def random_commuting_clifford( @@ -142,7 +143,7 @@ def random_commuting_clifford( new_qps_qbs.append(n_q_map[x]) qps_paulis.append(qps_dict[x]) - new_qps = QubitPauliString(new_qps_qbs, qps_paulis) + new_qps = QubitPauliString(cast(List[Qubit], new_qps_qbs), qps_paulis) place_with_map(rand_cliff_circ, n_q_map) @@ -177,7 +178,7 @@ def random_commuting_clifford( return rand_cliff_circ -def substitute_pauli(circ: Circuit, frame_name: str, pauli_pair: List[str]) -> Circuit: +def substitute_pauli(circ: Circuit, frame_name: str, pauli_pair: List[Op]) -> Circuit: """ Replace 2 qubit Pauli gate pair which surrounds Frame gate with a 2 qubit pauli gate and its inverse. @@ -185,8 +186,8 @@ def substitute_pauli(circ: Circuit, frame_name: str, pauli_pair: List[str]) -> C :type circ: Circuit :param frame_name: The opgroup of the Frame gate that the pauli gates will act either side of :type frame_name: str - :param pauli_pair: Two strings describing the 2 qubit Pauli gate - :type pauli_pair: List[str] + :param pauli_pair: Two Ops describing the 2 qubit Pauli gate + :type pauli_pair: List[Op] :return: A circuit, with the Pauli gates inserted either side of the given frame gate. :rtype: Circuit """ @@ -211,7 +212,7 @@ def substitute_pauli(circ: Circuit, frame_name: str, pauli_pair: List[str]) -> C def substitute_pauli_but_one( - circ: Circuit, to_replace_opgroup: str, pauli_pair: List[str] + circ: Circuit, to_replace_opgroup: str, pauli_pair: List[Op] ) -> Circuit: """Sets all Pauli gates to the identity, apart from those around the inputted Frame gate, described by its opgroup. @@ -221,8 +222,8 @@ def substitute_pauli_but_one( :param to_replace_opgroup: The opgroup of the frame gate who's corresponding Pauli gates should be replaced with the inputted gate. :type to_replace_opgroup: str - :param pauli_pair: Two strings describing the Pauli gate to be substituted in - :type pauli_pair: List[str] + :param pauli_pair: Two Ops describing the Pauli gate to be substituted in + :type pauli_pair: List[Op] :raises RuntimeError: Raised if the inputted circuit does not have a gate in the opgroup inputted. :return: The final circuit with the Pauli gates substituted in. :rtype: Circuit @@ -443,7 +444,6 @@ def task( all_obs_exp_index.add(obs_exp["experiment"]) for obs_exp_index in all_obs_exp_index: - # For each experiment index, create a list of the list structure information # of results corresponding to that experiment index. fixed_obs_exp_details = [ @@ -461,7 +461,6 @@ def task( fixed_obs_exp_results = [] for qps_index in all_qps_index: - # For each QubitPauliString index, create a list of the list structure information # of results corresponding to that QubitPauliString index. fixed_qps_details = [ @@ -473,7 +472,6 @@ def task( fixed_qps_results = [] for cliff_details in fixed_qps_details: - # For each Clifford circuit corresponding to a fixed experiment and # QuasiPauliString, gather the noisy results list structure information. fixed_cliff_details = [ @@ -485,7 +483,6 @@ def task( fixed_cliff_results = [] for noisy_details in fixed_cliff_details: - # For each noisy circuit corresponding to a fixed experiment, # QubitPauliString and Clifford circuit, gather the noisy ideal result pair. fixed_cliff_results.append( @@ -526,7 +523,7 @@ def learn_quasi_probs_task_gen(num_cliff_circ: int) -> MitTask: def task( obj, - results: List[List[List[Tuple[QubitPauliOperator]]]], + results: List[List[List[List[Tuple[QubitPauliOperator, QubitPauliOperator]]]]], ) -> Tuple[List[List[QuasiProbabilities]]]: """This implementation of learning base probabilistic error cancellation is based on the significant error approach of https://arxiv.org/abs/2005.07601 @@ -535,7 +532,7 @@ def task( expectation results for a fixed ObservableExperiment, QubitPauliString, clifford circuit, and Pauli noise. Each list level fixes consecutively an ObservableExperiment, QubitPauliString, and clifford circuit. - :type results: List[ List[List[Tuple[QubitPauliOperator]]] ] + :type results: List[List[List[List[Tuple[QubitPauliOperator, QubitPauliOperator]]]]] :return: List of quasi probabilities. The outer list corresponds to circuits, the second level list corresponds to Pauli strings, and the inner most list corresponds to quasi probabilities. @@ -543,12 +540,11 @@ def task( """ prob_list = [] - # qps_results is List[List[Tuple[QubitPauliOperator, QubitPauliOperator]]] + # qps_results is List[List[List[Tuple[QubitPauliOperator, QubitPauliOperator]]]] # each tuple is a noisy, noiseless pair of results for perturbed fixed Clifford circuit # each inner list is results for fixed clifford circuit # each outerlist is for a single Qubit Pauli String in experiment for qps_results in results: - qps_quasi_prob_list = [] # qps is List[List[Tuple[QubitPauliOperator, QubitPauliOperator]]] # containing all results for all fixed Clifford circuits @@ -797,13 +793,11 @@ def wrap_frame_gates(circ: Circuit) -> Circuit: framed_circ_command_list = [] for command in circ_command_list: - # Add command to new list if not a Frame gate. if "Computing" in command["opgroup"]: framed_circ_command_list.append(command.copy()) elif "Frame" in command["opgroup"]: - match_return = re.match(r"Frame (.*)", command["opgroup"]) if match_return is None: raise ValueError( @@ -926,10 +920,8 @@ def list_pauli_gates(circ: Circuit) -> List[Dict]: # the error acts on at most one Frame gate, it is enough to specify the error # and the Frame gate on which it acts. for opgroup in frame_opgroup_list: - for q1_pauli in ["X", "Y", "Z", "I"]: for q2_pauli in ["X", "Y", "Z", "I"]: - if (q1_pauli == "I") and (q2_pauli == "I"): continue @@ -973,7 +965,6 @@ def task( # For each circuit, create an equivalent circuit but on which one of the # possible errors occur. for experiment_num, experiment in enumerate(wire): - pauli_errors = list_pauli_gates(experiment.AnsatzCircuit.Circuit) for error_num, error in enumerate(pauli_errors): diff --git a/qermit/spam/full_spam_correction.py b/qermit/spam/full_spam_correction.py index a3e02718..ef35ac65 100644 --- a/qermit/spam/full_spam_correction.py +++ b/qermit/spam/full_spam_correction.py @@ -28,10 +28,11 @@ StateInfo, CorrectionMethod, ) +from pytket.unit_id import Node def gen_full_tomography_spam_circuits_task( - backend: Backend, shots: int, qubit_subsets: List[List[Qubit]] + backend: Backend, shots: int, qubit_subsets: List[List[Node]] ) -> MitTask: """Generate MitTask for calibration circuits according to the specified correlation and given backend. @@ -56,7 +57,7 @@ def task( # check correlations distance if ( obj.characterisation["FullCorrelatedSpamCorrection"].CorrelatedNodes - is qubit_subsets + == qubit_subsets ): return (wire, [], []) @@ -78,7 +79,7 @@ def task( def gen_full_tomography_spam_characterisation_task( - qubit_subsets: List[List[Qubit]], + qubit_subsets: List[List[Node]], ) -> MitTask: """ Uses results from device for characterisation circuits to characterise transition matrices diff --git a/qermit/spam/full_transition_tomography.py b/qermit/spam/full_transition_tomography.py index c5be2f84..27f071ac 100644 --- a/qermit/spam/full_transition_tomography.py +++ b/qermit/spam/full_transition_tomography.py @@ -26,6 +26,7 @@ from pytket.backends.backendresult import BackendResult from pytket.utils.outcomearray import OutcomeArray from enum import Enum +from pytket.unit_id import UnitID FullCorrelatedNoiseCharacterisation = namedtuple( "FullCorrelatedNoiseCharacterisation", @@ -86,8 +87,11 @@ def get_full_transition_tomography_circuits( should be processed without compilation. :rtype: List[Circuit] """ + def to_tuple(correlation_list: List[Node]) -> Tuple[Node, ...]: + return tuple(correlation_list) + subsets_matrix_map = OrderedDict.fromkeys( - sorted(map(tuple, correlations), key=len, reverse=True) + sorted(map(to_tuple, correlations), key=len, reverse=True) ) # ordered from largest to smallest via OrderedDict & sorted subset_dimensions = [len(subset) for subset in subsets_matrix_map] @@ -108,13 +112,14 @@ def get_full_transition_tomography_circuits( # set up CircBox of X gate for preparing basis states xcirc = Circuit(1).X(0) - xcirc = backend.get_compiled_circuit(xcirc) + xcirc = backend.get_compiled_circuit(xcirc, optimisation_level=0) FlattenRegisters().apply(xcirc) xbox = CircBox(xcirc) # need to be default register to add as box suitably n_qubits_pre_compile = process_circuit.n_qubits - process_circuit = backend.get_compiled_circuit(process_circuit) + # This needs to be optimisation level 0 to avoid using simplify initial + process_circuit = backend.get_compiled_circuit(process_circuit, optimisation_level=0) while process_circuit.n_qubits < n_qubits_pre_compile: process_circuit.add_qubit(Qubit("temp_q", process_circuit.n_qubits)) @@ -122,7 +127,8 @@ def get_full_transition_tomography_circuits( rename_map_pc = {} for index, qb in enumerate(process_circuit.qubits): rename_map_pc[qb] = Qubit(index) - process_circuit.rename_units(rename_map_pc) + process_circuit.rename_units(cast(Dict[UnitID, UnitID], rename_map_pc)) + pbox = CircBox(process_circuit) # set up base circuit for appending xbox to @@ -148,14 +154,14 @@ def get_full_transition_tomography_circuits( new_state_dicts[qubits] = major_state[:dim] # find only qubits that are expected to be in 1 state, add xbox to given qubits for flipped_qb in itertools.compress(qubits, major_state[:dim]): - state_circuit.add_circbox(xbox, [flipped_qb]) + state_circuit.add_circbox(xbox, [cast(UnitID, flipped_qb)]) # Decompose boxes, add barriers to preserve circuit, add measures - state_circuit.add_barrier(all_qubits) + state_circuit.add_barrier(cast(List[UnitID], all_qubits)) # add process circuit to measure - state_circuit.add_circbox(pbox, state_circuit.qubits) + state_circuit.add_circbox(pbox, cast(List[UnitID], state_circuit.qubits)) DecomposeBoxes().apply(state_circuit) - state_circuit.add_barrier(all_qubits) + state_circuit.add_barrier(cast(List[UnitID], all_qubits)) for q in measures: state_circuit.Measure(q, measures[q]) # add to returned types @@ -168,7 +174,7 @@ def get_full_transition_tomography_circuits( def calculate_correlation_matrices( results_list: List[BackendResult], states_info: List[StateInfo], - correlations: List[List[Qubit]], + correlations: List[List[Node]], ) -> FullCorrelatedNoiseCharacterisation: """Calculate the calibration matrices corresponding to some pure noise from the results of running calibration circuits. @@ -180,14 +186,17 @@ def calculate_correlation_matrices( representation and the qubit_to_bit_map for the corresponding state circuit. :type states_info: List[StateInfo] :param correlations: List of dict corresponding to each prepared basis state - :type correlations: List[List[Qubit]] + :type correlations: List[List[Node]] :return: Characterisation for pure noise given by process circuit :rtype: FullCorrelatedNoiseCharacterisation """ + def to_tuple(correlation_list: List[Node]) -> Tuple[Node, ...]: + return tuple(correlation_list) + subsets_matrix_map = OrderedDict.fromkeys( - sorted(map(tuple, correlations), key=len, reverse=True) + sorted(map(to_tuple, correlations), key=len, reverse=True) ) # ordered from largest to smallest via OrderedDict & sorted subset_dimensions = [len(subset) for subset in subsets_matrix_map] diff --git a/qermit/spectral_filtering/signal_filter.py b/qermit/spectral_filtering/signal_filter.py index eabe9202..8ddd7a97 100644 --- a/qermit/spectral_filtering/signal_filter.py +++ b/qermit/spectral_filtering/signal_filter.py @@ -1,4 +1,4 @@ -from abc import ABC +from abc import ABC, abstractmethod import numpy as np from copy import deepcopy from numpy.typing import NDArray @@ -7,6 +7,7 @@ class SignalFilter(ABC): """Base class for signal filtering.""" + @abstractmethod def filter(self, fft_result_val_grid: NDArray[np.float64]) -> NDArray[np.float64]: """Method transforming array of floats into filtered array of floats.""" diff --git a/qermit/spectral_filtering/spectral_filtering.py b/qermit/spectral_filtering/spectral_filtering.py index c2367c8c..cf1a5229 100644 --- a/qermit/spectral_filtering/spectral_filtering.py +++ b/qermit/spectral_filtering/spectral_filtering.py @@ -61,7 +61,6 @@ def task( interpolated_result_list = [] for result, points, obs_exp in zip(result_list, points_list, obs_exp_list): - # Extract point to be interpolated to. This is the symbol values # given in the initial experiment definition. interpolation_point = list( @@ -158,7 +157,6 @@ def task( fft_result_grid_list = [] for result_grid_dict in result_grid_dict_list: - # Perform the FFT on grids corresponding to each QubitPauliString. fft_result_grid_dict = dict() for qps, exp_val_grid in result_grid_dict.items(): @@ -197,7 +195,6 @@ def task( result_dict_list = [] for qpo_result_grid in result_grid_list: - # Take the QubitPauliOperator that is being measured from the 0 # coordinate element of the grid. zero_qpo_result_grid = qpo_result_grid[ @@ -419,7 +416,6 @@ def task( # ObservableExperiment in obs_exp_list obs_exp_grid_list = [] for obs_exp, sym_val_grid_list in zip(obs_exp_list, obs_exp_sym_val_grid_list): - # Initialise empty grid of ObservableExperiment obs_exp_grid = np.empty( sym_val_grid_list[0].shape, dtype=ObservableExperiment @@ -431,7 +427,6 @@ def task( [i for i in range(size)] for size in sym_val_grid_list[0].shape ] for grid_point in product(*grid_point_val_list): - # Generate dictionary mapping every symbol to it's value at # the given point in the grid. sym_map = { diff --git a/qermit/taskgraph/graphviz.py b/qermit/taskgraph/graphviz.py index 06231509..e950aed8 100644 --- a/qermit/taskgraph/graphviz.py +++ b/qermit/taskgraph/graphviz.py @@ -73,7 +73,6 @@ def _format_html_label(**kwargs): def _html_ports(ports: Iterable[str]) -> str: - return _HTML_PORTS_ROW_TEMPLATE.format( port_cells="".join( _HTML_PORT_TEMPLATE.format( diff --git a/qermit/taskgraph/mitex.py b/qermit/taskgraph/mitex.py index f86156c5..d24e0480 100644 --- a/qermit/taskgraph/mitex.py +++ b/qermit/taskgraph/mitex.py @@ -389,12 +389,15 @@ def __str__(self): def __call__( # type: ignore[override] self, experiment_wires: List[List[ObservableExperiment]], + cache: bool = False, characterisation: dict = {}, ) -> Tuple[List[QubitPauliOperator]]: return cast( Tuple[List[QubitPauliOperator]], super().run( - cast(List[Wire], experiment_wires), characterisation=characterisation + cast(List[Wire], experiment_wires), + cache=cache, + characterisation=characterisation, ), ) @@ -472,7 +475,10 @@ def add_wire(self): raise TypeError("MitEx.add_wire forbidden.") def run( # type: ignore[override] - self, mitex_wires: List[ObservableExperiment], characterisation: dict = {} + self, + mitex_wires: List[ObservableExperiment], + cache: bool = False, + characterisation: dict = {}, ) -> List[QubitPauliOperator]: """ Overloaded run method. @@ -495,7 +501,7 @@ def run( # type: ignore[override] :return: Observable experiment results as QubitPauliOperator, where values are expectations. :rtype: List[QubitPauliOperator] """ - return self([mitex_wires], characterisation)[0] + return self([mitex_wires], cache, characterisation)[0] def run_basic( self, mitex_wires: List[Tuple[CircuitShots, QubitPauliOperator]] diff --git a/qermit/taskgraph/mitres.py b/qermit/taskgraph/mitres.py index bd51fad3..be2c7108 100644 --- a/qermit/taskgraph/mitres.py +++ b/qermit/taskgraph/mitres.py @@ -29,6 +29,7 @@ from pytket.utils.outcomearray import OutcomeArray from pytket import Bit import numpy as np # type: ignore +from pytket import Circuit def backend_compile_circuit_shots_task_gen( @@ -88,7 +89,7 @@ def task(obj, circuit_wires: List[CircuitShots]) -> Tuple[List[ResultHandle]]: circs, shots = map(list, zip(*circuit_wires)) results = backend.process_circuits( - circs, n_shots=cast(Sequence[int], shots) + cast(List[Circuit], circs), n_shots=cast(Sequence[int], shots) ) return (results,) @@ -110,7 +111,6 @@ def backend_res_task_gen(backend: Backend) -> MitTask: """ def task(obj, handles: List[ResultHandle]) -> Tuple[List[BackendResult]]: - results = backend.get_results(handles) return (results,) @@ -216,11 +216,13 @@ def check_append_wires(self, task: Union[MitTask, "TaskGraph"]) -> bool: def __str__(self) -> str: return f"" - def __call__(self, circuits_wire: List[List[CircuitShots]], characterisation: dict = {}) -> Tuple[List[BackendResult]]: # type: ignore[override] + def __call__(self, circuits_wire: List[List[CircuitShots]], cache: bool = False, characterisation: dict = {}) -> Tuple[List[BackendResult]]: # type: ignore[override] return cast( Tuple[List[BackendResult]], super().run( - cast(List[Wire], circuits_wire), characterisation=characterisation + cast(List[Wire], circuits_wire), + cache=cache, + characterisation=characterisation, ), ) @@ -297,7 +299,7 @@ def add_wire(self): """ raise TypeError("MitRes.add_wire forbidden.") - def run(self, circuit_shots: List[CircuitShots], characterisation: dict = {}) -> List[BackendResult]: # type: ignore[override] + def run(self, circuit_shots: List[CircuitShots], cache: bool = False, characterisation: dict = {}) -> List[BackendResult]: # type: ignore[override] """ Overloaded run method from TaskGraph class to add type checking. A single experiment is defined by a Tuple containing a circuit to be run @@ -312,7 +314,7 @@ def run(self, circuit_shots: List[CircuitShots], characterisation: dict = {}) -> :return: A BackendResult object for each combination of circuit and shots. :rtype: List[BackendResult] """ - return self([circuit_shots], characterisation)[0] + return self([circuit_shots], cache, characterisation)[0] def split_shots_task_gen(max_shots: int) -> MitTask: diff --git a/qermit/taskgraph/mittask.py b/qermit/taskgraph/mittask.py index 0276e26a..ce5218ea 100644 --- a/qermit/taskgraph/mittask.py +++ b/qermit/taskgraph/mittask.py @@ -13,7 +13,7 @@ # limitations under the License. -from typing import List, Union, Callable, Dict +from typing import List, Union, Callable, Dict, Optional from collections import namedtuple from types import MethodType from enum import Enum @@ -82,7 +82,7 @@ def __init__( _label: str, _n_in_wires: int, _n_out_wires: int, - _method: Callable, + _method: Optional[Callable] = None, ): self._label = _label self._n_in_wires = _n_in_wires diff --git a/qermit/taskgraph/task_graph.py b/qermit/taskgraph/task_graph.py index ca85065c..cba7b941 100644 --- a/qermit/taskgraph/task_graph.py +++ b/qermit/taskgraph/task_graph.py @@ -357,7 +357,7 @@ def run( from the input vertex of the _task_graph. :type input_wires: List[Wire] :param cache: If True each Tasks output data is stored in an OrderedDict with the - Task.label_ attribute as its key. + Task._label attribute as its key. :type cache: bool diff --git a/qermit/taskgraph/utils.py b/qermit/taskgraph/utils.py index 5174ad91..8d16ba38 100644 --- a/qermit/taskgraph/utils.py +++ b/qermit/taskgraph/utils.py @@ -22,7 +22,7 @@ from pytket.backends.backendresult import BackendResult from copy import copy from sympy import Symbol # type: ignore -from typing import Iterable, Dict, Union, Tuple, List +from typing import Iterable, Dict, Union, Tuple, List, Optional from collections import OrderedDict from numpy import ndarray # type: ignore @@ -36,7 +36,7 @@ class SymbolsDict(object): symbols. """ - def __init__(self): + def __init__(self) -> None: """ Default constructor, creates an empty OrderedDict() object for future symbols to be added to. """ @@ -177,7 +177,7 @@ class MeasurementCircuit(object): for some Ansatz Circuit. """ - def __init__(self, symbolic_circuit: Circuit, symbols: SymbolsDict = None): + def __init__(self, symbolic_circuit: Circuit, symbols: Optional[SymbolsDict] = None): """ Stores information required to instantiate any MeasurementCircuit with parameterised symbols. @@ -187,7 +187,7 @@ def __init__(self, symbolic_circuit: Circuit, symbols: SymbolsDict = None): :type symbols: SymbolsDict """ self._symbolic_circuit: Circuit = symbolic_circuit - if symbols is None: + if not symbols: self._symbols: SymbolsDict = SymbolsDict.symbols_from_circuit( symbolic_circuit ) @@ -224,7 +224,7 @@ def get_parametric_circuit(self) -> Circuit: :rtype: Circuit """ _circuit = self._symbolic_circuit.copy() - _circuit.symbol_substitution(self._symbols._symbolic_map) + _circuit.symbol_substitution(self._symbols._symbolic_map) # type: ignore return _circuit @@ -255,7 +255,7 @@ def __init__(self, qubit_pauli_operator: QubitPauliOperator = QubitPauliOperator for k in self._qubit_pauli_operator._dict.keys(): self._qps_to_indices[k] = list() self._measurement_circuits: List[MeasurementCircuit] = list() - self._partitions: List[QubitPauliString] = list() + self._partitions: List[List[QubitPauliString]] = list() def from_ObservableTracker(to_copy: "ObservableTracker") -> "ObservableTracker": """ @@ -284,15 +284,15 @@ def __str__(self): def __repr__(self): return str(self) - def clear(self): + def clear(self) -> None: """ Erases all held information that is not the qubit pauli operator. """ - self._qps_to_indices = dict() + self._qps_to_indices.clear() for k in self._qubit_pauli_operator._dict.keys(): self._qps_to_indices[k] = list() - self._measurement_circuits: List[MeasurementCircuit] = list() - self._partitions: List[QubitPauliString] = list() + self._measurement_circuits.clear() + self._partitions.clear() def modify_coefficients( self, new_coefficients: List[Tuple[QubitPauliString, float]] diff --git a/qermit/zero_noise_extrapolation/zne.py b/qermit/zero_noise_extrapolation/zne.py index 457570cc..a63044ac 100644 --- a/qermit/zero_noise_extrapolation/zne.py +++ b/qermit/zero_noise_extrapolation/zne.py @@ -21,19 +21,21 @@ ObservableExperiment, TaskGraph, ) -from copy import copy +from pytket.unit_id import UnitID +from copy import copy, deepcopy from pytket.pauli import QubitPauliString # type: ignore from enum import Enum import numpy as np # type: ignore from scipy.optimize import curve_fit # type: ignore -from typing import List, Tuple, cast, Dict -from pytket import Circuit, OpType +from typing import List, Tuple, cast, Dict, Union +from pytket import Circuit, OpType, Qubit from pytket.predicates import CompilationUnit # type: ignore from pytket.utils import QubitPauliOperator import matplotlib.pyplot as plt # type: ignore from numpy.polynomial.polynomial import Polynomial # type: ignore from pytket.circuit import Node # type: ignore from math import isclose +from pytket.pauli import Pauli box_types = { @@ -56,6 +58,7 @@ class Folding(Enum): # TODO: circ does not appear as input in docs # TODO Generalise with 'partial folding' to allow for non integer noise scaling + @staticmethod def circuit(circ: Circuit, noise_scaling: int, **kwargs) -> Circuit: """Noise scaling by circuit folding. In this case the folded circuit is of the form :math:`CC^{-1}CC^{-1}...C` where :math:`C` is the original circuit. As such noise may be scaled by @@ -79,7 +82,7 @@ def circuit(circ: Circuit, noise_scaling: int, **kwargs) -> Circuit: folded_circ = circ.copy() for _ in range(noise_scaling // 2): # Add barrier between circuit and its inverse - folded_circ.add_barrier(folded_circ.qubits + folded_circ.bits) + folded_circ.add_barrier(cast(List[UnitID], folded_circ.qubits + folded_circ.bits)) # Add inverse circuit by iterating though commands and inverting them for gate in reversed(circ.get_commands()): @@ -91,7 +94,7 @@ def circuit(circ: Circuit, noise_scaling: int, **kwargs) -> Circuit: folded_circ.add_gate(gate.op.dagger, gate.args) # Add barrier between circuit and its inverse - folded_circ.add_barrier(folded_circ.qubits + folded_circ.bits) + folded_circ.add_barrier(cast(List[UnitID], folded_circ.qubits + folded_circ.bits)) # Add original circuit for gate in circ.get_commands(): @@ -104,6 +107,7 @@ def circuit(circ: Circuit, noise_scaling: int, **kwargs) -> Circuit: return folded_circ + @staticmethod def two_qubit_gate(circ: Circuit, noise_scaling: float, **kwargs) -> Circuit: """Noise scaling by folding 2 qubit gates. It is implicitly assumed that the noise on the 2 qubit gates dominate. Two qubit gates @@ -209,6 +213,7 @@ def two_qubit_gate(circ: Circuit, noise_scaling: float, **kwargs) -> Circuit: return folded_circuit + @staticmethod def gate(circ: Circuit, noise_scaling: float, **kwargs) -> Circuit: """Noise scaling by gate folding. In this case gates :math:`G` are replaced at random with :math:`GG^{-1}G` until the number of gates is sufficiently scaled. @@ -319,6 +324,7 @@ def gate(circ: Circuit, noise_scaling: float, **kwargs) -> Circuit: return folded_c + @staticmethod def odd_gate(circ: Circuit, noise_scaling: int, **kwargs) -> Circuit: """Noise scaling by gate folding. In this case odd gates :math:`G` are replaced :math:`GG^{-1}G` until the number of gates is sufficiently @@ -425,9 +431,8 @@ class Fit(Enum): """ # TODO Consider adding adaptive exponential extrapolation - def cube_root( - self, x: List[float], y: List[float], _show_fit: bool, *args - ) -> float: + @staticmethod + def cube_root(x: List[float], y: List[float], _show_fit: bool, *args) -> float: """Fit data to a cube root function. This is to say a function of the form :math:`a + b(x+c)^{1/3}`. :param x: Noise scaling values. @@ -455,8 +460,9 @@ def cube_root( return float(fit_to_zero) + @staticmethod def poly_exponential( - self, x: List[float], y: List[float], _show_fit: bool, deg: int + x: List[float], y: List[float], _show_fit: bool, deg: int ) -> float: """Fit data to a poly-exponential, which is to say a function of the form :math:`a+e^{z}`, where :math:`z` is a polynomial. @@ -517,9 +523,8 @@ def poly_exponential( return float(fit_to_zero) - def exponential( - self, x: List[float], y: List[float], _show_fit: bool, *args - ) -> float: + @staticmethod + def exponential(x: List[float], y: List[float], _show_fit: bool, *args) -> float: """Fit data to an exponential function. This is to say a function of the form :math:`a+e^{(b+x)}`. Note that this is a special case of the poly-exponential function. @@ -535,11 +540,10 @@ def exponential( # As the exponential function is a special case of the # poly-exponential function, it is called here - return Fit.poly_exponential(self, x, y, _show_fit, 1) + return Fit.poly_exponential(x, y, _show_fit, 1) - def polynomial( - self, x: List[float], y: List[float], _show_fit: bool, deg: int - ) -> float: + @staticmethod + def polynomial(x: List[float], y: List[float], _show_fit: bool, deg: int) -> float: """Fit data to a polynomial function. :param x: Noise scaling values. @@ -579,7 +583,8 @@ def polynomial( return float(fit_to_zero) - def linear(self, x: List[float], y: List[float], _show_fit: bool, *args) -> float: + @staticmethod + def linear(x: List[float], y: List[float], _show_fit: bool, *args) -> float: """Fit data to a linear function. This is to say a function of the form :math:`ax+b`. Note that this is a special case of the polynomial fitting function. @@ -594,11 +599,10 @@ def linear(self, x: List[float], y: List[float], _show_fit: bool, *args) -> floa """ # As this is a special case of a fit to a polynomial, the polynomial # fitting function is called here with a degree 1 - return Fit.polynomial(self, x, y, _show_fit, 1) + return Fit.polynomial(x, y, _show_fit, 1) - def richardson( - self, x: List[float], y: List[float], _show_fit: bool, *args - ) -> float: + @staticmethod + def richardson(x: List[float], y: List[float], _show_fit: bool, *args) -> float: """Use richardson extrapolation. This amounts to fitting to a polynomial of degree one less than the number of data points. @@ -613,7 +617,7 @@ def richardson( """ # As this is a special case of the polynomial fitting function, the polynomial fitting # function is called here with degree one less than the number of data points. - return Fit.polynomial(self, x, y, _show_fit, len(x) - 1) + return Fit.polynomial(x, y, _show_fit, len(x) - 1) def plot_fit( @@ -770,7 +774,7 @@ def task( QubitPauliOperator( { qpo_k: _fit_type( # type: ignore - obj, all_fold_vals, qpo_list_float[qpo_k], _show_fit, deg + all_fold_vals, qpo_list_float[qpo_k], _show_fit, deg ) for qpo_k in qpo_list_float } @@ -798,9 +802,9 @@ def copy_mitex_wire(wire: ObservableExperiment) -> ObservableExperiment: # Copy ansatz circuit new_ansatz_circuit = AnsatzCircuit( - Circuit=wire.AnsatzCircuit.Circuit.copy(), - Shots=copy(wire.AnsatzCircuit.Shots), - SymbolsDict=copy(wire.AnsatzCircuit.SymbolsDict), + Circuit=deepcopy(wire.AnsatzCircuit.Circuit), + Shots=deepcopy(wire.AnsatzCircuit.Shots), + SymbolsDict=deepcopy(wire.AnsatzCircuit.SymbolsDict), ) # copy qps and instantiate new measurement setup @@ -860,7 +864,10 @@ def task( ) -def qpo_node_relabel(qpo: QubitPauliOperator, node_map: Dict[Node, Node]): +def qpo_node_relabel( + qpo: QubitPauliOperator, + node_map: Dict[Union[UnitID, Qubit, Node], Union[UnitID, Qubit, Node]] +): """Relabel the nodes of qpo according to node_map :param qpo: Original qubit pauli operator @@ -878,7 +885,7 @@ def qpo_node_relabel(qpo: QubitPauliOperator, node_map: Dict[Node, Node]): new_qps_dict = {} for q in orig_qps_dict: new_qps_dict[node_map[q]] = orig_qps_dict[q] - new_qps = QubitPauliString(new_qps_dict) + new_qps = QubitPauliString(cast(Dict[Qubit, Pauli], new_qps_dict)) new_qpo_dict[new_qps] = orig_qpo_dict[orig_qps] return QubitPauliOperator(new_qpo_dict) @@ -899,7 +906,7 @@ def gen_initial_compilation_task( def task( obj, wire: List[ObservableExperiment] - ) -> Tuple[List[ObservableExperiment], List[Dict[Node, Node]]]: + ) -> Tuple[List[ObservableExperiment], List[Dict[UnitID, UnitID]]]: """Performs initial compilation before folding. This is to ensure minimal compilation after folding, as this could disrupt by how much the noise is increased. @@ -964,7 +971,9 @@ def gen_qubit_relabel_task() -> MitTask: """ def task( - obj, qpo_list: List[QubitPauliOperator], compilation_map_list: List[Dict[Node, Node]] + obj, + qpo_list: List[QubitPauliOperator], + compilation_map_list: List[Dict[Node, Node]], ) -> Tuple[List[QubitPauliOperator]]: """Use node map returned by compilation unit to undo the relabelling performed by gen_initial_compilation_task @@ -981,9 +990,8 @@ def task( new_qpo_list = [] for compilation_map, qpo in zip(compilation_map_list, qpo_list): - node_map = {value: key for key, value in compilation_map.items()} - new_qpo_list.append(qpo_node_relabel(qpo, node_map)) + new_qpo_list.append(qpo_node_relabel(qpo, cast(Dict[Union[UnitID, Qubit, Node], Union[UnitID, Qubit, Node]], node_map))) return (new_qpo_list,) @@ -1008,14 +1016,14 @@ def gen_ZNE_MitEx(backend: Backend, noise_scaling_list: List[float], **kwargs) - :return: MitEx object performing noise mitigation by ZNE. :rtype: MitEx """ - _experiment_mitres = copy( + _experiment_mitres = deepcopy( kwargs.get( "experiment_mitres", MitRes(backend), ) ) - _experiment_mitex = copy( + _experiment_mitex = deepcopy( kwargs.get( "experiment_mitex", MitEx(backend, _label="ExperimentMitex", mitres=_experiment_mitres), @@ -1035,18 +1043,7 @@ def gen_ZNE_MitEx(backend: Backend, noise_scaling_list: List[float], **kwargs) - for fold in noise_scaling_list: _label = str(fold) + "FoldMitEx" - _fold_mitres = copy( - kwargs.get( - "experiment_mitres", - MitRes(backend), - ) - ) - _fold_mitex = copy( - kwargs.get( - "experiment_mitex", - MitEx(backend, _label=_label, mitres=_fold_mitres), - ) - ) + _fold_mitex = deepcopy(_experiment_mitex) _fold_mitex._label = _label + _fold_mitex._label digital_folding_task = digital_folding_task_gen( backend, fold, _folding_type, _allow_approx_fold diff --git a/tests/leakage_gadget_test.py b/tests/leakage_gadget_test.py new file mode 100644 index 00000000..ed80c57a --- /dev/null +++ b/tests/leakage_gadget_test.py @@ -0,0 +1,78 @@ +# Copyright 2019-2023 Quantinuum +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from qermit.mock_backend import MockQuantinuumBackend +from pytket import Circuit +from qermit.taskgraph import gen_compiled_MitRes +from qermit import CircuitShots +from qermit.leakage_detection import get_leakage_detection_mitres +from qermit.leakage_detection.leakage_detection import gen_add_leakage_gadget_circuit_task +from pytket.extensions.quantinuum.backends.leakage_gadget import prune_shots_detected_as_leaky +from qermit.postselection.postselect_mitres import gen_postselect_task + + +def test_leakage_gadget() -> None: + + backend = MockQuantinuumBackend() + circuit = Circuit(2).H(0).measure_all() + compiled_mitres = gen_compiled_MitRes( + backend=backend, + optimisation_level=0, + ) + leakage_gadget_mitres = get_leakage_detection_mitres( + backend=backend, + mitres=compiled_mitres + ) + n_shots = 50 + result_list = leakage_gadget_mitres.run( + [CircuitShots(Circuit=circuit, Shots=n_shots)] + ) + counts = result_list[0].get_counts() + assert all(shot in list(counts.keys()) for shot in [(0, 0), (1, 0)]) + assert sum(val for val in counts.values()) <= n_shots + + +def test_compare_with_prune() -> None: + # A test to check for updates to prune that we should know about. + + circuit_0 = Circuit(2).measure_all() + circuit_1 = Circuit(2).Rz(0.3, 0).measure_all() + circuit_shot_0 = CircuitShots(Circuit=circuit_0, Shots=10) + circuit_shot_1 = CircuitShots(Circuit=circuit_1, Shots=20) + + backend = MockQuantinuumBackend() + + generation_task = gen_add_leakage_gadget_circuit_task(backend=backend) + postselection_task = gen_postselect_task() + + detection_circuit_shots_list, postselect_mgr_list = generation_task( + ([circuit_shot_0, circuit_shot_1], ) + ) + result_list = [ + backend.run_circuit( + circuit=backend.get_compiled_circuit( + circuit=detection_circuit_shots.Circuit, + optimisation_level=0 + ), + n_shots=detection_circuit_shots.Shots, + ) for detection_circuit_shots in detection_circuit_shots_list + ] + + qermit_result_list = postselection_task( + (result_list, postselect_mgr_list, ) + ) + pytket_result_list = [ + prune_shots_detected_as_leaky(result) for result in result_list + ] + assert qermit_result_list[0] == pytket_result_list diff --git a/tests/mitex_test.py b/tests/mitex_test.py index f3f5c634..53375de3 100644 --- a/tests/mitex_test.py +++ b/tests/mitex_test.py @@ -19,6 +19,7 @@ ObservableTracker, CircuitShots, AnsatzCircuit, + ObservableExperiment, ) from qermit.taskgraph.mitex import ( # type: ignore filter_observable_tracker_task_gen, @@ -34,6 +35,42 @@ from pytket.extensions.qiskit import AerBackend # type: ignore +def test_mitex_cache(): + + circuit = Circuit(1).X(0) + backend = AerBackend() + mitex = MitEx(backend=backend) + mitex.decompose_TaskGraph_nodes() + + ansatz = AnsatzCircuit( + Circuit=circuit, + Shots=100000, + SymbolsDict=SymbolsDict() + ) + qubits = circuit.qubits + qps = QubitPauliString( + qubits=qubits, + paulis=[Pauli.Z for _ in qubits], + ) + qpo = QubitPauliOperator({qps: 1}) + obs = ObservableTracker(qubit_pauli_operator=qpo) + obs_exp = ObservableExperiment(AnsatzCircuit=ansatz, ObservableTracker=obs) + + mitex.run( + mitex_wires=[obs_exp], cache=True + ) + cache = mitex.get_cache() + assert list(cache.keys()) == [ + 'FilterObservableTracker', + 'CollateExperimentCircuits', + 'MitResCompileCircuitShots', + 'MitResCircuitsToHandles', + 'MitResHandlesToResults', + 'SplitResults', + 'GenerateExpectations', + ] + + def gen_test_wire_objs(): sym_0 = fresh_symbol("alpha") sym_1 = fresh_symbol("beta") diff --git a/tests/pec_test.py b/tests/pec_test.py index 4ceae18a..f8a58d2b 100644 --- a/tests/pec_test.py +++ b/tests/pec_test.py @@ -51,7 +51,7 @@ def test_no_qubit_relabel(): noiseless_backend = AerBackend() lagos_backend = IBMQEmulatorBackend( - "ibm_lagos", hub="partner-cqc", group="internal", project="default" + "ibm_lagos", instance='partner-cqc/internal/default' ) pec_mitex = gen_PEC_learning_based_MitEx( device_backend=lagos_backend, simulator_backend=noiseless_backend diff --git a/tests/postselection_test.py b/tests/postselection_test.py new file mode 100644 index 00000000..49f70684 --- /dev/null +++ b/tests/postselection_test.py @@ -0,0 +1,58 @@ +from qermit.postselection import PostselectMgr, gen_postselect_mitres +from pytket.circuit import Bit +from collections import Counter +from pytket.backends.backendresult import BackendResult +from pytket.utils.outcomearray import OutcomeArray +from pytket.extensions.qiskit import AerBackend +from qermit import CircuitShots +from pytket import Circuit + + +def test_postselect_manager() -> None: + + compute_cbits = [Bit(name='A', index=0), Bit(name='C', index=0)] + postselect_cbits = [Bit(name='B', index=0), Bit(name='A', index=1)] + + count_mgr = PostselectMgr( + compute_cbits=compute_cbits, + postselect_cbits=postselect_cbits, + ) + + counts = { + (0, 0, 0, 0): 100, + (0, 1, 0, 0): 100, + (0, 0, 0, 1): 100, + (0, 1, 0, 1): 100, + (1, 0, 0, 0): 100, + (1, 1, 0, 0): 100, + } + + result = BackendResult( + counts=Counter( + { + OutcomeArray.from_readouts([key]): val + for key, val in counts.items() + } + ), + c_bits=[Bit(name='A', index=0), Bit(name='A', index=1), Bit(name='B', index=0), Bit(name='C', index=0)], + ) + + assert count_mgr.postselect_result(result=result).get_counts() == Counter({(0, 0): 100, (0, 1): 100, (1, 0): 100}) + assert count_mgr.merge_result(result=result).get_counts() == Counter({(0, 0): 200, (0, 1): 200, (1, 0): 200}) + + +def test_postselect_mitres() -> None: + + backend = AerBackend() + circuit = Circuit(2).H(0).measure_all() + cbits = circuit.bits + postselect_mgr = PostselectMgr( + compute_cbits=[cbits[1]], + postselect_cbits=[cbits[0]], + ) + postselect_mitres = gen_postselect_mitres( + backend=backend, + postselect_mgr=postselect_mgr + ) + result_list = postselect_mitres.run([CircuitShots(Circuit=circuit, Shots=50)]) + assert list(result_list[0].get_counts().keys()) == [(0,)] diff --git a/tests/spectral_filtering_test.py b/tests/spectral_filtering_test.py index 3f8d9ed3..64cc27cd 100644 --- a/tests/spectral_filtering_test.py +++ b/tests/spectral_filtering_test.py @@ -1,3 +1,17 @@ +# Copyright 2019-2023 Quantinuum +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from qermit.spectral_filtering.spectral_filtering import ( gen_result_extraction_task, gen_wire_copy_task, diff --git a/tests/test_requirements.txt b/tests/test_requirements.txt index f9d93b6b..1b3dcea9 100644 --- a/tests/test_requirements.txt +++ b/tests/test_requirements.txt @@ -1,4 +1,5 @@ -pytket-qiskit~=0.23 +pytket-qiskit +qiskit-ibmq-provider pytest pytest-cov flake8 diff --git a/tests/zne_test.py b/tests/zne_test.py index 18eeafa0..7ab931af 100644 --- a/tests/zne_test.py +++ b/tests/zne_test.py @@ -44,6 +44,7 @@ import pytest from pytket.circuit import Node # type: ignore from qermit.mock_backend import MockQuantinuumBackend # type: ignore +from qermit.taskgraph import gen_MeasurementReduction_MitEx n_qubits = 2 @@ -69,12 +70,112 @@ REASON = "IBMQ account not configured" +def get_string_operator(meas_qubits, string): + # A small utility method for generating observables with expectation + # corresponding to bit string probabilities. + + identity_pauli_list = [Pauli.I for _ in meas_qubits] + + identity_qps = QubitPauliString( + qubits=meas_qubits, + paulis=identity_pauli_list, + ) + qpo = QubitPauliOperator({identity_qps: 1}) + + for i in range(len(meas_qubits)): + temp_pauli_list = identity_pauli_list.copy() + temp_pauli_list[i] = Pauli.Z + temp_qps = QubitPauliString( + qubits=meas_qubits, + paulis=temp_pauli_list, + ) + if string[i] == 0: + qpo *= QubitPauliOperator({temp_qps: 0.5, identity_qps: 0.5}) + elif string[i] == 1: + qpo *= QubitPauliOperator({temp_qps: -0.5, identity_qps: 0.5}) + else: + raise Exception(f"{string} is not a binary string.") + + return qpo + + +@pytest.mark.high_compute +def test_measurement_reduction_integration(): + + tol = 0.01 + + # A bell state + circuit = Circuit() + qubit_0 = Qubit(name='my_qubit', index=0) + qubit_1 = Qubit(name='my_qubit', index=1) + circuit.add_qubit(qubit_0) + circuit.add_qubit(qubit_1) + circuit.H(qubit_0).CX(qubit_0, qubit_1) + meas_qubits = [qubit_0, qubit_1] + + backend = AerBackend() + reduction_mitex = gen_MeasurementReduction_MitEx(backend=backend) + + ansatz = AnsatzCircuit( + Circuit=circuit, + Shots=1000000, + SymbolsDict=SymbolsDict() + ) + + qpo = get_string_operator(meas_qubits, [0, 0]) + obs = ObservableTracker(qubit_pauli_operator=qpo) + obs_exp = ObservableExperiment(AnsatzCircuit=ansatz, ObservableTracker=obs) + result = reduction_mitex.run( + mitex_wires=[obs_exp] + ) + # The probability of measuring 00 is 0.5 + assert abs(sum(result[0]._dict.values()) - 0.5) < tol + + qpo = get_string_operator(meas_qubits, [0, 1]) + obs = ObservableTracker(qubit_pauli_operator=qpo) + obs_exp = ObservableExperiment(AnsatzCircuit=ansatz, ObservableTracker=obs) + result = reduction_mitex.run( + mitex_wires=[obs_exp] + ) + # The probability of measuring 01 is 0 + assert abs(sum(result[0]._dict.values())) < tol + + folding_type = Folding.two_qubit_gate + fit_type = Fit.linear + noise_scaling_list = [1.5, 2, 2.5, 3, 3.5] + zne_mitex = gen_ZNE_MitEx( + backend=backend, + experiment_mitex=reduction_mitex, + noise_scaling_list=noise_scaling_list, + folding_type=folding_type, + fit_type=fit_type, + ) + + qpo = get_string_operator(meas_qubits, [1, 0]) + obs = ObservableTracker(qubit_pauli_operator=qpo) + obs_exp = ObservableExperiment(AnsatzCircuit=ansatz, ObservableTracker=obs) + result = zne_mitex.run( + mitex_wires=[obs_exp] + ) + # The probability of measuring 10 is 0 + assert abs(sum(result[0]._dict.values())) < tol + + qpo = get_string_operator(meas_qubits, [1, 1]) + obs = ObservableTracker(qubit_pauli_operator=qpo) + obs_exp = ObservableExperiment(AnsatzCircuit=ansatz, ObservableTracker=obs) + result = zne_mitex.run( + mitex_wires=[obs_exp] + ) + # The probability of measuring 11 is 0.5 + assert abs(sum(result[0]._dict.values()) - 0.5) < tol + + @pytest.mark.skipif(skip_remote_tests, reason=REASON) @pytest.mark.high_compute def test_no_qubit_relabel(): lagos_backend = IBMQEmulatorBackend( - "ibm_lagos", hub="partner-cqc", group="internal", project="default" + "ibm_lagos", instance='partner-cqc/internal/default' ) zne_mitex = gen_ZNE_MitEx(backend=lagos_backend, noise_scaling_list=[3, 5, 7])