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])