From 0d25d6c2050da7168f5d75a51ed0f79e52f4d0ca Mon Sep 17 00:00:00 2001 From: Zhiyao Li <40032538+zhiyaol@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:11:12 -0700 Subject: [PATCH 01/26] chore: enable mypy `warn_unused_ignores` and address associated errors (#79) --- deltakit-circuit/src/deltakit_circuit/_circuit.py | 6 +++--- deltakit-circuit/src/deltakit_circuit/_gate_layer.py | 2 +- .../src/deltakit_circuit/_qubit_identifiers.py | 2 +- .../src/deltakit_circuit/gates/_abstract_gates.py | 4 ++-- .../src/deltakit_circuit/gates/_two_qubit_gates.py | 8 ++++---- .../decoding_graphs/_decoding_graph_tools.py | 2 +- .../src/deltakit_core/decoding_graphs/_dem_parsing.py | 2 +- .../src/deltakit_core/decoding_graphs/_syndromes.py | 4 ++-- .../noise_sources/_generic_noise_sources.py | 2 +- .../noise_sources/_matching_noise_sources.py | 6 +++--- .../test_empirical_decoding_error_distribution.py | 2 +- .../src/deltakit_explorer/_api/_gql_client.py | 2 +- mypy.ini | 4 +--- 13 files changed, 22 insertions(+), 24 deletions(-) diff --git a/deltakit-circuit/src/deltakit_circuit/_circuit.py b/deltakit-circuit/src/deltakit_circuit/_circuit.py index e9853101..f860e70a 100644 --- a/deltakit-circuit/src/deltakit_circuit/_circuit.py +++ b/deltakit-circuit/src/deltakit_circuit/_circuit.py @@ -207,7 +207,7 @@ def transform_qubits(self, id_mapping: Mapping[T, U]): if isinstance(layer, (GateLayer, NoiseLayer, Circuit)): layer.transform_qubits(id_mapping) elif isinstance(layer, Detector): - layer.transform_coordinates(id_mapping) # type: ignore[arg-type] + layer.transform_coordinates(id_mapping) def append_layers(self, layers: Layer | Iterable[Layer]): """Append any layers to the end of this circuit. If a layer consisting @@ -426,13 +426,13 @@ def remove_noise(self, recursive: bool = True): if isinstance(gate, OneQubitMeasurementGate): layer.replace_gates( { - gate: lambda gate: type(gate)(gate.qubit) # type: ignore + gate: lambda gate: type(gate)(gate.qubit) } ) elif isinstance(gate, MPP): layer.replace_gates( { - gate: lambda gate: type(gate)(gate.pauli_product) # type: ignore + gate: lambda gate: type(gate)(gate.pauli_product) } ) elif isinstance(layer, Circuit) and recursive: diff --git a/deltakit-circuit/src/deltakit_circuit/_gate_layer.py b/deltakit-circuit/src/deltakit_circuit/_gate_layer.py index 23a1d780..7495eff2 100644 --- a/deltakit-circuit/src/deltakit_circuit/_gate_layer.py +++ b/deltakit-circuit/src/deltakit_circuit/_gate_layer.py @@ -115,7 +115,7 @@ def transform_qubits(self, id_mapping: Mapping[T, U]): for gate in chain(self._non_measurement_gates, self._measurement_gates): # Is maybe dangerous because the mutation happens regardless of # whether the error is raised or not - gate.transform_qubits(id_mapping) # type: ignore[arg-type] + gate.transform_qubits(id_mapping) if intersection := new_qubits.intersection(gate.qubits): raise DuplicateQubitError(gate, intersection) new_qubits.update(gate.qubits) diff --git a/deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py b/deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py index f7ef147d..3458dc3b 100644 --- a/deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py +++ b/deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py @@ -184,7 +184,7 @@ class Coordinate(tuple): """ def __new__(cls, *coordinates: float): - return super().__new__(cls, coordinates) # type: ignore[arg-type] + return super().__new__(cls, coordinates) def __eq__(self, other: object) -> bool: return isinstance(other, Coordinate) and super().__eq__(other) diff --git a/deltakit-circuit/src/deltakit_circuit/gates/_abstract_gates.py b/deltakit-circuit/src/deltakit_circuit/gates/_abstract_gates.py index da7ad747..c1772d43 100644 --- a/deltakit-circuit/src/deltakit_circuit/gates/_abstract_gates.py +++ b/deltakit-circuit/src/deltakit_circuit/gates/_abstract_gates.py @@ -381,9 +381,9 @@ def transform_qubits( # type: ignore[override] self, id_mapping: Mapping[T, U] ): if (new_id1 := id_mapping.get(self._operand1.unique_identifier)) is not None: - self._operand1 = Qubit(new_id1) # type: ignore[arg-type] + self._operand1 = Qubit(new_id1) if (new_id2 := id_mapping.get(self._operand2.unique_identifier)) is not None: - self._operand2 = Qubit(new_id2) # type: ignore[arg-type] + self._operand2 = Qubit(new_id2) def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and set(self.qubits) == set( diff --git a/deltakit-circuit/src/deltakit_circuit/gates/_two_qubit_gates.py b/deltakit-circuit/src/deltakit_circuit/gates/_two_qubit_gates.py index a05dbe96..07f17dbb 100644 --- a/deltakit-circuit/src/deltakit_circuit/gates/_two_qubit_gates.py +++ b/deltakit-circuit/src/deltakit_circuit/gates/_two_qubit_gates.py @@ -74,7 +74,7 @@ class CY(ControlledGate[Union[Qubit[T], SweepBit, MeasurementRecord], Qubit[T]]) stim_string: ClassVar[str] = "CY" -class CZ( # type: ignore[misc] +class CZ( ControlledGate[Union[Qubit[T], SweepBit, MeasurementRecord], Qubit[T]], SymmetricTwoQubitGate, ): @@ -346,7 +346,7 @@ class SQRT_ZZ_DAG(SymmetricTwoQubitGate[T]): stim_string: ClassVar[str] = "SQRT_ZZ_DAG" -class XCX( # type: ignore[misc] +class XCX( ControlledGate[Qubit[T], Qubit[T]], SymmetricTwoQubitGate[Qubit[T]] ): """The X-controlled X gate. First qubit is the control, second qubit is @@ -476,7 +476,7 @@ class YCX(ControlledGate[Qubit[T], Qubit[T]]): stim_string: ClassVar[str] = "YCX" -class YCY( # type: ignore[misc] +class YCY( ControlledGate[Qubit[T], Qubit[T]], SymmetricTwoQubitGate[Qubit[T]] ): """The Y-controlled Y gate. First qubit is the control, second qubit is @@ -575,7 +575,7 @@ class CXSWAP(ControlledGate[Qubit[T], Qubit[T]]): stim_string: ClassVar[str] = "CXSWAP" -class CZSWAP( # type: ignore[misc] +class CZSWAP( SymmetricTwoQubitGate, ControlledGate[Qubit[T], Qubit[T]] ): """A combination CZ-and-SWAP gate. This gate is kak-equivalent diff --git a/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph_tools.py b/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph_tools.py index e23406ab..fa4c893c 100644 --- a/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph_tools.py +++ b/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph_tools.py @@ -138,7 +138,7 @@ def graph_to_json( If full flag is True, we additionally serialize: - - detector_records: dict contiaining detector metadata + - detector_records: dict containing detector metadata - edge_record: dict containing edge metadata Parameters diff --git a/deltakit-core/src/deltakit_core/decoding_graphs/_dem_parsing.py b/deltakit-core/src/deltakit_core/decoding_graphs/_dem_parsing.py index ea271e67..59408320 100644 --- a/deltakit-core/src/deltakit_core/decoding_graphs/_dem_parsing.py +++ b/deltakit-core/src/deltakit_core/decoding_graphs/_dem_parsing.py @@ -39,7 +39,7 @@ class CoordinateOffset(tuple): # noqa: PLW1641 """Class to track the coordinate offset in a Detector Error Model.""" def __new__(cls, offset: Iterable[float | int] = ()): - return super().__new__(cls, offset) # type: ignore[arg-type] + return super().__new__(cls, offset) def __add__(self, other: object) -> CoordinateOffset: if isinstance(other, collections.abc.Iterable): diff --git a/deltakit-core/src/deltakit_core/decoding_graphs/_syndromes.py b/deltakit-core/src/deltakit_core/decoding_graphs/_syndromes.py index a499f156..0f4783e2 100644 --- a/deltakit-core/src/deltakit_core/decoding_graphs/_syndromes.py +++ b/deltakit-core/src/deltakit_core/decoding_graphs/_syndromes.py @@ -440,7 +440,7 @@ def __getitem__(self, index: slice) -> Bitstring: ... def __getitem__(self, index): if isinstance(index, int): - return (self._bits >> index) & 1 # type: ignore[return-value] + return (self._bits >> index) & 1 if isinstance(index, slice): start, stop, _ = index.indices(len(self)) mask = (1 << (stop - start)) - 1 @@ -564,7 +564,7 @@ def __len__(self) -> int: def __getitem__(self, index): if isinstance(index, int): - return (self._bits >> index) & 1 # type: ignore[return-value] + return (self._bits >> index) & 1 if isinstance(index, slice): start, stop, _ = index.indices(len(self)) mask = (1 << (stop - start)) - 1 diff --git a/deltakit-decode/src/deltakit_decode/noise_sources/_generic_noise_sources.py b/deltakit-decode/src/deltakit_decode/noise_sources/_generic_noise_sources.py index 620f4180..b0725e0a 100644 --- a/deltakit-decode/src/deltakit_decode/noise_sources/_generic_noise_sources.py +++ b/deltakit-decode/src/deltakit_decode/noise_sources/_generic_noise_sources.py @@ -314,7 +314,7 @@ def _get_gen( sizes[index_smallest_bucket] += size gens[index_smallest_bucket] = chain(gens[index_smallest_bucket], gen) - return tuple(zip(gens, sizes)) # type: ignore + return tuple(zip(gens, sizes)) @staticmethod def _lazy_product( diff --git a/deltakit-decode/src/deltakit_decode/noise_sources/_matching_noise_sources.py b/deltakit-decode/src/deltakit_decode/noise_sources/_matching_noise_sources.py index b80a4553..69cd01cd 100644 --- a/deltakit-decode/src/deltakit_decode/noise_sources/_matching_noise_sources.py +++ b/deltakit-decode/src/deltakit_decode/noise_sources/_matching_noise_sources.py @@ -339,7 +339,7 @@ def importance_sampling_decomposition( # The lambda needs to be annotated with a default argument so that the # argument is captured within the for loop. edge_p_err_decompositions = ( - UniformMatchingNoise(p_err, lambda _, e=edges: e) # type: ignore + UniformMatchingNoise(p_err, lambda _, e=edges: e) .importance_sampling_decomposition(code_data, inner_coefficient_limit) for (p_err, edges), inner_coefficient_limit in zip( p_err_edges.items(), inner_model_coefficient_limits) @@ -492,7 +492,7 @@ def error_generator( partial(model.error_generator, code_data, seed_) for model, seed_ in zip(self.internal_sources, offset_seed(seed)) ) - for error in CombinedSequences._lazy_product(*error_generators): # type: ignore[arg-type] + for error in CombinedSequences._lazy_product(*error_generators): yield OrderedDecodingEdges(chain.from_iterable(error)) def sequence_size(self, code_data: Any) -> int: @@ -593,7 +593,7 @@ def split_error_generator( min_bucket_i = sizes.index(min(sizes)) sizes[min_bucket_i] += size iterators[min_bucket_i] = chain(iterators[min_bucket_i], iterator) - return tuple(zip(iterators, sizes)) # type: ignore + return tuple(zip(iterators, sizes)) def field_values(self) -> Dict[str, Any]: base_dict = super().field_values() diff --git a/deltakit-decode/tests/analysis/test_empirical_decoding_error_distribution.py b/deltakit-decode/tests/analysis/test_empirical_decoding_error_distribution.py index 7980691d..ee04c7f5 100644 --- a/deltakit-decode/tests/analysis/test_empirical_decoding_error_distribution.py +++ b/deltakit-decode/tests/analysis/test_empirical_decoding_error_distribution.py @@ -179,7 +179,7 @@ def test_addition_is_commutative(self, num_logicals: int): self._distribution_data_is_equal(distr1 + distr2,distr2 + distr1) @pytest.mark.parametrize("num_logicals", [5, 1, 13]) - def test_addition_is_assoicative(self, num_logicals: int): + def test_addition_is_associative(self, num_logicals: int): distr1 = EmpiricalDecodingErrorDistribution(num_logicals) distr2 = EmpiricalDecodingErrorDistribution(num_logicals) distr3 = EmpiricalDecodingErrorDistribution(num_logicals) diff --git a/deltakit-explorer/src/deltakit_explorer/_api/_gql_client.py b/deltakit-explorer/src/deltakit_explorer/_api/_gql_client.py index 25e8cb42..fa7fe364 100644 --- a/deltakit-explorer/src/deltakit_explorer/_api/_gql_client.py +++ b/deltakit-explorer/src/deltakit_explorer/_api/_gql_client.py @@ -49,7 +49,7 @@ def __init__( """ The constructor accepts and endpoint and a timeout. `base_url` is a URL of the service. - - `/api/graphql/` accepts GraphQL POST-qeries, + - `/api/graphql/` accepts GraphQL POST-queries, - `/api/data/` allows GET-requests of content. Args: diff --git a/mypy.ini b/mypy.ini index 798aa622..21c8f137 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,6 +5,7 @@ allow_untyped_globals = false allow_redefinition = false implicit_optional = false warn_no_return = true +warn_unused_ignores = true ; strict option settings that we fail @@ -31,9 +32,6 @@ exclude = deltakit-circuit/tests|docs/conf.py|build|deltakit-explorer/tests|tool ; 27 errors ;warn_return_any = true -; 20 errors -;warn_unused_ignores = true - ; 159 errors ; implicit_reexport = false From a47e99fc703f59e47147b43382fa7b0bc69aa414 Mon Sep 17 00:00:00 2001 From: Zhiyao Li <40032538+zhiyaol@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:37:50 -0700 Subject: [PATCH 02/26] chore: fix some `warn_return_any` rule violations (#80) --- deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py | 2 +- .../deltakit_core/decoding_graphs/_explained_dem_parsing.py | 4 ++-- .../src/deltakit_explorer/codes/_css/_css_code.py | 2 +- deltakit-explorer/src/deltakit_explorer/types/_data_string.py | 2 +- mypy.ini | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py b/deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py index 3458dc3b..90e270a3 100644 --- a/deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py +++ b/deltakit-circuit/src/deltakit_circuit/_qubit_identifiers.py @@ -174,7 +174,7 @@ def __repr__(self) -> str: return f"SweepBit({self._bit_index})" -class Coordinate(tuple): +class Coordinate(Tuple[float, ...]): """Class which represents general coordinates. Parameters diff --git a/deltakit-core/src/deltakit_core/decoding_graphs/_explained_dem_parsing.py b/deltakit-core/src/deltakit_core/decoding_graphs/_explained_dem_parsing.py index 78335ac9..c039477a 100644 --- a/deltakit-core/src/deltakit_core/decoding_graphs/_explained_dem_parsing.py +++ b/deltakit-core/src/deltakit_core/decoding_graphs/_explained_dem_parsing.py @@ -41,7 +41,7 @@ def depolarising_as_independent(probability: float, num_qubits: int) -> float: ) p_with_i = probability / mixing_probability exponent = 1 / 2 ** (2 * num_qubits - 1) - return (1 - ((1 - p_with_i) ** exponent)) / 2 + return float((1 - (1 - p_with_i) ** exponent) / 2) def noise_probability(noise_channel: stim.CircuitTargetsInsideInstruction) -> float: @@ -69,7 +69,7 @@ def noise_probability(noise_channel: stim.CircuitTargetsInsideInstruction) -> fl if noise_channel.gate == "DEPOLARIZE2": return depolarising_as_independent(noise_channel.args[0], 2) if noise_channel.gate in ("X_ERROR", "Y_ERROR", "Z_ERROR"): - return noise_channel.args[0] + return float(noise_channel.args[0]) raise TypeError(f"Unsupported gate type: {noise_channel.gate}") diff --git a/deltakit-explorer/src/deltakit_explorer/codes/_css/_css_code.py b/deltakit-explorer/src/deltakit_explorer/codes/_css/_css_code.py index 115adbe8..e8aec25f 100644 --- a/deltakit-explorer/src/deltakit_explorer/codes/_css/_css_code.py +++ b/deltakit-explorer/src/deltakit_explorer/codes/_css/_css_code.py @@ -868,7 +868,7 @@ def calculate_number_of_logical_qubits(self) -> int: """ hx_mat, hz_mat = self.parity_check_matrices - return len(self.data_qubits) - ( + return len(self.data_qubits) - int( np.linalg.matrix_rank(galois.GF2(hx_mat)) + np.linalg.matrix_rank(galois.GF2(hz_mat)) ) diff --git a/deltakit-explorer/src/deltakit_explorer/types/_data_string.py b/deltakit-explorer/src/deltakit_explorer/types/_data_string.py index 4eaa0565..5226de8f 100644 --- a/deltakit-explorer/src/deltakit_explorer/types/_data_string.py +++ b/deltakit-explorer/src/deltakit_explorer/types/_data_string.py @@ -125,7 +125,7 @@ def from_data_string(cls, data_string: str) -> DataString: return DataString(cls._hex_string_to_bytes(data_string[7:])) @property - def data(self): + def data(self) -> bytes: """Read-only accessor to the data.""" return self._data diff --git a/mypy.ini b/mypy.ini index 21c8f137..15899f71 100644 --- a/mypy.ini +++ b/mypy.ini @@ -29,8 +29,8 @@ exclude = deltakit-circuit/tests|docs/conf.py|build|deltakit-explorer/tests|tool ; 112 errors ;disallow_any_unimported = true -; 27 errors -;warn_return_any = true +; 31 errors +; warn_return_any = true ; 159 errors ; implicit_reexport = false From eb540e0ecbf67cc33ac320c9ea570ec8e8f62234 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Thu, 2 Oct 2025 22:54:26 -0700 Subject: [PATCH 03/26] chore: ignore `pip` vulnerability reported by `pip-audit` (#84) --------- Co-authored-by: Guen Prawiroatmodjo <4041805+guenp@users.noreply.github.com> --- .github/workflows/static_code_analysis.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static_code_analysis.yml b/.github/workflows/static_code_analysis.yml index 7efd1d7f..049b7715 100644 --- a/.github/workflows/static_code_analysis.yml +++ b/.github/workflows/static_code_analysis.yml @@ -58,5 +58,5 @@ jobs: if: ${{ always() }} - name: pip_audit - run: pixi run pip_audit + run: pixi run pip_audit --ignore-vuln GHSA-4xh5-x5gv-qwph if: ${{ always() }} diff --git a/pyproject.toml b/pyproject.toml index 0d7399f3..ba62dc88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,7 @@ dev = [ "python-semantic-release>=10.2.0", "tomlkit>=0.13,<1", "deptry>=0.23, <1", - "ipykernel >=6.29.5,<7" + "ipykernel >=6.29.5,<7", ] # for mypy configuration, see `mypy.ini`. From 10809532c79a4f02ab8d9d157181fce0b3cb3acf Mon Sep 17 00:00:00 2001 From: Guen Prawiroatmodjo <4041805+guenp@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:19:33 -0700 Subject: [PATCH 04/26] chore: check if token exists before running docs build (#85) --- .github/workflows/docs.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7508542d..41e3b514 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,7 +8,21 @@ on: types: [opened, synchronize, reopened] jobs: + check-deltakit-token: + name: Check if Deltakit token exists + runs-on: ubuntu-latest + outputs: + exists: ${{ steps.deltakit-token.outputs.exists }} + steps: + - id: deltakit-token + env: + DELTAKIT_TOKEN: ${{ secrets.DELTAKIT_TOKEN }} + if: ${{ env.DELTAKIT_TOKEN != '' }} + run: echo "exists=true" >> $GITHUB_OUTPUT + build-docs: + needs: [check-deltakit-token] + if: needs.check-deltakit-token.outputs.exists == 'true' runs-on: ubuntu-latest steps: - name: Checkout repo From a78cc49f4000c2fc3ca44bf54bfcdadda39a8c27 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Thu, 2 Oct 2025 23:33:52 -0700 Subject: [PATCH 05/26] chore: simplify .readthedocs.yaml; add `-W` (fail on warnings) to custom build command (#86) --------- Co-authored-by: Guen Prawiroatmodjo <4041805+guenp@users.noreply.github.com> --- .readthedocs.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8244e7d5..107bffa0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,10 +11,4 @@ build: python: "3.13" commands: - curl -sSf https://pixi.sh/install.sh | sh - - /home/docs/.pixi/bin/pixi run sphinx-build -T -b html -d _build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html - -# Build documentation in the "docs/" directory with Sphinx -sphinx: - builder: html - configuration: docs/conf.py - fail_on_warning: true + - /home/docs/.pixi/bin/pixi run sphinx-build -T -W -b html -d _build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html From 56c480a7911581bb852efb0ce354e04a260d8d30 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Fri, 3 Oct 2025 00:46:48 -0700 Subject: [PATCH 06/26] chore: run `ruff format` with pre-commit (#69) * chore: configure ruff format to format appropriate folders * chore: run ruff format in pre-commit * chore: run ruff format * ci: run ruff format --check in CI * Change loop parameter naming for clearer format * MAINT: updates to fix typos, end-of-line whitespace * ruff format --- .github/workflows/static_code_analysis.yml | 6 +- .pre-commit-config.yaml | 4 +- .../src/deltakit_circuit/_circuit.py | 10 +- .../gates/_two_qubit_gates.py | 12 +- docs/conf.py | 31 ++- docs/guide/decoding.md | 16 +- docs/guide/simulation.md | 4 +- ruff.toml | 13 +- tests/qmem/test_qmem.py | 60 +++-- .../test_decoding_graph_tools_with_stim.py | 232 +++++++++++------- tools/extract_version_notes.py | 1 + tools/propagate_version.py | 7 +- tools/set_pre_version.py | 11 +- tools/transform_labels.py | 16 +- 14 files changed, 254 insertions(+), 169 deletions(-) diff --git a/.github/workflows/static_code_analysis.yml b/.github/workflows/static_code_analysis.yml index 049b7715..1d5c6833 100644 --- a/.github/workflows/static_code_analysis.yml +++ b/.github/workflows/static_code_analysis.yml @@ -37,10 +37,14 @@ jobs: - name: typos run: pixi run typos - - name: ruff + - name: ruff check run: pixi run lint if: ${{ always() }} + - name: ruff format + run: pixi run ruff format --check + if: ${{ always() }} + - name: mypy run: pixi run mypy-all if: ${{ always() }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf9d341e..c221bdd5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,10 +24,10 @@ repos: # Run the linter. - id: ruff-check # Run the formatter. - # - id: ruff-format + - id: ruff-format - repo: https://github.com/crate-ci/typos - rev: v1.34.0 + rev: v1.37.1 hooks: - id: typos diff --git a/deltakit-circuit/src/deltakit_circuit/_circuit.py b/deltakit-circuit/src/deltakit_circuit/_circuit.py index f860e70a..37b69a8a 100644 --- a/deltakit-circuit/src/deltakit_circuit/_circuit.py +++ b/deltakit-circuit/src/deltakit_circuit/_circuit.py @@ -424,16 +424,10 @@ def remove_noise(self, recursive: bool = True): if isinstance(layer, GateLayer): for gate in layer.gates: if isinstance(gate, OneQubitMeasurementGate): - layer.replace_gates( - { - gate: lambda gate: type(gate)(gate.qubit) - } - ) + layer.replace_gates({gate: lambda gate: type(gate)(gate.qubit)}) elif isinstance(gate, MPP): layer.replace_gates( - { - gate: lambda gate: type(gate)(gate.pauli_product) - } + {gate: lambda gate: type(gate)(gate.pauli_product)} ) elif isinstance(layer, Circuit) and recursive: layer.remove_noise(recursive) diff --git a/deltakit-circuit/src/deltakit_circuit/gates/_two_qubit_gates.py b/deltakit-circuit/src/deltakit_circuit/gates/_two_qubit_gates.py index 07f17dbb..8882bc53 100644 --- a/deltakit-circuit/src/deltakit_circuit/gates/_two_qubit_gates.py +++ b/deltakit-circuit/src/deltakit_circuit/gates/_two_qubit_gates.py @@ -346,9 +346,7 @@ class SQRT_ZZ_DAG(SymmetricTwoQubitGate[T]): stim_string: ClassVar[str] = "SQRT_ZZ_DAG" -class XCX( - ControlledGate[Qubit[T], Qubit[T]], SymmetricTwoQubitGate[Qubit[T]] -): +class XCX(ControlledGate[Qubit[T], Qubit[T]], SymmetricTwoQubitGate[Qubit[T]]): """The X-controlled X gate. First qubit is the control, second qubit is the target. Applies an X gate to the target if the control is in the ``|->`` state. @@ -476,9 +474,7 @@ class YCX(ControlledGate[Qubit[T], Qubit[T]]): stim_string: ClassVar[str] = "YCX" -class YCY( - ControlledGate[Qubit[T], Qubit[T]], SymmetricTwoQubitGate[Qubit[T]] -): +class YCY(ControlledGate[Qubit[T], Qubit[T]], SymmetricTwoQubitGate[Qubit[T]]): """The Y-controlled Y gate. First qubit is the control, second qubit is the target. Applies a Y gate to the target if the control is in the ``|-i>`` state. @@ -575,9 +571,7 @@ class CXSWAP(ControlledGate[Qubit[T], Qubit[T]]): stim_string: ClassVar[str] = "CXSWAP" -class CZSWAP( - SymmetricTwoQubitGate, ControlledGate[Qubit[T], Qubit[T]] -): +class CZSWAP(SymmetricTwoQubitGate, ControlledGate[Qubit[T], Qubit[T]]): """A combination CZ-and-SWAP gate. This gate is kak-equivalent to the `ISWAP` gate. Equivalent to `H` on the target qubit, followed by `CNOT` from target to control, `CNOT` from control to target, diff --git a/docs/conf.py b/docs/conf.py index b591b476..8932a30d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,12 @@ "sphinx.ext.mathjax", "sphinx_design", ] -myst_enable_extensions = ["colon_fence", "dollarmath", "amsmath", "substitution",] +myst_enable_extensions = [ + "colon_fence", + "dollarmath", + "amsmath", + "substitution", +] nb_execution_mode = "force" nb_execution_excludepatterns = [ @@ -63,12 +68,16 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["Thumbs.db", ".DS_Store", "json/*", - "_build/jupyter_execute", - "_build/html", - "examples/notebooks/demo", - "examples/notebooks/template_notebook.ipynb", - "examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb"] +exclude_patterns = [ + "Thumbs.db", + ".DS_Store", + "json/*", + "_build/jupyter_execute", + "_build/html", + "examples/notebooks/demo", + "examples/notebooks/template_notebook.ipynb", + "examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb", +] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" @@ -82,7 +91,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_permalinks_icon = '#' +html_permalinks_icon = "#" html_theme = "sphinxawesome_theme" # no link to an *.rst source @@ -102,11 +111,11 @@ "packages": {"[+]": ["physics"]} # Add the physics package }, "loader": { - "load": ["[tex]/physics"] # Load physics extension - } + "load": ["[tex]/physics"] # Load physics extension + }, } -highlight_language = 'python3' +highlight_language = "python3" suppress_warnings = [ "autosummary.import_cycle", diff --git a/docs/guide/decoding.md b/docs/guide/decoding.md index 7bb08eb6..8179de51 100644 --- a/docs/guide/decoding.md +++ b/docs/guide/decoding.md @@ -198,7 +198,7 @@ isolated bits of syndrome. AC also presents several advantages over decoders such as MWPM: -* AC is a hypergraph decoder, and can work with codes beyond surface codes (e.g. colour codes, +* AC is a hypergraph decoder, and can work with codes beyond surface codes (e.g. colour codes, qLDPC codes). MWPM only works in the world of surface codes. * MWPM only has the notion of X and Z noise. If these noises correlate, such that the system has Y-noise, AC may be more accurate than MWPM. @@ -213,8 +213,8 @@ The LCD contains two main components to achieve this balance between speed and a 1. A decoding engine that allows the decoder to scale; 2. An adaptivity engine that helps deal with leakage. -Leakage is a source of noise where qubits no longer occupy the $\vert 0\rangle$ and $\vert 1\rangle$ computational basis states, and instead -drift into higher energy 'leaked' states, specified as $\vert 2\rangle$, $\vert 3\rangle$, $\vert 4\rangle$, etc. Leakage noise is long-living and may +Leakage is a source of noise where qubits no longer occupy the $\vert 0\rangle$ and $\vert 1\rangle$ computational basis states, and instead +drift into higher energy 'leaked' states, specified as $\vert 2\rangle$, $\vert 3\rangle$, $\vert 4\rangle$, etc. Leakage noise is long-living and may spread to other qubits through multi-qubit gates. `parameters` is an optional dictionary, which may contain flags and values @@ -243,7 +243,7 @@ Particular decoders may also expose parameters for tuning. These are the cases f * The `BPOSDecoder` supports two optional parameters: - * `max_bp_rounds` is an integer, and specifies the maximum number of iterations + * `max_bp_rounds` is an integer, and specifies the maximum number of iterations of message passing that should be performed during the execution of belief propagation. It may terminate earlier. By default, this is 20. @@ -251,15 +251,15 @@ Particular decoders may also expose parameters for tuning. These are the cases f * The `ACDecoder` supports two optional parameters: - * `bp_rounds` is an integer, and specifies how many iterations of message passing + * `bp_rounds` is an integer, and specifies how many iterations of message passing should be performed during the execution of belief propagation. Note that `bp_rounds` in AC is different from `max_bp_rounds` in `BP_OSD` as early termination is not allowed. Typically, setting this equal to the distance of the code is sufficient. By default, this is 20. - * `ac_kappa_proportion` is a float, between 0.0 and 1.0, and reflects the number - of error mechanisms, in addition to those used to find a first solution, that should be - used to grow clusters to search for additional solutions, expressed as a proportion + * `ac_kappa_proportion` is a float, between 0.0 and 1.0, and reflects the number + of error mechanisms, in addition to those used to find a first solution, that should be + used to grow clusters to search for additional solutions, expressed as a proportion of the total number of error mechanisms. Setting this number higher results in better accuracy at the cost of slower performance. Start with 0.0 and increase by 0.01 until the desired accuracy is reached. Reasonable values lie between 0 and 0.1, as larger values will diff --git a/docs/guide/simulation.md b/docs/guide/simulation.md index ac38e71d..4b1bfd49 100644 --- a/docs/guide/simulation.md +++ b/docs/guide/simulation.md @@ -17,7 +17,7 @@ kernelspec: ## 1. Simulation of Measurements Decoding a quantum error correction experiment requires measurement data. -This can either be data simulated on a classical computer or data obtained from a QPU. +This can either be data simulated on a classical computer or data obtained from a QPU. If you have already run a quantum error correction experiment, you may have measurement results in one of the formats. Otherwise, you can use Deltakit and Stim to generate simulated measurement data, as described on this page. @@ -116,7 +116,7 @@ which allows you to define leakage and relaxation events, as well as leakage her ``` LEAKAGE(event_probability) [list of targets] RELAX(event_probability) [list of targets] -HERALD_LEAKAGE_EVENT(error_probability) [list of targets] +HERALD_LEAKAGE_EVENT(error_probability) [list of targets] ``` `HERALD_LEAKAGE_EVENT` commands typically go together with measurements. diff --git a/ruff.toml b/ruff.toml index c0b6d8cf..b71c591e 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,9 +1,9 @@ -# Repository-wide Ruff configuration. +# Repository-wide Ruff configuration. # -# Should only contain project-wide configuration. +# Should only contain project-wide configuration. # -# Note that: -# - if a Ruff configuration file is present in any subdirectory +# Note that: +# - if a Ruff configuration file is present in any subdirectory # and does not contain an "extend" value pointing directly or # indirectly (through a chain of "extend" to this file # then this file is completely ignored by ruff, @@ -48,7 +48,7 @@ exclude = [ # Only include the deltakit folders, not the docs/, tools/ or # other utility folder. -include = ["deltakit-*/**/*.py", "src/deltakit/**/*.py"] +include = ["deltakit-*/**/*.py", "src/deltakit/**/*.py", "tools/**/*.py", "tests/**/*.py"] # Same as Black. line-length = 88 @@ -63,7 +63,7 @@ select = ["A", "B", "E", "F", "N", "PL", "RUF100", "W", "NPY"] # Specific per-file ignores [lint.per-file-ignores] "**/__init__.py" = [ - # For the moment, dynamic __all__ generation raises a lot of issues with + # For the moment, dynamic __all__ generation raises a lot of issues with # F401 enabled, so ignore it where it happens, in __init__.py files. "F401", ] @@ -71,6 +71,7 @@ select = ["A", "B", "E", "F", "N", "PL", "RUF100", "W", "NPY"] [format] # Like Black, use double quotes for strings. quote-style = "double" +exclude = ["deltakit-explorer/**", "./deltakit-decode/**"] # Like Black, indent with spaces, rather than tabs. indent-style = "space" diff --git a/tests/qmem/test_qmem.py b/tests/qmem/test_qmem.py index 6b74427e..ba1e0984 100644 --- a/tests/qmem/test_qmem.py +++ b/tests/qmem/test_qmem.py @@ -10,14 +10,13 @@ import pytest from deltakit_circuit import Circuit, measurement_noise_profile from deltakit_circuit.gates import CX, MZ, RZ, H, OneQubitResetGate, PauliBasis -from deltakit_circuit.noise_channels import (Depolarise1, Depolarise2, - PauliXError) +from deltakit_circuit.noise_channels import Depolarise1, Depolarise2, PauliXError from deltakit_decode import PyMatchingDecoder from deltakit_decode.analysis import RunAllAnalysisEngine, StimDecoderManager -from deltakit_explorer.codes._css._css_code_experiment_circuit import \ - css_code_memory_circuit -from deltakit_explorer.codes._planar_code._rotated_planar_code import \ - RotatedPlanarCode +from deltakit_explorer.codes._css._css_code_experiment_circuit import ( + css_code_memory_circuit, +) +from deltakit_explorer.codes._planar_code._rotated_planar_code import RotatedPlanarCode from deltakit_explorer.qpu import QPU from deltakit_explorer.qpu._native_gate_set import NativeGateSet from deltakit_explorer.qpu._noise._noise_parameters import NoiseParameters @@ -30,11 +29,13 @@ # File name to save output data. filename = f"RPC_MWPM_X_MEM_{max_shots:.0e}_shots.csv" + def rotated_planar_xmem_noiseless( x_distance: int, z_distance: int, num_rounds: int ) -> Tuple[RotatedPlanarCode, Circuit]: - """This function takes as input quantum memory parameters (X-distance, Z-distance, Num Rounds) - and returns a noiseless logical-X quantum memory circuit in a default gate set. + """This function takes as input quantum memory parameters (X-distance, Z-distance, + Num Rounds) and returns a noiseless logical-X quantum memory circuit in a default + gate set. Parameters ---------- @@ -62,10 +63,11 @@ def rotated_planar_xmem_noiseless( return code, memory_circuit + def noise_model(physical_error_rate: float) -> NoiseParameters: - """This function takes as input a single parameter: the physical noise rate (p) of our noise model. - It creates the same noise model as outlined in the paper https://arxiv.org/abs/2205.09828 and returns - the relevant NoiseParameters object. + """This function takes as input a single parameter: the physical noise rate (p) of + our noise model. It creates the same noise model as outlined in the paper + https://arxiv.org/abs/2205.09828 and returns the relevant NoiseParameters object. Parameters ---------- @@ -97,7 +99,8 @@ def noise_model(physical_error_rate: float) -> NoiseParameters: ) ] - # Measurements outcomes are incorrectly recorded with probability physical_error_rate. + # Measurements outcomes are incorrectly recorded with probability + # physical_error_rate. measurement_flip_noise = measurement_noise_profile(physical_error_rate) # Idle qubits are affected by unbiased depolarising noise channels. @@ -114,11 +117,13 @@ def idle_noise(qubit, _t): return noise_model + def rotated_planar_xmem_noisy( x_distance: int, z_distance: int, num_rounds: int, noise_model: NoiseParameters ) -> Circuit: - """This function uses similar inputs to `rotated_planar_xmem_noiseless`, with the addition of a noise model. - It also has a restricted gate set hardcoded in, to match the gate set used in the paper. + """This function uses similar inputs to `rotated_planar_xmem_noiseless`, with the + addition of a noise model. It also has a restricted gate set hardcoded in, to match + the gate set used in the paper. Parameters @@ -135,7 +140,8 @@ def rotated_planar_xmem_noisy( Returns ------- Circuit - A `deltakit.circuit.Circuit` object describing a noisy quantum memory experiment. + A `deltakit.circuit.Circuit` object describing a noisy quantum memory + experiment. """ # Get code object and the noiseless circuit @@ -164,10 +170,12 @@ def rotated_planar_xmem_noisy( return noisy_circuit + def experiment_decoder_manager( x_distance: int, z_distance: int, num_rounds: int, physical_error_rate: float ) -> StimDecoderManager: - """This function will combine all previous functions to return a single value: the logical error rate. + """This function will combine all previous functions to return a single value: the + logical error rate. Parameters ---------- @@ -251,8 +259,10 @@ def test_qmem(): pt1pct1_shots, pt1pct1_fails = experiment_decoder_manager( x_distance=3, z_distance=3, num_rounds=3, physical_error_rate=0.001 ).run_batch_shots(1e5) - print(f"Logical error rate with physical error 1%: {pct1_fails/pct1_shots}") - print(f"Logical error rate with physical error .1%: {pt1pct1_fails/pt1pct1_shots}") + print(f"Logical error rate with physical error 1%: {pct1_fails / pct1_shots}") + print( + f"Logical error rate with physical error .1%: {pt1pct1_fails / pt1pct1_shots}" + ) distances = [3, 5, 7, 9] physical_error_rates = np.logspace(-5, -1.75, 14) @@ -268,18 +278,20 @@ def test_qmem(): for phys in physical_error_rates: num_shots = int(min(phys**-2, max_shots)) decoder_managers.extend( - (num_shots, experiment_decoder_manager(distance, distance, distance, phys)) - for distance in distances + (num_shots, experiment_decoder_manager(d, d, d, phys)) + for d in distances ) # Aggregate managers into run engines for multiprocessing data_frames = [ RunAllAnalysisEngine( - experiment_name = f"experiment_{num_shots}_{phys}", - decoder_managers = [manager for _, manager in manager_groups], - max_shots = num_shots + experiment_name=f"experiment_{num_shots}_{phys}", + decoder_managers=[manager for _, manager in manager_groups], + max_shots=num_shots, ).run() - for num_shots, manager_groups in groupby(decoder_managers, key=itemgetter(0)) + for num_shots, manager_groups in groupby( + decoder_managers, key=itemgetter(0) + ) ] pd.concat(data_frames).to_csv(filename, index=False) diff --git a/tests/unit/test_decoding_graph_tools_with_stim.py b/tests/unit/test_decoding_graph_tools_with_stim.py index 9273a7bb..d30030e0 100644 --- a/tests/unit/test_decoding_graph_tools_with_stim.py +++ b/tests/unit/test_decoding_graph_tools_with_stim.py @@ -5,108 +5,140 @@ import stim from deltakit_core.decoding_graphs import NXDecodingGraph from deltakit_core.decoding_graphs._decoding_graph_tools import ( - compute_graph_distance, filter_to_data_edges, filter_to_measure_edges, - graph_to_json, nx_graph_from_json, unweight_graph) + compute_graph_distance, + filter_to_data_edges, + filter_to_measure_edges, + graph_to_json, + nx_graph_from_json, + unweight_graph, +) from deltakit_decode.utils._graph_circuit_helpers import ( - parse_stim_circuit, stim_circuit_to_graph_dem) + parse_stim_circuit, + stim_circuit_to_graph_dem, +) -class TestGraphToJSON(): - +class TestGraphToJSON: def test_2_boundary_graph_to_json_raises_exception(self): - graph = NXDecodingGraph.from_edge_list([(i, (i + 1) % 17) for i in range(17)], - boundaries=[16, 8]) + graph = NXDecodingGraph.from_edge_list( + [(i, (i + 1) % 17) for i in range(17)], boundaries=[16, 8] + ) logicals = [[(4, 5)]] - with pytest.raises(ValueError, - match="JSON graph representation supports maximum one boundary"): + with pytest.raises( + ValueError, match="JSON graph representation supports maximum one boundary" + ): graph_to_json(graph, logicals) - @pytest.fixture(scope="class", params=[ - "surface_code:rotated_memory_x", - "surface_code:unrotated_memory_z", - ]) + @pytest.fixture( + scope="class", + params=[ + "surface_code:rotated_memory_x", + "surface_code:unrotated_memory_z", + ], + ) def stim_circuit(self, request): distance = 5 - stim_circ = stim.Circuit.generated(request.param, - distance=distance, rounds=distance, - after_clifford_depolarization=0.01, - before_measure_flip_probability=0.01, - before_round_data_depolarization=0.01) + stim_circ = stim.Circuit.generated( + request.param, + distance=distance, + rounds=distance, + after_clifford_depolarization=0.01, + before_measure_flip_probability=0.01, + before_round_data_depolarization=0.01, + ) return stim_circ - def test_full_graph_to_json_on_code_tasks_matches_original_graph(self, stim_circuit): + def test_full_graph_to_json_on_code_tasks_matches_original_graph( + self, stim_circuit + ): graph, logicals, _ = parse_stim_circuit(stim_circuit) json_str = graph_to_json(graph, logicals, full=True) - reconstructed_graph, reconstructed_logicals = nx_graph_from_json( - json_str) + reconstructed_graph, reconstructed_logicals = nx_graph_from_json(json_str) assert set(reconstructed_graph.nodes) == set(graph.nodes) assert set(reconstructed_graph.edges) == set(graph.edges) - assert all(log == rec_log for log, rec_log in zip( - logicals, reconstructed_logicals)) + assert all( + log == rec_log + for log, rec_log in zip(logicals, reconstructed_logicals, strict=True) + ) assert reconstructed_graph.detector_records == graph.detector_records assert reconstructed_graph.edge_records == graph.edge_records def test_graph_to_json_on_code_tasks_matches_original_graph(self, stim_circuit): graph, logicals, _ = parse_stim_circuit(stim_circuit) json_str = graph_to_json(graph, logicals) - reconstructed_graph, reconstructed_logicals = nx_graph_from_json( - json_str) + reconstructed_graph, reconstructed_logicals = nx_graph_from_json(json_str) assert set(reconstructed_graph.nodes) == set(graph.nodes) assert set(reconstructed_graph.edges) == set(graph.edges) - assert all(log == rec_log for log, rec_log in zip( - logicals, reconstructed_logicals)) + assert all( + log == rec_log + for log, rec_log in zip(logicals, reconstructed_logicals, strict=True) + ) def test_stim_circuit_to_graph_dem_does_not_decompose_the_rep_code(): - stim_rep_code = stim.Circuit.generated("repetition_code:memory", - distance=5, rounds=5, - after_clifford_depolarization=0.1) + stim_rep_code = stim.Circuit.generated( + "repetition_code:memory", + distance=5, + rounds=5, + after_clifford_depolarization=0.1, + ) - assert str(stim_circuit_to_graph_dem(stim_rep_code)).find('^') == -1 + assert str(stim_circuit_to_graph_dem(stim_rep_code)).find("^") == -1 -@pytest.mark.parametrize("code_task", [ - "surface_code:rotated_memory_x", - "surface_code:unrotated_memory_z", -]) +@pytest.mark.parametrize( + "code_task", + [ + "surface_code:rotated_memory_x", + "surface_code:unrotated_memory_z", + ], +) def test_stim_circuit_to_graph_dem_does_decompose_non_rep_codes(code_task): - stim_rep_code = stim.Circuit.generated(code_task, - distance=5, rounds=5, - after_clifford_depolarization=0.1) + stim_rep_code = stim.Circuit.generated( + code_task, distance=5, rounds=5, after_clifford_depolarization=0.1 + ) - assert str(stim_circuit_to_graph_dem(stim_rep_code)).find('^') != -1 + assert str(stim_circuit_to_graph_dem(stim_rep_code)).find("^") != -1 -@pytest.mark.parametrize("code_task", [ - "surface_code:rotated_memory_x", - "surface_code:unrotated_memory_z", -]) -@pytest.mark.parametrize("distance", [ - 3, 5, 7 -]) +@pytest.mark.parametrize( + "code_task", + [ + "surface_code:rotated_memory_x", + "surface_code:unrotated_memory_z", + ], +) +@pytest.mark.parametrize("distance", [3, 5, 7]) def test_compute_graph_distance(code_task, distance): - stim_circ = stim.Circuit.generated(code_task, - distance=distance, rounds=distance, - after_clifford_depolarization=0.01, - before_measure_flip_probability=0.01, - before_round_data_depolarization=0.01) + stim_circ = stim.Circuit.generated( + code_task, + distance=distance, + rounds=distance, + after_clifford_depolarization=0.01, + before_measure_flip_probability=0.01, + before_round_data_depolarization=0.01, + ) graph, logicals, stim_circ = parse_stim_circuit(stim_circ) assert compute_graph_distance(graph, logicals) == distance -@pytest.mark.parametrize("code_task", [ - "surface_code:rotated_memory_x", - "surface_code:unrotated_memory_z", -]) -@pytest.mark.parametrize("distance", [ - 3, 5, 7 -]) +@pytest.mark.parametrize( + "code_task", + [ + "surface_code:rotated_memory_x", + "surface_code:unrotated_memory_z", + ], +) +@pytest.mark.parametrize("distance", [3, 5, 7]) def test_compute_graph_weighted_distance(code_task, distance): - stim_circ = stim.Circuit.generated(code_task, - distance=distance, rounds=distance, - after_clifford_depolarization=0.01, - before_measure_flip_probability=0.01, - before_round_data_depolarization=0.01) + stim_circ = stim.Circuit.generated( + code_task, + distance=distance, + rounds=distance, + after_clifford_depolarization=0.01, + before_measure_flip_probability=0.01, + before_round_data_depolarization=0.01, + ) graph, logicals, stim_circ = parse_stim_circuit(stim_circ) computed_distance = compute_graph_distance(graph, logicals, weighted=True) assert isinstance(computed_distance, float) @@ -116,49 +148,65 @@ def test_compute_graph_weighted_distance(code_task, distance): assert computed_distance <= max_weight * distance -@pytest.mark.parametrize("circuit_path, expected_distance", [ - (Path("stim/circuit_logical_off_boundary.stim"), 3), - (Path("stim/circuit_two_equivalent_logicals.stim"), 3), - (Path("stim/circuit_multi_logicals.stim"), 3), -]) -def test_compute_graph_distance_from_file(circuit_path, expected_distance, reference_data_dir): +@pytest.mark.parametrize( + "circuit_path, expected_distance", + [ + (Path("stim/circuit_logical_off_boundary.stim"), 3), + (Path("stim/circuit_two_equivalent_logicals.stim"), 3), + (Path("stim/circuit_multi_logicals.stim"), 3), + ], +) +def test_compute_graph_distance_from_file( + circuit_path, expected_distance, reference_data_dir +): stim_circ = stim.Circuit.from_file(reference_data_dir / circuit_path) graph, logicals, stim_circ = parse_stim_circuit(stim_circ) assert compute_graph_distance(graph, logicals) == expected_distance -@pytest.mark.parametrize("code_task", [ - "surface_code:rotated_memory_x", - "surface_code:unrotated_memory_z", -]) +@pytest.mark.parametrize( + "code_task", + [ + "surface_code:rotated_memory_x", + "surface_code:unrotated_memory_z", + ], +) def test_unweight_graph(code_task): distance = 5 - stim_circ = stim.Circuit.generated(code_task, - distance=distance, rounds=distance, - after_clifford_depolarization=0.01, - before_measure_flip_probability=0.01, - before_round_data_depolarization=0.01) + stim_circ = stim.Circuit.generated( + code_task, + distance=distance, + rounds=distance, + after_clifford_depolarization=0.01, + before_measure_flip_probability=0.01, + before_round_data_depolarization=0.01, + ) unweighted_graph, _, stim_circ = parse_stim_circuit(stim_circ) unweight_graph(unweighted_graph) - assert all(record["weight"] == 1 - for record in unweighted_graph.edge_records.values()) + assert all( + record["weight"] == 1 for record in unweighted_graph.edge_records.values() + ) # check that p_err property is set so that the computed weight is also the same assert all(record.weight == 1 for record in unweighted_graph.edge_records.values()) -@pytest.mark.parametrize("code_task", [ - "surface_code:rotated_memory_x", - "surface_code:unrotated_memory_z", -]) -@pytest.mark.parametrize("distance", [ - 3, 5, 7 -]) +@pytest.mark.parametrize( + "code_task", + [ + "surface_code:rotated_memory_x", + "surface_code:unrotated_memory_z", + ], +) +@pytest.mark.parametrize("distance", [3, 5, 7]) def test_filter_to_edges(code_task, distance): - stim_circ = stim.Circuit.generated(code_task, - distance=distance, rounds=distance, - after_clifford_depolarization=0.01, - before_measure_flip_probability=0.01, - before_round_data_depolarization=0.01) + stim_circ = stim.Circuit.generated( + code_task, + distance=distance, + rounds=distance, + after_clifford_depolarization=0.01, + before_measure_flip_probability=0.01, + before_round_data_depolarization=0.01, + ) graph, _, _ = parse_stim_circuit(stim_circ) data_edges = filter_to_data_edges(graph) assert data_edges # not empty diff --git a/tools/extract_version_notes.py b/tools/extract_version_notes.py index 6b9e63e8..0e53422c 100644 --- a/tools/extract_version_notes.py +++ b/tools/extract_version_notes.py @@ -1,6 +1,7 @@ """ Extracts most recent release notes from `CHANGELOG.md` for use in a GitHub release """ + import re import sys from pathlib import Path diff --git a/tools/propagate_version.py b/tools/propagate_version.py index 3b573ad0..0f6fe39c 100644 --- a/tools/propagate_version.py +++ b/tools/propagate_version.py @@ -1,10 +1,13 @@ """ -This module is used by the "release" pixi task in order to propagate the version in the toml file of the top-level project into the toml files of the sub-projects in the monorepo. +This module is used by the "release" pixi task in order to propagate the version in the +toml file of the top-level project into the toml files of the sub-projects in the +monorepo. """ + import tomlkit # type: ignore[import-not-found] # Read the top-level version -with open("pyproject.toml", "r", encoding='utf-8') as f: +with open("pyproject.toml", "r", encoding="utf-8") as f: top_data = tomlkit.load(f) version = top_data["project"]["version"] diff --git a/tools/set_pre_version.py b/tools/set_pre_version.py index 4262e5d3..f74b8e68 100644 --- a/tools/set_pre_version.py +++ b/tools/set_pre_version.py @@ -3,13 +3,14 @@ Usage: `python tools/set_pre_version.py ` e.g. `python tools/set_pre_version.py .dev20250820160500` """ + import argparse import os import tomlkit # type: ignore[import-not-found] # Read the top-level version -with open("pyproject.toml", "r", encoding='utf-8') as f: +with open("pyproject.toml", "r", encoding="utf-8") as f: top_data = tomlkit.load(f) base_version = top_data["project"]["version"] @@ -20,7 +21,13 @@ version = base_version + args.suffix -projects = [".", "deltakit-explorer", "deltakit-circuit", "deltakit-core", "deltakit-decode"] +projects = [ + ".", + "deltakit-explorer", + "deltakit-circuit", + "deltakit-core", + "deltakit-decode", +] for project in projects: path = f"{project}/pyproject.toml" diff --git a/tools/transform_labels.py b/tools/transform_labels.py index 8115e2cb..f69f622e 100644 --- a/tools/transform_labels.py +++ b/tools/transform_labels.py @@ -1,25 +1,37 @@ """ Transform labels to JSON compatible list to be used in GitHub actions with https://github.com/actions/labeler """ + import json import os -DEFAULT_PACKAGES = ["deltakit-explorer", "deltakit-circuit", "deltakit-core", "deltakit-decode"] +DEFAULT_PACKAGES = [ + "deltakit-explorer", + "deltakit-circuit", + "deltakit-core", + "deltakit-decode", +] def transform(labels_str: str) -> str: if not labels_str.strip(): return json.dumps(DEFAULT_PACKAGES) - labels = [label.strip() for label in labels_str.split(",") if label.strip() in DEFAULT_PACKAGES] + labels = [ + label.strip() + for label in labels_str.split(",") + if label.strip() in DEFAULT_PACKAGES + ] if not labels: return json.dumps(DEFAULT_PACKAGES) return json.dumps(labels) + def main(): all_labels = os.getenv("ALL_LABELS", "") with open(os.getenv("GITHUB_OUTPUT"), "a") as f: f.write(f"JSON_LABELS_ALL={transform(all_labels)}\n") + if __name__ == "__main__": main() From f8e6010b3f7912882fd2072acc29ee9fe5e38a64 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Fri, 3 Oct 2025 10:11:11 -0700 Subject: [PATCH 07/26] docs: amend pixi setup instruction (#93) * docs: amend pixi setup instruction * docs: adjustment --- CONTRIBUTING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8580cc60..f547a8bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,9 +79,11 @@ We recommend that contributors use [`pixi`](https://prefix.dev/) to manage their environment and run tasks. After cloning the repository and [installing `pixi`](https://pixi.sh/latest/), navigate to -the root directory of the repository in a terminal and install dependencies with `pixi install`. You can then activate the Python environment with `pixi shell`. To deactivate the environment, run `exit`. - -Run `pixi install -e dev` to create a development environment that includes `pytest` dependencies. You can then activate the Python environment with `pixi shell -e dev`. To set up the Python interpreter in VS Code, you can set the `python.defaultInterpreterPath` variable to `"${workspaceFolder}/.pixi/envs/dev/bin/python"` in `settings.json`. +the root directory of the repository in a terminal and install dependencies with `pixi install`. +You can then activate the Python environment with `pixi shell`. +To deactivate the environment, run `exit`. +To set up the Python interpreter in VS Code, you can set the `python.defaultInterpreterPath` +variable to `"${workspaceFolder}/.pixi/envs/default/bin/python"` in `settings.json`. ```{dropdown} Linux/macOS users... Depending on system settings, you may experience a `Too many open files (os error 24) at path...` From 1a4ef4a2c6a143b37f7df2c5d05165b934af054c Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Fri, 3 Oct 2025 10:18:31 -0700 Subject: [PATCH 08/26] docs: links should point to `docs` (built from `docs-live` branch) (#74) * docs: links should point to latest stable docs * Update index.rst * Update README.md * Apply suggestions from code review Co-authored-by: Guen Prawiroatmodjo <4041805+guenp@users.noreply.github.com> --- README.md | 8 ++++---- deltakit-circuit/README.md | 2 +- deltakit-core/README.md | 2 +- deltakit-decode/README.md | 2 +- deltakit-explorer/README.md | 2 +- docs/index.rst | 8 ++++---- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6eaccd25..90b4a8e3 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![DOI][doi-badge]][doi-link] [docs-badge]: https://readthedocs.org/projects/deltakit/badge/?version=latest -[docs-link]: https://deltakit.readthedocs.io/en/latest/ +[docs-link]: https://deltakit.readthedocs.io/ [pypi-badge]: https://img.shields.io/pypi/v/deltakit.svg [pypi-link]: https://pypi.org/project/deltakit/ @@ -51,8 +51,8 @@ Whether you're a seasoned QEC researcher or just starting out, Deltakit supports in exploring new ways to implement QEC logic all the way to running complex QEC circuits on QPU hardware. -
- +
+ @@ -138,7 +138,7 @@ circuit = css_code_stability_circuit( print(circuit) ``` -Learn more by reading the [Deltakit docs](https://deltakit.readthedocs.io/en/latest/)! +Learn more by reading the [Deltakit docs](https://deltakit.readthedocs.io/)! ## Support diff --git a/deltakit-circuit/README.md b/deltakit-circuit/README.md index f6c2b0c3..2872e7b4 100644 --- a/deltakit-circuit/README.md +++ b/deltakit-circuit/README.md @@ -12,7 +12,7 @@ [![Discussions][discussions-badge]][discussions-link] [docs-badge]: https://readthedocs.org/projects/deltakit/badge/?version=latest -[docs-link]: https://deltakit.readthedocs.io/en/latest/ +[docs-link]: https://deltakit.readthedocs.io/ [pypi-badge]: https://img.shields.io/pypi/v/deltakit.svg [pypi-link]: https://pypi.org/project/deltakit/ diff --git a/deltakit-core/README.md b/deltakit-core/README.md index f69370e9..596faf2b 100644 --- a/deltakit-core/README.md +++ b/deltakit-core/README.md @@ -12,7 +12,7 @@ [![Discussions][discussions-badge]][discussions-link] [docs-badge]: https://readthedocs.org/projects/deltakit/badge/?version=latest -[docs-link]: https://deltakit.readthedocs.io/en/latest/ +[docs-link]: https://deltakit.readthedocs.io/ [pypi-badge]: https://img.shields.io/pypi/v/deltakit.svg [pypi-link]: https://pypi.org/project/deltakit/ diff --git a/deltakit-decode/README.md b/deltakit-decode/README.md index 967500c6..a0b215fc 100644 --- a/deltakit-decode/README.md +++ b/deltakit-decode/README.md @@ -12,7 +12,7 @@ [![Discussions][discussions-badge]][discussions-link] [docs-badge]: https://readthedocs.org/projects/deltakit/badge/?version=latest -[docs-link]: https://deltakit.readthedocs.io/en/latest/ +[docs-link]: https://deltakit.readthedocs.io/ [pypi-badge]: https://img.shields.io/pypi/v/deltakit.svg [pypi-link]: https://pypi.org/project/deltakit/ diff --git a/deltakit-explorer/README.md b/deltakit-explorer/README.md index 3308f468..4dacedb5 100644 --- a/deltakit-explorer/README.md +++ b/deltakit-explorer/README.md @@ -12,7 +12,7 @@ [![Discussions][discussions-badge]][discussions-link] [docs-badge]: https://readthedocs.org/projects/deltakit/badge/?version=latest -[docs-link]: https://deltakit.readthedocs.io/en/latest/ +[docs-link]: https://deltakit.readthedocs.io/ [pypi-badge]: https://img.shields.io/pypi/v/deltakit.svg [pypi-link]: https://pypi.org/project/deltakit/ diff --git a/docs/index.rst b/docs/index.rst index f2c7c588..06d623fa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,25 +39,25 @@ To get started with Deltakit, follow these steps: :alt: Error-correcting codes :align: center :width: 250px - :target: https://deltakit.readthedocs.io/en/latest/api.html#deltakit-explorer-codes + :target: https://deltakit.readthedocs.io/en/docs/api.html#deltakit-explorer-codes - .. figure:: _static/images/qpu_noise.png :alt: QPU and noise analysis :align: center :width: 250px - :target: https://deltakit.readthedocs.io/en/latest/api.html#deltakit-explorer-qpu + :target: https://deltakit.readthedocs.io/en/docs/api.html#deltakit-explorer-qpu * - .. figure:: _static/images/experiments.png :alt: Experiments :align: center :width: 250px - :target: https://deltakit.readthedocs.io/en/latest/api.html#deltakit-explorer + :target: https://deltakit.readthedocs.io/en/docs/api.html#deltakit-explorer - .. figure:: _static/images/decoders.png :alt: Decoders :align: center :width: 250px - :target: https://deltakit.readthedocs.io/en/latest/api.html#deltakit-decode + :target: https://deltakit.readthedocs.io/en/docs/api.html#deltakit-decode .. toctree:: :maxdepth: 2 From f6ad96ab62056c1fbd516c97a43def9ca458a4e5 Mon Sep 17 00:00:00 2001 From: Adrien Suau <12374487+nelimee@users.noreply.github.com> Date: Sat, 4 Oct 2025 00:18:10 +0100 Subject: [PATCH 09/26] feat: better logical error probability per round (#67) --------- Co-authored-by: Guen Prawiroatmodjo <4041805+guenp@users.noreply.github.com> --- .../deltakit_explorer/analysis/_analysis.py | 418 +++++++++++++++++- .../tests/analysis/test_analysis.py | 197 ++++++++- mypy.ini | 4 + 3 files changed, 590 insertions(+), 29 deletions(-) diff --git a/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py b/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py index 2b9771ac..8eb39caf 100644 --- a/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py +++ b/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py @@ -2,10 +2,14 @@ """`analysis` module aggregates client-side analytical tools. """ from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from math import floor +import warnings import numpy as np import numpy.typing as npt +from scipy.optimize import curve_fit from deltakit_explorer._utils._logging import Logging @@ -113,6 +117,373 @@ def get_exp_fit( Logging.error(ex, query_id) raise +@dataclass(frozen=True) +class LogicalErrorRatePerRoundResults: + """Named-tuple-like class containing computation results from + :func:`compute_logical_error_per_round`. + + Attributes: + leppr (float): Logical Error Probability Per Round (LEPPR). + leppr_stddev (float): LEPPR standard deviation. + spam_error (float): computed SPAM error-rate. + spam_error_stddev (float): SPAM error-rate standard deviation. + leppr_stddev_propagated (float): standard deviation due to the propagation of + individual logical error probabilities standard deviations used to estimate + the LEPPR. + leppr_stddev_fit (float): standard deviation due to the linear fit involved in + LEPPR computation. + spam_error_stddev_propagated (float): standard deviation due to the propagation + of individual logical error probabilities standard deviations used to + estimate the SPAM error-rate. + spam_error_stddev_fit (float): standard deviation due to the linear fit involved + in SPAM error-rate computation. + + Note: + attributes ending in ``_stddev_propagated`` or ``_stddev_fit`` are internal + estimations that might be useful to understand the contribution of each process + to the final standard-deviation estimation. + """ + leppr: float + leppr_stddev: float + spam_error: float + spam_error_stddev: float + + leppr_stddev_propagated: float + leppr_stddev_fit: float + spam_error_stddev_propagated: float + spam_error_stddev_fit: float + + +def compute_logical_error_per_round( + num_failed_shots: npt.NDArray[np.int_] | Sequence[int], + num_shots: npt.NDArray[np.int_] | Sequence[int], + num_rounds: npt.NDArray[np.int_] | Sequence[int], + *, + force_include_single_round: bool = False, +) -> LogicalErrorRatePerRoundResults: + """Compute the logical error-rate per round from different logical error-rate + computations. + + This function implements the method described in: + + 1. https://arxiv.org/pdf/2310.05900.pdf (p.40) + 2. https://arxiv.org/pdf/2207.06431.pdf (p.21) + 3. https://arxiv.org/pdf/2505.09684.pdf (p.8) + + to recover an estimator of the logical error-rate per round from the estimated + values of logical error-rates for several round durations. + + Args: + num_failed_shots (npt.NDArray[np.int_] | Sequence[int]): + a sequence of integers representing the number of failed shots for each of + the number of rounds in ``num_rounds``. Should be the same length as + ``num_rounds``. + num_shots (npt.NDArray[np.int_] | Sequence[int]): + a sequence of integers representing the total number of shots for each of + the number of rounds in ``num_rounds``. Should be the same length as + ``num_rounds``. + num_rounds (npt.NDArray[np.int_] | Sequence[int]): + a sequence of integers representing the number of rounds used to get the + corresponding results in ``num_failed_shots`` and ``num_shots``. Any value + inferior to 1 (``< 1``) is automatically removed from this list along with + the corresponding values in ``num_shots`` and ``num_failed_shots``. Any + value equal to 1 is removed from this list along with the corresponding + values in ``num_shots`` and ``num_failed_shots`` iff + ``force_include_single_round`` is ``False``. If only one data-point is + provided (or left after the removal process described just before), the SPAM + error is assumed to be ``0`` and an estimation will still be returned. + + Heuristically, to increase the returned estimation precision, you should try + to provide data for rounds such that the estimated logical error-rate for + the number of rounds ``max(num_rounds)`` is approximately ``0.4``. This + ``0.4`` value has been set to reduce fitting errors. + force_include_single_round (bool): + if ``True``, data obtained from 1-round experiment will be used in the + computation if provided in ``num_rounds``. Default to ``False`` which + results in 1-round data being ignored due to boundary effects that affect + the final estimation. See https://arxiv.org/pdf/2207.06431.pdf (p.21). + + Returns: + LEPPRResults: detailed results of the computation. + + Examples: + Calculating per-round logical error probability and its standard deviation + given number of fails, and number of shots for several rounds:: + + res = compute_logical_error_per_round( + num_failed_shots=[34, 151, 356], + num_shots=[500000] * 3, + num_rounds=[2, 4, 6], + ) + leppr, leppr_stddev = res.leppr, res.leppr_stddev + spam, spam_stddev = res.spam_error, res.spam_error_stddev + + """ + # Get the inputs as numpy arrays. + # Sanitization: also make sure that the inputs are sorted. + isort = np.argsort(num_rounds) + num_rounds = np.asarray(num_rounds)[isort] + num_failed_shots = np.asarray(num_failed_shots)[isort] + num_shots = np.asarray(num_shots)[isort] + + # Check that we do not have duplicate data for the same number of rounds as that + # will confuse the numerical methods used in this function. + unique_counts = np.unique_counts(num_rounds) + non_unique_entries_mask = unique_counts.counts > 1 + if np.any(non_unique_entries_mask): + non_unique_values = unique_counts.values[non_unique_entries_mask].tolist() + raise RuntimeError( + "Multiple entries were provided for the following number of rounds: " + f"{non_unique_values}. This is not supported. Please make sure you only " + "provide one entry per number of rounds." + ) + + # Check that we do not have any num_rounds <= 0 entry. + while num_rounds[0] <= 0: + warnings.warn( + f"Found an invalid number of rounds: {num_rounds[0]}. Number of rounds " + "should be >= 1." + ) + num_rounds = num_rounds[1:] + num_failed_shots = num_failed_shots[1:] + num_shots = num_shots[1:] + + # Filter out the r == 1 input if not forced to include it by the user. + if num_rounds[0] == 1 and not force_include_single_round: + num_rounds = num_rounds[1:] + num_failed_shots = num_failed_shots[1:] + num_shots = num_shots[1:] + + if np.any(num_failed_shots == 0): + raise RuntimeError( + "Got an experiment without any errors. You should increase the number of " + "shots to have at least one error, else the problem is ill-formed." + ) + + logical_error_rates = num_failed_shots / num_shots + fidelities = 1 - 2 * logical_error_rates + + if np.any(fidelities <= 0): + raise RuntimeError( + "Got estimations of logical error-rates above 0.5. That is not supported " + "by this function. Please reduce your number of rounds. Estimated logical " + f"error-rates: {logical_error_rates}." + ) + # Check if the heuristic guideline on the number of rounds is verified. + max_logical_error_rate = np.max(logical_error_rates) + if max_logical_error_rate < 0.2: + warnings.warn( + f"The maximum estimated logical error-rate ({max_logical_error_rate}) is " + "below 0.2. The returned estimation might be better if you add data with " + "more rounds such that the maximum estimated logical error-rate is closer " + "to 0.4." + ) + + # We want to do a linear regression on the log values of fidelity, and obtain the + # per-round error rate like that. + # Applying the logarithm function will change non-uniformly the standard deviation + # of each variable, which makes the standard linear regression estimator biased. The + # best linear unbiased estimator in that case is obtained by solving a weighted + # least square problem where the weights corresponds to the reciprocal of the + # variance of each observation. + # See https://en.wikipedia.org/wiki/Weighted_least_squares. + logfidelity = np.log(fidelities) + # We approximate the standard deviation with an error propagation analysis. This + # method has been tested against scipy and returns similar results. + # Alias for more readability + pl = logical_error_rates + pl_stddev = np.sqrt(pl * (1 - pl) / num_shots) + logfidelities_stddev = 2 * pl_stddev / fidelities + + # If the user only provided one data point, we add a noiseless data-point assuming + # that the SPAM error is 0. + if logfidelity.size == 1: + warnings.warn( + "Only one data-point provided for logical error probability per round. " + "Continuing computation assuming that SPAM error is negligible." + ) + num_rounds = np.hstack([[0], num_rounds]) + logfidelity = np.hstack([[0], logfidelity]) + # We cannot set the stddev to 0 here because curve_fit will divide by that + # quantity, so make it very small. + logfidelities_stddev = np.hstack([[1e-12], logfidelities_stddev]) + + # Note that the covariance matrix is used later to estimate the logical error-rate + # per round standard deviation. + (slope, offset), cov = curve_fit( + lambda x, s, o: s * x + o, + num_rounds, + logfidelity, + sigma=logfidelities_stddev, + absolute_sigma=True, + # If the error probabilities are exactly 0, the solution should be (0, 0). + # Because we expect the error probabilities to be close to 0, start from (0, 0) + # as a first estimate. + p0=(0, 0), + # Both slope and offset are used to recover a probability with the expression + # p = (1 - np.exp(value)) / 2. Because a probability needs to be in [0, 1], we + # have that value <= np.log(1). + # Note: even though the below bounds are valid, setting bounds changes the + # default optimisation method from "lm" to "trf". There is at least one + # real-world example where setting those bounds led to incorrect results, so not + # including them for the moment. + # bounds=((-np.inf, -np.inf), (np.log(1), np.log(1))), + ) + + estimated_logical_error_per_round = float((1 - np.exp(slope)) / 2) + # Compute the standard R2 (Coefficient of determination) using the formula + # ``R2 = 1 - SSE / SST`` where SSE is the Sum of Squares Error and SST is the Sum of + # Square Total that are computed below. + sse = np.sum((logfidelity - offset - slope * num_rounds) ** 2) + sst = np.sum((logfidelity - np.mean(logfidelity)) ** 2) + r2 = float(1 - sse / sst) + if abs(r2) < 0.98: + warnings.warn( + f"Got a R2 value of {r2} < 0.98. Estimation might be imprecise. Increasing " + "the number of shots or re-performing the computation might help in removing " + "this warning." + ) + + # Following https://arxiv.org/pdf/2505.09684v1 (Methods - Extracting logical error + # per cycle, page 8) we estimate the variance on the logical error-rate per cycle + # (named Perrc below) using the formula + # sigma(Perrc) = (1 - Perrc) * sigma(slope) + # The standard deviation on the linear fit parameters can be obtained through the + # covariance matrix diagonal entries. + slope_stddev, offset_stddev = np.sqrt(np.diagonal(cov)) + estimated_logical_error_per_round_stddev = ( + (1 - 2 * estimated_logical_error_per_round) * slope_stddev / 2 + ) + + # Else + estimated_spam_error = float((1 - np.exp(offset)) / 2) + estimated_spam_error_stddev = (1 - 2 * estimated_spam_error) * offset_stddev / 2 + return LogicalErrorRatePerRoundResults( + estimated_logical_error_per_round, + estimated_logical_error_per_round_stddev, + estimated_spam_error, + estimated_spam_error_stddev, + (1 - 2 * estimated_logical_error_per_round) / 2, + slope_stddev, + (1 - 2 * estimated_spam_error) / 2, + offset_stddev, + ) + +def simulate_different_round_numbers_for_lep_per_round_estimation( + simulator: Callable[[int], tuple[int, int]], + initial_round_number: int = 2, + next_round_number_func: Callable[[int], int] = lambda x: 2 * x, + maximum_round_number: int | None = None, + heuristic_logical_error_lower_bound: float = 0.25, + heuristic_logical_error_upper_bound: float = 0.45, +) -> tuple[npt.NDArray[np.int_], npt.NDArray[np.int_], npt.NDArray[np.int_]]: + """Compute QEC results to estimate the logical error-rate per round. + + This function aims at encapsulating the practical knowledge about logical error-rate + per round computation to help any user computing the required logical error-rates + for useful number of rounds. + + It repeatedly calls ``simulator`` with a number of rounds growing according to + ``next_round_number_func``, starting from ``initial_round_number``, + until the logical error-rate is above ``heuristic_logical_error_lower_bound``. If + the final step returned a logical error-rate above + ``heuristic_logical_error_upper_bound``, the algorithm then goes backward and + replaces that last value with the first one under that limit. + + Args: + simulator (Callable[[int], tuple[int, int]]): + a callable that returns a tuple ``(num_fails, num_shots)`` from a number of + rounds given as input. + initial_round_number (int): initial value for the geometric series that will be + used to generate the number of rounds. + next_round_number_func (Callable[[int], int]): function used to compute the + next round number that should be tested. Default to a linear scaling up to + 500 rounds and then an exponential scaling. The initial linear scaling is to + avoid the nearby points generated at the beginning of the exponential + scaling whereas the final exponential scaling is to avoid spending too much + time if the noise is really low. + maximum_round_number (int): if set, this function will stop once the next + number of rounds (computed with ``next_round_number_func``) is above that + threshold. If not set, only the other stopping criterions apply. + heuristic_logical_error_lower_bound (float): minimum target logical error-rate + for the final round. Might not be verified by the return of this function if + ``maximum_round_number`` is set and reached before that minimum threshold. + heuristic_logical_error_upper_bound (float): maximal target logical error-rate + for the final round. Should be set sufficiently below ``0.5`` such that the + uncertainties (mostly due to finite sampling) on the computed logical + error-rate (LER) are low enough to not introduce a plateau in the log-plot + of the fidelity log(F) = log(1 - 2*LER). Experimentally, ``0.45`` seems to + check that. + + Returns: + tuple[npt.NDArray[np.int_], npt.NDArray[np.int_], npt.NDArray[np.int_]]: + A tuple consisting of + - the different number of rounds corresponding to the two other entries, + - the number of failed shots for the corresponding number of rounds, + - the total number of shots for the corresponding number of rounds. + + Examples: + Calculating per-round logical error probability and its standard deviation + given number of fails, and number of shots for several rounds:: + + def perfect_simulator(num_rounds: int) -> tuple[int, int]: + error_per_round: float = 0.001 + total_error: float = (1 - error_per_round) ** num_rounds + num_shots: int = 100_000 + num_fails = total_error * num_shots + return num_fails, num_shots + + + nrounds, nfails, nshots = ( + simulate_different_round_numbers_for_lep_per_round_estimation( + simulator=perfect_simulator, + initial_round_number=2, + geometric_factor=1.7, + ) + ) + """ + if maximum_round_number is None: + maximum_round_number = 2**30 + + nrounds: list[int] = [initial_round_number] + nfails: list[int] = [] + nshots: list[int] = [] + + nfail, nshot = simulator(nrounds[-1]) + nfails.append(nfail) + nshots.append(nshot) + + # Generate experiments until the number of repetitions is large enough (which is + # heuristically determined as + # ``logical error-rate > heuristic_logical_error_lower_bound``). + while (nfails[-1] / nshots[-1]) < heuristic_logical_error_lower_bound: + new_round_number = next_round_number_func(nrounds[-1]) + if new_round_number > maximum_round_number: + break + nrounds.append(new_round_number) + nfail, nshot = simulator(nrounds[-1]) + nfails.append(nfail) + nshots.append(nshot) + + # We do not want to include logical error-rates above + # ``heuristic_logical_error_upper_bound``. + # We go back using smaller steps until we find a last point that is over + # ``heuristic_logical_error_lower_bound`` but under + # ``heuristic_logical_error_upper_bound``. + maximum_number_of_backward_steps: int = 5 + backward_arithmetic_factor: int = int(floor( + (nrounds[-1] - nrounds[-2]) / (maximum_number_of_backward_steps + 1) + )) + while (nfails[-1] / nshots[-1]) > heuristic_logical_error_upper_bound: + out_of_bound_round_value = nrounds[-1] + nrounds, nfails, nshots = nrounds[:-1], nfails[:-1], nshots[:-1] + nrounds.append(out_of_bound_round_value - backward_arithmetic_factor) + nfail, nshot = simulator(nrounds[-1]) + nfails.append(nfail) + nshots.append(nshot) + + return np.asarray(nrounds), np.asarray(nfails), np.asarray(nshots) + def calculate_lep_and_lep_stddev( fails: npt.NDArray[np.int_] | Sequence[int] | int, @@ -156,10 +527,10 @@ def calculate_lep_and_lep_stddev( def calculate_lambda_and_lambda_stddev( - distances: list[int], - lep_per_round: list[float], - lep_stddev_per_round: list[float], -) -> tuple[np.float64, np.float64]: + distances: npt.NDArray[np.int_] | Sequence[int], + lep_per_round: npt.NDArray[np.float64] | Sequence[float], + lep_stddev_per_round: npt.NDArray[np.float64] | Sequence[float], +) -> tuple[float, float]: """ Calculate the error suppression factor (lambda) and its standard deviation. @@ -173,13 +544,14 @@ def calculate_lambda_and_lambda_stddev( threshold and for low code distances. Args: - distances (List[int]): The distances of the code. - lep_per_round (List[float]): The logical error probabilities per round. - lep_stddev_per_round (List[float]): + distances (npt.NDArray[np.int\\_] | Sequence[int]): The distances of the code. + lep_per_round (npt.NDArray[np.float64] | Sequence[float]): + The logical error probabilities per round. + lep_stddev_per_round (npt.NDArray[np.float64] | Sequence[float]): The standard deviation of the logical error probabilities per round. Returns: - Tuple[np.float64, np.float64]: + Tuple[float, float]: A tuple consisting of:: - the exponential error rate suppression factor, Lambda; - the standard deviation of the Lambda value; @@ -192,17 +564,20 @@ def calculate_lambda_and_lambda_stddev( lambda_, lambda_stddev = Analysis.calculate_lambda_and_lambda_stddev( distances=[5, 7, 9], lep_per_round=[1.992e-04, 4.314e-05, 7.556e-06], - lep_stddev_per_round=[1.2e-05, 9.3e-06, 3.9e-06] + lep_stddev_per_round=[1.2e-05, 9.3e-06, 3.9e-06], ) + lambda_, lambda_stddev = res.lambda_, res.lambda_stddev """ query_id = Logging.info_and_generate_uid(locals()) - if min(distances) < 5: - Logging.warn("Lambda unreliable at low code distances." - "Please use distance 5 as a minimum.", query_id) + # Make sure that the inputs are numpy arrays sorted by distance + isort = np.argsort(distances) + distances = np.asarray(distances)[isort] + lep_per_round = np.asarray(lep_per_round)[isort] + lep_stddev_per_round = np.asarray(lep_stddev_per_round)[isort] + if len(distances) < 3: - error = ValueError("Minimum of 3 distances are required to " - "calculate lambda.") + error = ValueError("Minimum of 3 distances are required to calculate lambda.") Logging.error(error, query_id) raise error params, cov = np.polyfit( @@ -213,8 +588,17 @@ def calculate_lambda_and_lambda_stddev( lep_per_round, lep_stddev_per_round)], cov='unscaled' ) - lambda_value = np.exp(-2*params[0]) - lambda_value_stddev = lambda_value*2*np.sqrt(np.diag(cov))[0] + lambda_value = float(np.exp(-2 * params[0])) + lambda_value_stddev = float(lambda_value * 2 * np.sqrt(np.diag(cov))[0]) + + # See Fig. S15 of Supplementary information of "Quantum error correction below the + # surface code threshold" (https://www.nature.com/articles/s41586-024-08449-y#Sec8). + if lambda_value < 1.5 and min(distances) < 5: + Logging.warn( + "Lambda estimation is unreliable at low code distances and low " + "values of lambda. Please use distance 5 as a minimum.", + query_id, + ) return lambda_value, lambda_value_stddev diff --git a/deltakit-explorer/tests/analysis/test_analysis.py b/deltakit-explorer/tests/analysis/test_analysis.py index f28d56ba..68e6499f 100644 --- a/deltakit-explorer/tests/analysis/test_analysis.py +++ b/deltakit-explorer/tests/analysis/test_analysis.py @@ -1,13 +1,178 @@ # (c) Copyright Riverlane 2020-2025. from __future__ import annotations +import itertools +import re +from deltakit_explorer.analysis._analysis import compute_logical_error_per_round import numpy as np import pytest from deltakit_explorer import Logging, analysis -class TestCurveFit: +class TestLEPPerRoundComputation: + @pytest.mark.parametrize( + "leppr,spam_error,is_noisy", + itertools.product( + (1e-5, 1e-4, 1e-3, 1e-2), (1e-5, 1e-4, 1e-3, 1e-2), (False, True) + ), + ) + def test_on_synthetic_inputs(self, leppr: float, spam_error: float, is_noisy: bool): + f_0 = 1 - 2 * spam_error + rounds = np.arange(2, np.ceil(np.log(0.3 / f_0) / np.log(1 - 2 * leppr)), 2) + num_shots = 100_000 + np.zeros_like(rounds) + fidelities = f_0 * (1 - 2 * leppr) ** rounds + lep = (1 - fidelities) / 2 + rng = np.random.default_rng(239845794235) + if is_noisy: + lep *= 1 - 1e-4 * rng.random(lep.size) + nfails = np.rint(num_shots * lep) + + res = compute_logical_error_per_round(nfails, num_shots, rounds) + # Test that the estimated quantities are within 3*sigma of the real one. + assert pytest.approx(res.leppr, abs=3 * res.leppr_stddev) == leppr + assert ( + pytest.approx(res.spam_error, abs=3 * res.spam_error_stddev) == spam_error + ) + assert isinstance(res.leppr, float) + assert isinstance(res.leppr_stddev, float) + assert isinstance(res.spam_error, float) + assert isinstance(res.spam_error_stddev, float) + + @pytest.mark.parametrize( + "rounds", ([0, 1, 2, 3, 4, 3], [-2, 1, 1, 3, 3, 3], [4, 8, 4, 0, 5]) + ) + def test_raises_when_duplicated_round_number(self, rounds: list[int]): + f_0, leppr = 0.999, 0.001 + nprounds = np.asarray(rounds) + num_shots = 100_000 + np.zeros_like(rounds) + fidelities = f_0 * (1 - 2 * leppr) ** nprounds + lep = (1 - fidelities) / 2 + nfails = np.rint(num_shots * lep) + + message = "^Multiple entries were provided for the following number of rounds:" + with pytest.raises(RuntimeError, match=message): + compute_logical_error_per_round(nfails, num_shots, nprounds) + + @pytest.mark.parametrize( + "rounds", ([0, 1, 2, 3, 4], [-1, 4, 5, 7], [8, 4, 0, 5, -1, -348975]) + ) + def test_raises_when_invalid_round_number(self, rounds: list[int]): + f_0, leppr = 0.999, 0.001 + nprounds = np.asarray(rounds) + num_shots = 100_000 + np.zeros_like(rounds) + fidelities = f_0 * (1 - 2 * leppr) ** nprounds + lep = (1 - fidelities) / 2 + nfails = np.rint(num_shots * lep) + + with pytest.warns(UserWarning) as reporter: + compute_logical_error_per_round(nfails, num_shots, nprounds) + # Check that at least the "invalid number of rounds" warning has been raised + # once or more. + pattern = r"Found an invalid number of rounds: -?[0-9]+" + assert any(re.match(pattern, str(warning.message)) for warning in reporter) + + def test_real_world_example(self): + # Note that this test fails when the ``bounds`` optional parameter is set in the + # call to curve_fit. My best guess at the moment is that the optimiser used by + # curve_fit when bounds are provided ("trf") behaves strangely with the + # optimisation problem we give it, whereas the default optimiser without bounds + # ("lm") works nicely. + num_failed_shots = [9949, 8434, 9649, 9926] + num_shots = [50000, 20000, 20000, 20000] + num_rounds = [5, 10, 15, 20] + res = compute_logical_error_per_round(num_failed_shots, num_shots, num_rounds) + + assert pytest.approx(res.leppr, 3 * res.leppr_stddev) == 0.11912 + + def test_raises_when_no_fails(self): + shots = 100_000 + message = "^Got an experiment without any errors.*" + with pytest.raises(RuntimeError, match=message): + compute_logical_error_per_round([0, 0, 0], [shots, shots, shots], [2, 4, 6]) + + def test_raises_when_too_many_fails(self): + shots = 100_000 + message = "^Got estimations of logical error-rates above 0.5.*" + with pytest.raises(RuntimeError, match=message): + compute_logical_error_per_round( + [shots // 2 + 1] * 3, [shots] * 3, [2, 4, 6] + ) + + def test_warn_when_max_lep_is_too_small(self): + shots = 100_000 + message = r"^The maximum estimated logical error-rate \([^\)]+\) is below 0.2.*" + with pytest.warns(UserWarning, match=message): + compute_logical_error_per_round([2460, 4343, 6151], [shots] * 3, [2, 4, 6]) + + def test_warn_when_linear_fit_is_bad(self): + f_0 = 1 - 0.01 + rounds = np.arange(2, 61, 5) + # Non-constant logical error-rate per round that should trigger the R2 check. + leppr = np.array( + [ + 0.00485509, + 0.00606816, + 0.00226491, + 0.00426893, + 0.00082944, + 0.00218146, + 0.00558842, + 0.0088417, + 0.00508088, + 0.00051394, + 0.00762123, + 0.00475807, + ] + ) + num_shots = 100_000 + np.zeros_like(rounds) + fidelities = f_0 * (1 - 2 * leppr) ** rounds + lep = (1 - fidelities) / 2 + nfails = np.rint(num_shots * lep) + + message = r"Got a R2 value of -?[0-9]+\.[0-9]+ < 0.98." + with pytest.warns(UserWarning, match=message): + compute_logical_error_per_round(nfails, num_shots, rounds) + + def test_stddev_reduces_with_more_shots(self): + f_0, leppr = 1 - 0.001, 0.001 + max_round = np.ceil(np.log(0.3 / f_0) / np.log(1 - 2 * leppr)) + rounds = np.arange(2, max_round, 2) + fidelities = f_0 * (1 - 2 * leppr) ** rounds + lep = (1 - fidelities) / 2 + leppr_stddevs = [] + for nshots in [1_000, 10_000, 100_000, 1_000_000]: + num_shots = nshots + np.zeros_like(rounds) + nfails = np.rint(num_shots * lep) + res = compute_logical_error_per_round(nfails, num_shots, rounds) + leppr_stddevs.append(res.leppr_stddev) + + assert leppr_stddevs == sorted(leppr_stddevs, reverse=True) + + @pytest.mark.parametrize("leppr", [5e-5, 1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2]) + def test_single_point_fit(self, leppr: float): + rounds = 30 + num_shots = 100_000 + fidelity = (1 - 2 * leppr) ** rounds + lep = (1 - fidelity) / 2 + nfails = np.rint(num_shots * lep) + + with pytest.warns(UserWarning) as warning_collector: + res = compute_logical_error_per_round([nfails], [num_shots], [rounds]) + expected_message = ( + "^Only one data-point provided for logical error probability per round. " + "Continuing computation assuming that SPAM error is negligible.$" + ) + assert any( + re.match(expected_message, str(w.message)) for w in warning_collector + ), "Expected to get a warning on single data-point being used." + # Test that the estimated quantities are within 3*sigma of the real one. + assert pytest.approx(res.leppr, abs=3 * res.leppr_stddev) == leppr + assert pytest.approx(res.spam_error, abs=3 * res.spam_error_stddev) == 0 + assert isinstance(res.leppr, float) + assert isinstance(res.leppr_stddev, float) + +class TestCurveFit: def test_get_exp_fit_fits(self): # the method is very fragile, as it involves 3 mathematical concepts with # different restrictions. @@ -89,17 +254,29 @@ def test_calculate_lambda_few_distance_raises(self): lep_stddev_per_round=lep_stddev ) - def test_calculate_lambda_warns(self, mocker): + @pytest.mark.parametrize( + "lambd,distances,should_warn", + [ + (1.3, [2, 3, 4], True), + (1.55, [2, 3, 4], False), + (1.3, [5, 7, 9], False), + (1.55, [5, 7, 9], False), + ] + ) + def test_calculate_lambda_warns(self, mocker, lambd, distances, should_warn): mocker.patch("deltakit_explorer._utils._logging.Logging.warn") - distances = [2, 3, 4] - lep = [0.000996, 0.000302, 0.000302] - lep_stddev = [2.0e-09, 6.0e-10, 6.0e-10] + blep, blep_stddev = 0.000996, 2.0e-09 + lep = [blep / lambd**((d - distances[0]) // 2) for d in distances] + lep_stddev = [blep_stddev / lambd**((d - distances[0]) // 2) for d in distances] analysis.calculate_lambda_and_lambda_stddev( distances=distances, lep_per_round=lep, lep_stddev_per_round=lep_stddev ) - Logging.warn.assert_called_once() + if should_warn: + Logging.warn.assert_called_once() + else: + Logging.warn.assert_not_called() Logging.set_log_to_console(False) @@ -127,21 +304,17 @@ def test_calculate_lep_returns_correct_values(self): def test_calculate_lep_returns_correct_values_with_scalars(self): true_lep = 0.1 true_lep_stddev = 0.00948683 # copied from above - lep, lep_stddev = analysis.calculate_lep_and_lep_stddev( - fails=100, - shots=1000 - ) + lep, lep_stddev = analysis.calculate_lep_and_lep_stddev(fails=100, shots=1000) np.testing.assert_allclose(lep, true_lep) np.testing.assert_allclose(lep_stddev, true_lep_stddev, atol=1e-8) class TestGetLambdaFit: - def test_get_lambda_fit_returns_correct_values(self): true_lep_fit = [0.000201, 0.000039, 0.00000758] lep_fit = analysis.get_lambda_fit( distances=[5, 7, 9], lep_per_round=[1.992e-04, 4.314e-05, 7.556e-06], - lep_stddev_per_round=[1.99579718e-05, 9.28881002e-06, 3.88728658e-07] + lep_stddev_per_round=[1.99579718e-05, 9.28881002e-06, 3.88728658e-07], ) assert pytest.approx(lep_fit, rel=0.002) == true_lep_fit diff --git a/mypy.ini b/mypy.ini index 15899f71..b011eddb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -84,3 +84,7 @@ follow_imports = skip ; 1 additional error ignore_missing_imports = true follow_imports = skip + +[mypy-scipy.optimize.*] +ignore_missing_imports = true +follow_imports = skip From ed1b148159ebdceb426e9418db1baeb32bbaf93f Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Mon, 6 Oct 2025 02:32:11 -0700 Subject: [PATCH 10/26] tests: allow mypy to run from top level (#95) * chore: allow mypy to run from top level * chore: fix whitespace * dev: run mypy . in CI * chore: adjust mypy.ini to make mypy happy * dev: add mypy to pre-commit * dev: add scipy-stubs --- .github/workflows/static_code_analysis.yml | 6 +- .pre-commit-config.yaml | 27 +++++++ deltakit-decode/pyproject.toml | 1 + deltakit-decode/ruff.toml | 2 +- deltakit-explorer/pyproject.toml | 2 + .../src/deltakit_explorer/_api/_client.py | 2 +- .../qpu/_noise/_physical_noise.py | 2 +- mypy.ini | 71 +++++-------------- 8 files changed, 57 insertions(+), 56 deletions(-) diff --git a/.github/workflows/static_code_analysis.yml b/.github/workflows/static_code_analysis.yml index 1d5c6833..d63af3c5 100644 --- a/.github/workflows/static_code_analysis.yml +++ b/.github/workflows/static_code_analysis.yml @@ -45,10 +45,14 @@ jobs: run: pixi run ruff format --check if: ${{ always() }} - - name: mypy + - name: mypy-all (checking packages individually) run: pixi run mypy-all if: ${{ always() }} + - name: mypy (checking packages together) + run: pixi run mypy . + if: ${{ always() }} + - name: deptry run: pixi run deptry if: ${{ always() }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c221bdd5..83ab0351 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,6 +26,33 @@ repos: # Run the formatter. - id: ruff-format + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.17.0 + hooks: + - id: mypy + args: + - --config-file=mypy.ini + exclude: 'tests|build|tools|conftest\.py$' + additional_dependencies: + - numpy + - matplotlib + - seaborn + - networkx + - plotly + - pandas + - tqdm + - requests + - gql + - graphql-core + - galois + - types-seaborn + - types-networkx + - pandas-stubs + - types-tqdm + - types-requests + - plotly-stubs + - scipy-stubs + - repo: https://github.com/crate-ci/typos rev: v1.37.1 hooks: diff --git a/deltakit-decode/pyproject.toml b/deltakit-decode/pyproject.toml index 56ee2687..b29f94ae 100644 --- a/deltakit-decode/pyproject.toml +++ b/deltakit-decode/pyproject.toml @@ -46,6 +46,7 @@ dependencies = [ "typing-extensions", "tqdm >=4.66.2", "pandas-stubs", + "plotly-stubs", ] [project.urls] diff --git a/deltakit-decode/ruff.toml b/deltakit-decode/ruff.toml index de0ac27c..1d91e259 100644 --- a/deltakit-decode/ruff.toml +++ b/deltakit-decode/ruff.toml @@ -6,7 +6,7 @@ include = ["src/deltakit_decode/**/*.py", "tests/**/*.py"] [lint] ignore = [ - # deltakit_decode does not follow ruff format, and so long lines exist. This should + # deltakit_decode does not follow ruff format, and so long lines exist. This should # be fixed eventually, but is not urgent. It might be solved very quickly if we # adopt ruff format. # This ignore is purely stylistic, and so should not hide potential bugs. diff --git a/deltakit-explorer/pyproject.toml b/deltakit-explorer/pyproject.toml index 8158c8c3..66cf6970 100644 --- a/deltakit-explorer/pyproject.toml +++ b/deltakit-explorer/pyproject.toml @@ -44,6 +44,8 @@ dependencies = [ "bposd >= 2.1", "pandas >= 2.0", "pandas-stubs", + "types-seaborn", + "scipy-stubs", ] [project.urls] diff --git a/deltakit-explorer/src/deltakit_explorer/_api/_client.py b/deltakit-explorer/src/deltakit_explorer/_api/_client.py index 9574db01..7630c464 100644 --- a/deltakit-explorer/src/deltakit_explorer/_api/_client.py +++ b/deltakit-explorer/src/deltakit_explorer/_api/_client.py @@ -528,7 +528,7 @@ def get_experiment_detectors_and_defect_rates( trimmed_circuit, trimmed_dets = self.trim_circuit_and_detectors( stim_circuit=experiment.noisy_circuit, # detectors are computed above - detectors=experiment.detectors, # type: ignore[arg-type] + detectors=experiment.detectors, ) # get defect rates for trimmed circuit all_qubit_defect_rates = self.defect_rates( diff --git a/deltakit-explorer/src/deltakit_explorer/qpu/_noise/_physical_noise.py b/deltakit-explorer/src/deltakit_explorer/qpu/_noise/_physical_noise.py index ebe984f9..0d90a253 100644 --- a/deltakit-explorer/src/deltakit_explorer/qpu/_noise/_physical_noise.py +++ b/deltakit-explorer/src/deltakit_explorer/qpu/_noise/_physical_noise.py @@ -71,7 +71,7 @@ def _gate_noise(noise_context): continue if len(gate.qubits) == 2: noise_ops.append( - Depolarise2(*gate.qubits, self.p_2_qubit_gate_error) + Depolarise2(*gate.qubits, self.p_2_qubit_gate_error) # type: ignore[call-arg] ) elif isinstance(gate, OneQubitCliffordGate): noise_ops.append( diff --git a/mypy.ini b/mypy.ini index b011eddb..8073d2f3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -6,85 +6,52 @@ allow_redefinition = false implicit_optional = false warn_no_return = true warn_unused_ignores = true +disallow_any_decorated = true +check_untyped_defs = true ; strict option settings that we fail -; 4 errors in docs/conf.py ; 347 errors in deltakit-circuit/tests -exclude = deltakit-circuit/tests|docs/conf.py|build|deltakit-explorer/tests|tools -; Can't apply module-specific exceptions because deltakit-circuits.tests isn't a module +exclude = tests|build|tools|conftest\.py$ -; at least one of the conftest.py files must be excluded for some reason -;exclude = deltakit-circuit/tests/conftest.py - -; 35 errors -;check_untyped_defs = true - -; 315 errors +; 222 errors ;disallow_untyped_defs = true -; 118 errors +; 147 errors ;disallow_incomplete_defs = true -; 112 errors +; 149 errors ;disallow_any_unimported = true +; 70 errors +;implicit_reexport = false + ; 31 errors ; warn_return_any = true -; 159 errors -; implicit_reexport = false - ; 7 errors ;warn_unreachable = true -; 16 errors +; 46 errors ;disallow_any_explicit = true -; 639 errors +; 272 errors ;disallow_any_expr = true -; 57 errors -;disallow_any_decorated = true - -[mypy-stim] -ignore_missing_imports = true -follow_imports = skip - -; can't install types-seaborn because (through a long chain of reasoning) -; it conflicts with code explorer -[mypy-seaborn] -ignore_missing_imports = true - -[mypy-plotly] -ignore_missing_imports = true - -[mypy-plotly.graph_objects] +[mypy-stim] ; doesn't have py.typed file ignore_missing_imports = true [mypy-pymatching] ignore_missing_imports = true -[mypy-deltakit_circuit.*] -; 447 additional errors -disable_error_code = dict-item, misc, arg-type, return-value, union-attr, index -ignore_missing_imports = true -follow_imports = skip - -[mypy-deltakit_decode.*] -; 12 additional errors -disable_error_code = return-value, type-var, arg-type, misc, override, index - [mypy-pathos.*] -; 1 additional error ignore_missing_imports = true -follow_imports = skip -[mypy-progress.*] -; 1 additional error -ignore_missing_imports = true -follow_imports = skip +[mypy-deltakit_explorer.*] ; 423 errors +disable_error_code = var-annotated, arg-type, attr-defined, override, return-value, operator, assignment, index, union-attr, dict-item, list-item, misc, annotation-unchecked -[mypy-scipy.optimize.*] -ignore_missing_imports = true -follow_imports = skip +[mypy-deltakit_circuit.*] ; 30 errors +disable_error_code = dict-item, misc, arg-type, return-value, union-attr, index + +[mypy-deltakit_decode.*] ; 19 errors +disable_error_code = return-value, type-var, arg-type, misc, override, index, name-defined From a475093fd2c0d874546780b6732c5a204b47d042 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Mon, 6 Oct 2025 02:39:48 -0700 Subject: [PATCH 11/26] tests: allow `pytest` to run from top level (#94) * tests: allow pytest to run from top level * chore: remove unnecessary __init__.py files in test folders --- deltakit-decode/tests/__init__.py | 1 - deltakit-explorer/tests/__init__.py | 1 - deltakit-explorer/tests/helpers/__init__.py | 1 - 3 files changed, 3 deletions(-) delete mode 100644 deltakit-decode/tests/__init__.py delete mode 100644 deltakit-explorer/tests/__init__.py delete mode 100644 deltakit-explorer/tests/helpers/__init__.py diff --git a/deltakit-decode/tests/__init__.py b/deltakit-decode/tests/__init__.py deleted file mode 100644 index a46083c7..00000000 --- a/deltakit-decode/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# (c) Copyright Riverlane 2020-2025. diff --git a/deltakit-explorer/tests/__init__.py b/deltakit-explorer/tests/__init__.py deleted file mode 100644 index a46083c7..00000000 --- a/deltakit-explorer/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# (c) Copyright Riverlane 2020-2025. diff --git a/deltakit-explorer/tests/helpers/__init__.py b/deltakit-explorer/tests/helpers/__init__.py deleted file mode 100644 index a46083c7..00000000 --- a/deltakit-explorer/tests/helpers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# (c) Copyright Riverlane 2020-2025. From e90167e438fff2cdb7ea39a0d9d1936faf3eb3f0 Mon Sep 17 00:00:00 2001 From: Adrien Suau <12374487+nelimee@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:01:52 +0100 Subject: [PATCH 12/26] dev: add `mypy_path` to fix pre-commit (#97) --- mypy.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 8073d2f3..d50cdf46 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,6 @@ [mypy] - +; Make sure that mypy is always picking up deltakit* repositories +mypy_path="$MYPY_CONFIG_FILE_DIR/src/:$MYPY_CONFIG_FILE_DIR/deltakit-core/src/:$MYPY_CONFIG_FILE_DIR/deltakit-circuit/src/:$MYPY_CONFIG_FILE_DIR/deltakit-decode/src/:$MYPY_CONFIG_FILE_DIR/deltakit-explorer/src/" ; strict option settings that we pass allow_untyped_globals = false allow_redefinition = false From 91a3d081631bf7c48ae194d63b153cdca6e20c56 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Wed, 8 Oct 2025 01:47:59 -0700 Subject: [PATCH 13/26] DEV: add upper bound on supported Python (#105) * DEV: add upper bound on supported Python * Update pyproject.toml * Update .github/workflows/docs.yml * Update .github/workflows/docs.yml * Update .github/workflows/static_code_analysis.yml * CI: no need to install Python before pixi? * CI: remove unnecessary upper bound? * CI: remove unnecessary upper bound? * CI: update setup-pixi and pixi versions * CI: don't set up Python if not required * CI: avoid unnecessary command? * CI: no need for Python on RTD? * CI: check one last time that we need this constraint * Revert "CI: check one last time that we need this constraint" --- .github/workflows/docs.yml | 9 ++------- .github/workflows/labeled_tests.yml | 12 ++---------- .github/workflows/static_code_analysis.yml | 9 ++------- .readthedocs.yaml | 2 -- pixi.toml | 4 ++-- 5 files changed, 8 insertions(+), 28 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 41e3b514..c656994b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,15 +28,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Set up Pixi - uses: prefix-dev/setup-pixi@v0.8.10 + uses: prefix-dev/setup-pixi@v0.9.1 with: - pixi-version: v0.50.2 + pixi-version: v0.56.0 - name: Install dependencies run: | diff --git a/.github/workflows/labeled_tests.yml b/.github/workflows/labeled_tests.yml index 359f55e9..8215694d 100644 --- a/.github/workflows/labeled_tests.yml +++ b/.github/workflows/labeled_tests.yml @@ -44,18 +44,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version.version }} - - name: Set up Pixi - uses: prefix-dev/setup-pixi@v0.8.10 + uses: prefix-dev/setup-pixi@v0.9.1 with: - pixi-version: v0.49.0 - - - name: Instruct pixi to use specified Python version - run: pixi add python=${{ matrix.python-version.version }} + pixi-version: v0.56.0 - name: Run ${{ matrix.package }} tests run: pixi run -e ${{ matrix.package }}-tests-py${{ matrix.python-version.label }} tests diff --git a/.github/workflows/static_code_analysis.yml b/.github/workflows/static_code_analysis.yml index d63af3c5..b98c024e 100644 --- a/.github/workflows/static_code_analysis.yml +++ b/.github/workflows/static_code_analysis.yml @@ -21,15 +21,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Set up Pixi - uses: prefix-dev/setup-pixi@v0.8.10 + uses: prefix-dev/setup-pixi@v0.9.1 with: - pixi-version: v0.49.0 + pixi-version: v0.56.0 - name: Install dependencies run: pixi install diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 107bffa0..a0ed7da1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,8 +7,6 @@ version: 2 # Set the OS, Python version, and other tools you might need build: os: ubuntu-24.04 - tools: - python: "3.13" commands: - curl -sSf https://pixi.sh/install.sh | sh - /home/docs/.pixi/bin/pixi run sphinx-build -T -W -b html -d _build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html diff --git a/pixi.toml b/pixi.toml index 3d18ab8a..7c096334 100644 --- a/pixi.toml +++ b/pixi.toml @@ -116,7 +116,7 @@ cmd = "isort ." [dependencies] pandoc = ">=3.7.0.2,<4" -python = ">=3.10" +python = ">=3.10,<3.14" actionlint = ">=1.7" pixi-pycharm = ">=0.0.8,<0.0.9" @@ -128,7 +128,7 @@ deltakit-explorer = { path = "./deltakit-explorer/", editable = true } deltakit = { path = "./", editable = true, extras=["dev"] } [feature.python.dependencies] -python = ">=3.10" +python = ">=3.10,<3.14" [feature.py310.dependencies] python = "3.10.*" From 662cd7e9cead90aa722693caea995b63d2659d73 Mon Sep 17 00:00:00 2001 From: Arindam Singh <98768062+Arindam2407@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:21:47 +0100 Subject: [PATCH 14/26] Fixed token setting tests by changing 403 to 401 (#104) * Fixed token setting tests by changing 403 to 401 * Remove Whitespace * Added Requested Comment * Removed unnecessary file --- .../tests/server/test_api_v2_client.py | 4 ++-- deltakit-explorer/tests/server/test_token.py | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/deltakit-explorer/tests/server/test_api_v2_client.py b/deltakit-explorer/tests/server/test_api_v2_client.py index 1a739366..6411c4e1 100644 --- a/deltakit-explorer/tests/server/test_api_v2_client.py +++ b/deltakit-explorer/tests/server/test_api_v2_client.py @@ -184,9 +184,9 @@ def test_get_job_status_other_failure(self, mocker): url = "https://unknown/url" client = APIv2Client(url) mocker.patch.object( - client._request_session, "get", return_value=FakeResponse(403) + client._request_session, "get", return_value=FakeResponse(401) ) - with pytest.raises(ServerException, match=r"\[403\] BODY text"): + with pytest.raises(ServerException, match=r"\[401\] BODY text"): client._get_job_status("123") diff --git a/deltakit-explorer/tests/server/test_token.py b/deltakit-explorer/tests/server/test_token.py index bdb1a09c..e80c9956 100644 --- a/deltakit-explorer/tests/server/test_token.py +++ b/deltakit-explorer/tests/server/test_token.py @@ -30,9 +30,10 @@ def test_set_token_raises_on_connection(self, api_version, mocker): def test_set_token_raises_on_server_v1_error(self): old_server = os.environ.pop(utils.DELTAKIT_SERVER_URL_ENV, default="") - os.environ[utils.DELTAKIT_SERVER_URL_ENV] = "https://riverlane.com/" - with pytest.raises(ServerException, match="^Token failed validation: Status 403"): - GQLClient("https://riverlane.com/").set_token("abc", validate=True) + os.environ[utils.DELTAKIT_SERVER_URL_ENV] = "https://deltakit.riverlane.com/proxy" + with pytest.raises(ServerException, match="^Token failed validation: Status 401"): + GQLClient("https://deltakit.riverlane.com/proxy").set_token("abcdefghijklmnopqrstuvwxyzabcdef", validate=True) + # Token should be a 32-character string if old_server: os.environ[utils.DELTAKIT_SERVER_URL_ENV] = old_server @@ -74,15 +75,13 @@ def test_set_token_works_with_404_on_v2(self, mocker): Client.set_token(token, validate=True) assert _auth.get_token() == token - def test_set_token_v2_fails_on_403(self, mocker): + def test_set_token_v2_fails_on_401(self, mocker): client = Client("https://localhost/", api_version=2) mocker.patch( "deltakit_explorer._api._client.Client.get_instance", return_value=client, ) - mocker.patch.object(client._api._request_session, "get", return_value=FakeResponse(403)) + mocker.patch.object(client._api._request_session, "get", return_value=FakeResponse(401)) Path.unlink(utils.get_config_file_path()) - randint = random.randint(1000, 9999) - token = f"abc-{randint}" with pytest.raises(ServerException): - Client.set_token(token, validate=True) + Client.set_token("abcdefghijklmnopqrstuvwxyzabcdef", validate=True) # Token should be a 32-character string From 479c1eba25a5ca4b21d6f7effbda0b60991dcef1 Mon Sep 17 00:00:00 2001 From: Zhiyao Li <40032538+zhiyaol@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:42:16 -0700 Subject: [PATCH 15/26] docs: guidance on how conda users can use pixi without conda/pixi interaction issues (#103) --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f547a8bf..4f35f3ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,6 +90,9 @@ Depending on system settings, you may experience a `Too many open files (os erro error. This is [known issue](https://github.com/prefix-dev/pixi/issues/2626) that can easily be resolved by increasing the maximum number of open file descriptors; e.g., `ulimit -n 512`. ``` +```{dropdown} Conda users... +We suggest deactivating any `conda` environments before using `pixi`. If the `conda` `base` environment activates by default whenever a terminal session is opened, you can turn it off with `conda config --set auto_activate_base false`. +``` `pixi shell` activates a development virtual environment with editable installs of Deltakit packages so you can make changes and interact with the modified code. This environment From d817092f73f2fc5ca97d4c49dbe602bb9d9a5bbd Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Thu, 9 Oct 2025 10:33:02 -0700 Subject: [PATCH 16/26] dev: add support for package managers other than `pixi` (#92) * dev: add conda environment.yml * dev: add poetry dependencies * docs: add hatch support * dev: add uv support * docs: document alternative package managers * Revert "docs: document alternative package managers" This reverts commit f9a3a06ed23afb04c1d56dda66aaacc153cf753c. * Revert "dev: add uv support" This reverts commit f966fe46c4ad927f8a11e52eda827b365271e9fb. * Revert "docs: add hatch support" This reverts commit a940ead3ec9f35b60ba8dbdeca81349e771c9a40. * Revert "Revert "docs: add hatch support"" This reverts commit b9d5178720a18fcea5c2440730f4c6fc46ddefa3. * Revert "Revert "docs: document alternative package managers"" This reverts commit 3417f0bb8f01ca21375f7e4cfe63f6d27bd30d23. * docs: fix dropdown/tabs * dev: remove pixi-pycharm from environment.yml * dev: add uv support * dev: make uv install component packages as editable * dev: make hatch install component packages as editable * chore: ensure Python <3.14 for conda and poetry * docs: add whitespace --------- Co-authored-by: Adrien Suau <12374487+nelimee@users.noreply.github.com> --- .gitignore | 6 ++-- CONTRIBUTING.md | 88 +++++++++++++++++++++++++++++++++++++++++++++---- environment.yml | 15 +++++++++ pyproject.toml | 30 +++++++++++++++++ 4 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 environment.yml diff --git a/.gitignore b/.gitignore index 5f1dedc6..c865c10f 100644 --- a/.gitignore +++ b/.gitignore @@ -98,15 +98,15 @@ ipython_config.py # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. -#uv.lock +uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock -#poetry.toml +poetry.lock +poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f35f3ce..0b1b0d91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,23 +80,25 @@ environment and run tasks. After cloning the repository and [installing `pixi`](https://pixi.sh/latest/), navigate to the root directory of the repository in a terminal and install dependencies with `pixi install`. -You can then activate the Python environment with `pixi shell`. +`pixi shell` activates a development virtual environment with editable installs of Deltakit +packages so you can make changes and interact with the modified code. To deactivate the environment, run `exit`. -To set up the Python interpreter in VS Code, you can set the `python.defaultInterpreterPath` -variable to `"${workspaceFolder}/.pixi/envs/default/bin/python"` in `settings.json`. ```{dropdown} Linux/macOS users... Depending on system settings, you may experience a `Too many open files (os error 24) at path...` error. This is [known issue](https://github.com/prefix-dev/pixi/issues/2626) that can easily be resolved by increasing the maximum number of open file descriptors; e.g., `ulimit -n 512`. ``` + ```{dropdown} Conda users... We suggest deactivating any `conda` environments before using `pixi`. If the `conda` `base` environment activates by default whenever a terminal session is opened, you can turn it off with `conda config --set auto_activate_base false`. ``` -`pixi shell` activates a development virtual environment with editable installs of Deltakit -packages so you can make changes and interact with the modified code. This environment -is also available in [several popular IDEs](https://pixi.sh/dev/integration/editor/vscode/). +```{dropdown} IDE users... +This environment is also available in [several popular IDEs](https://pixi.sh/dev/integration/editor/vscode/). +For instance, to set up the Python interpreter in VS Code, you can set the `python.defaultInterpreterPath` +variable to `"${workspaceFolder}/.pixi/envs/default/bin/python"` in `settings.json`. +``` You can also perform important tasks with `pixi run`. For example: @@ -134,6 +136,80 @@ package. To enable `pre-commit`, run `pre-commit install` within a `pixi shell` (or `pixi run pre-commit install` otherwise). +:::::{dropdown} Conda / Mamba / Poetry / Hatch / uv users... +You are welcome to manage your development environment using tools other than `pixi`. + +::::{tab-set} +:::{tab-item} Conda / Mamba +:sync: tab1 + +```bash +# Create the virtual environment +conda env create -f environment.yml + +# Activate the environment +conda activate deltakit-conda + +# Run tests: +pytest +``` +::: + +:::{tab-item} Poetry +:sync: tab2 +```bash +# Install deltakit and developer dependencies +poetry install --extras "dev" + +# Run tests: +poetry run pytest +``` +As shown, `pytest` is run outside of a poetry shell, hence the need for the +`poetry run` prefix. If you have the `poetry shell` extension and activate it, +the prefix is not needed. +::: + +:::{tab-item} Hatch +:sync: tab3 +```bash +# Create environment +hatch env create + +# Activate shell +hatch shell + +# Run tests: +pytest +``` +::: + +:::{tab-item} uv +:sync: tab4 +```bash +# Install dependencies +uv sync --extra dev + +# Run tests: +uv run pytest +``` +As shown, `pytest` is run outside of a virtual environment, hence the need for the +`uv run` prefix. If you use virtual environments with `uv` and activate the new +virtual environment, the prefix is not needed. +::: + +:::: + +There is not a designated task runner for use with tools other than `pixi`. Common +commands besides `pytest` shown above are `ruff check` for linting, `typos` +to find typos, `mypy` for static type checking, and `pre-commit run -a` +to run several pre-commit checks. For additional commands, see the `[tasks]` +section of [`pixi.ini`](https://github.com/Deltakit/deltakit/blob/main/pixi.toml). + +Deltakit itself is compatible with Python 3.10+, but some of its dependencies currently +require Python <3.14. If you run into difficulties associated with a Python version, +consider configuring your package manager to use an earlier version of Python. +::::: + ### Code of Conduct When contributing, always follow our [code of conduct](CODE_OF_CONDUCT.md). diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..9898652a --- /dev/null +++ b/environment.yml @@ -0,0 +1,15 @@ +name: deltakit-conda +channels: +- conda-forge +- nodefaults +dependencies: +- pandoc >=3.7.0.2,<4 +- python >=3.10,<3.14 +- actionlint >=1.7 +- pip +- pip: + - -e ./deltakit-core/ + - -e ./deltakit-circuit/ + - -e ./deltakit-decode/ + - -e ./deltakit-explorer/ + - -e ./[dev] diff --git a/pyproject.toml b/pyproject.toml index ba62dc88..713ddbb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -212,3 +212,33 @@ mode = "update" [tool.semantic_release.commit_parser_options] other_allowed_tags = ["build", "chore", "ci", "docs", "style", "refactor", "tests", "release", "dev"] allowed_tags = ["feat", "fix", "perf", "build", "chore", "ci", "docs", "style", "refactor", "tests", "release", "dev"] + +[tool.poetry.dependencies] +python = ">=3.10,<3.14" +deltakit-core = { path = "./deltakit-core/", develop = true } +deltakit-circuit = { path = "./deltakit-circuit/", develop = true } +deltakit-decode = { path = "./deltakit-decode/", develop = true } +deltakit-explorer = { path = "./deltakit-explorer/", develop = true } + +[tool.hatch.envs.default] +pre-install-commands = [ + "pip install -e ./deltakit-core", + "pip install -e ./deltakit-circuit", + "pip install -e ./deltakit-decode", + "pip install -e ./deltakit-explorer", +] +features = ["dev"] + +[tool.uv.workspace] +members = [ + "deltakit-core", + "deltakit-circuit", + "deltakit-decode", + "deltakit-explorer", +] + +[tool.uv.sources] +deltakit-core = { workspace = true } +deltakit-circuit = { workspace = true } +deltakit-decode = { workspace = true } +deltakit-explorer = { workspace = true } From d24e4cdf67371717b730e53c39199bed18cd5d33 Mon Sep 17 00:00:00 2001 From: Zhiyao Li <40032538+zhiyaol@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:36:36 -0700 Subject: [PATCH 17/26] docs: add download link to notebooks (#111) --- docs/examples/notebooks/demo/1. setup.ipynb | 2 ++ docs/examples/notebooks/demo/2. quick start.ipynb | 2 ++ .../notebooks/demo/3. experiment step by step.ipynb | 2 ++ docs/examples/notebooks/demo/4. advanced demo.ipynb | 8 ++++++++ docs/examples/notebooks/demo/solutions.ipynb | 2 ++ .../notebooks/google_qmem_experiment/decode.ipynb | 12 +++++++++++- .../google_qmem_experiment/noise_analysis.ipynb | 6 +++++- .../qmem/rotated_planar_quantum_memory_plots.ipynb | 8 ++++++++ docs/examples/notebooks/simulation/ac_decoding.ipynb | 2 ++ .../simulation/leakage-aware_decoding.ipynb | 7 +++++++ .../examples/notebooks/simulation/leakage_demo.ipynb | 3 +++ .../notebooks/simulation/server_simulation.ipynb | 2 ++ .../notebooks/simulation/shot_analysis.ipynb | 2 ++ .../notebooks/simulation/stim_simulation.ipynb | 2 ++ docs/examples/notebooks/template_notebook.ipynb | 8 ++++++++ docs/guide/adding_noise.md | 2 ++ docs/guide/analysis.ipynb | 2 ++ docs/guide/authentication.md | 2 ++ docs/guide/circuit_generation.md | 2 ++ docs/guide/decoding.md | 2 ++ docs/guide/getting_started.md | 2 ++ docs/guide/simulation.md | 2 ++ 22 files changed, 80 insertions(+), 2 deletions(-) diff --git a/docs/examples/notebooks/demo/1. setup.ipynb b/docs/examples/notebooks/demo/1. setup.ipynb index 661af362..b4973457 100644 --- a/docs/examples/notebooks/demo/1. setup.ipynb +++ b/docs/examples/notebooks/demo/1. setup.ipynb @@ -7,6 +7,8 @@ "source": [ "# Setting up everything\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.<1. setup.ipynb>`*\n", + "\n", "First of all, make sure you have the python library installed.\n", "\n", "If you are an external user, you will have received a python wheel file. If you are an internal user, please download this wheel (`.whl` file) [from the latest release](https://github.com/riverlane/deltakit/releases/latest).\n", diff --git a/docs/examples/notebooks/demo/2. quick start.ipynb b/docs/examples/notebooks/demo/2. quick start.ipynb index 483efaf2..52942373 100644 --- a/docs/examples/notebooks/demo/2. quick start.ipynb +++ b/docs/examples/notebooks/demo/2. quick start.ipynb @@ -7,6 +7,8 @@ "source": [ "# QEC in one glance\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.<2. quick start.ipynb>`*\n", + "\n", "This notebook will demo you the essence of QEC experiment in a bird's eye view.\n", "\n", "If you don't have this notebook on your drive, please download it and put in the same folder you are already using for venv and jupyter. Done? Let's go!\n", diff --git a/docs/examples/notebooks/demo/3. experiment step by step.ipynb b/docs/examples/notebooks/demo/3. experiment step by step.ipynb index 3fbabf16..d97bfde1 100644 --- a/docs/examples/notebooks/demo/3. experiment step by step.ipynb +++ b/docs/examples/notebooks/demo/3. experiment step by step.ipynb @@ -11,6 +11,8 @@ "source": [ "# Step-by-step through the experiment\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.<3. experiment step by step.ipynb>`*\n", + "\n", "## 1. Circuit generation" ] }, diff --git a/docs/examples/notebooks/demo/4. advanced demo.ipynb b/docs/examples/notebooks/demo/4. advanced demo.ipynb index 8e8c9c9d..fe8f945d 100644 --- a/docs/examples/notebooks/demo/4. advanced demo.ipynb +++ b/docs/examples/notebooks/demo/4. advanced demo.ipynb @@ -8,6 +8,14 @@ "# Realistic experiment" ] }, + { + "cell_type": "markdown", + "id": "d3e17440", + "metadata": {}, + "source": [ + "*Want to follow along? {nb-download}`Download this notebook.<4. advanced demo.ipynb>`*" + ] + }, { "cell_type": "code", "execution_count": 1, diff --git a/docs/examples/notebooks/demo/solutions.ipynb b/docs/examples/notebooks/demo/solutions.ipynb index e359bee8..b68011f0 100644 --- a/docs/examples/notebooks/demo/solutions.ipynb +++ b/docs/examples/notebooks/demo/solutions.ipynb @@ -7,6 +7,8 @@ "source": [ "## Exercise #1\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.`*\n", + "\n", "We decoder one particular experiment, and every time you run it, you should have similar numbers.\n", "What we are often interested in is HOW logical error probability respond to different parameter tweaking.\n", "\n", diff --git a/docs/examples/notebooks/google_qmem_experiment/decode.ipynb b/docs/examples/notebooks/google_qmem_experiment/decode.ipynb index bc91d7c0..4ee4728b 100644 --- a/docs/examples/notebooks/google_qmem_experiment/decode.ipynb +++ b/docs/examples/notebooks/google_qmem_experiment/decode.ipynb @@ -8,6 +8,14 @@ "# Decoding a quantum memory experiment" ] }, + { + "cell_type": "markdown", + "id": "4be5a892", + "metadata": {}, + "source": [ + "*Want to follow along? {nb-download}`Download this notebook.`*" + ] + }, { "cell_type": "markdown", "id": "75dff611", @@ -114,7 +122,9 @@ "id": "fab883be", "metadata": {}, "outputs": [], - "source": "! if [ ! -e \"../../data/examples/google_qmem_experiment/input\" ]; then ../../scripts/get_google_data.sh ../../data ; fi" + "source": [ + "! if [ ! -e \"../../data/examples/google_qmem_experiment/input\" ]; then ../../scripts/get_google_data.sh ../../data ; fi" + ] }, { "cell_type": "markdown", diff --git a/docs/examples/notebooks/google_qmem_experiment/noise_analysis.ipynb b/docs/examples/notebooks/google_qmem_experiment/noise_analysis.ipynb index 0a1f20c6..3f341363 100644 --- a/docs/examples/notebooks/google_qmem_experiment/noise_analysis.ipynb +++ b/docs/examples/notebooks/google_qmem_experiment/noise_analysis.ipynb @@ -6,6 +6,8 @@ "source": [ "# Noise analysis for a quantum memory experiment\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.`*\n", + "\n", "In this notebook we demonstrate all the steps required to perform noise source analysis for a quantum error correction experiment. Specifically, we will be using data from a quantum memory experiment published by Google ([Google Quantum AI, _Nature 614, 676-681_ (2023)](https://www.nature.com/articles/s41586-022-05434-1)) and calculate defect rates and correlation matrices. In the [Decoding a quantum memory experiment](decode.ipynb) example notebook, we decoded this data to obtain logical error probabilities for different experiment scenarios. In this notebook, we will explore the experiment data further, generating insights into the errors that are occurring on the quantum computer during the experiment.\n", "\n", "This notebook covers the following steps: \n", @@ -75,7 +77,9 @@ "execution_count": 3, "metadata": {}, "outputs": [], - "source": "! if [ ! -e \"../../data/examples/google_qmem_experiment/input\" ]; then ../../scripts/get_google_data.sh ../../data ; fi" + "source": [ + "! if [ ! -e \"../../data/examples/google_qmem_experiment/input\" ]; then ../../scripts/get_google_data.sh ../../data ; fi" + ] }, { "cell_type": "markdown", diff --git a/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb b/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb index ba630b42..8566e3e5 100644 --- a/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb +++ b/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb @@ -1,5 +1,13 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "0675505d", + "metadata": {}, + "source": [ + "*Want to follow along? {nb-download}`Download this notebook.`*" + ] + }, { "cell_type": "code", "execution_count": 1, diff --git a/docs/examples/notebooks/simulation/ac_decoding.ipynb b/docs/examples/notebooks/simulation/ac_decoding.ipynb index 8b36ad40..7a91c022 100644 --- a/docs/examples/notebooks/simulation/ac_decoding.ipynb +++ b/docs/examples/notebooks/simulation/ac_decoding.ipynb @@ -7,6 +7,8 @@ "source": [ "# Decoding with Ambiguity Clustering\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.`*\n", + "\n", "In this notebook, we demonstrate decoding with Riverlane's proprietary decoder: the [Ambiguity Clustering (AC) decoder](https://www.nature.com/articles/s41586-024-07107-7). In particular, we\n", "\n", "* generate a circuit for a surface code quantum memory experiment, define a noise model, and generate measurement and detector data from the circuit with the noise model applied;\n", diff --git a/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb b/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb index 471b5cda..d22ac6fb 100644 --- a/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb +++ b/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb @@ -7,6 +7,13 @@ "# Leakage-Aware Decoding with the Local Clustering Decoder" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*Want to follow along? {nb-download}`Download this notebook.`*" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/docs/examples/notebooks/simulation/leakage_demo.ipynb b/docs/examples/notebooks/simulation/leakage_demo.ipynb index d2d5085e..579ceac3 100644 --- a/docs/examples/notebooks/simulation/leakage_demo.ipynb +++ b/docs/examples/notebooks/simulation/leakage_demo.ipynb @@ -5,6 +5,9 @@ "metadata": {}, "source": [ "# Leakage-Aware Decoding with LCD Demo\n", + "\n", + "*Want to follow along? {nb-download}`Download this notebook.`*\n", + "\n", "In this notebook, we briefly demonstrate Leakage functionality. In particular, we define a leakage noise model and add this to a generated surface code circuit. We then perform decoding with Riverlane's [Local Clustering Decoder (LCD)](https://arxiv.org/pdf/2411.10343), benchmarking leakage-aware decoding against leakage-unaware decoding.\n", "## Necessary imports" ] diff --git a/docs/examples/notebooks/simulation/server_simulation.ipynb b/docs/examples/notebooks/simulation/server_simulation.ipynb index 80a5c7da..372eeab9 100644 --- a/docs/examples/notebooks/simulation/server_simulation.ipynb +++ b/docs/examples/notebooks/simulation/server_simulation.ipynb @@ -7,6 +7,8 @@ "source": [ "# Simulation of STIM-like circuits\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.`*\n", + "\n", "Deltakit provides both client-side and server-side simulation capabilities.\n", "For typical simulations involving only features provided by STIM, simulation can be performed locally; this has the advantage of avoiding data transfer and network latency for small circuits.\n", "If the simulation involves advanced features, such as leakage support, simulations will be performed on a Deltakit server." diff --git a/docs/examples/notebooks/simulation/shot_analysis.ipynb b/docs/examples/notebooks/simulation/shot_analysis.ipynb index 9fa8a662..31131089 100644 --- a/docs/examples/notebooks/simulation/shot_analysis.ipynb +++ b/docs/examples/notebooks/simulation/shot_analysis.ipynb @@ -7,6 +7,8 @@ "source": [ "# Analysis of Per-shot Decoding\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.`*\n", + "\n", "In this notebook, we show how you may analyse decoding performance on a per-shot basis. Firstly, we will simulate a quantum memory experiment with the repetition code.\n", "Secondly, we will extract a particular shot which led to a decoder failure. And lastly, we will visualise detectors which lit up in this failure event." ] diff --git a/docs/examples/notebooks/simulation/stim_simulation.ipynb b/docs/examples/notebooks/simulation/stim_simulation.ipynb index 4a295151..2ff82631 100644 --- a/docs/examples/notebooks/simulation/stim_simulation.ipynb +++ b/docs/examples/notebooks/simulation/stim_simulation.ipynb @@ -7,6 +7,8 @@ "source": [ "# Simulation and decoding of a quantum error correction experiment\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.`*\n", + "\n", "In this notebook, we simulate a quantum error correction experiment and demonstrate all the steps required to decode the data produced. In particular, we:\n", "\n", "* generate a circuit for a repetition code quantum memory experiment;\n", diff --git a/docs/examples/notebooks/template_notebook.ipynb b/docs/examples/notebooks/template_notebook.ipynb index dafb03b6..422998f3 100644 --- a/docs/examples/notebooks/template_notebook.ipynb +++ b/docs/examples/notebooks/template_notebook.ipynb @@ -1,5 +1,13 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "e9d1bf94", + "metadata": {}, + "source": [ + "*Want to follow along? {nb-download}`Download this notebook.`*" + ] + }, { "cell_type": "code", "execution_count": 1, diff --git a/docs/guide/adding_noise.md b/docs/guide/adding_noise.md index fb718796..246bc88b 100644 --- a/docs/guide/adding_noise.md +++ b/docs/guide/adding_noise.md @@ -14,6 +14,8 @@ kernelspec: # Adding Noise +*Want to follow along? {download}`Download this notebook.`* + ## Motivation The goal of Quantum Error Correction is to correct errors, which are caused by different noise mechanisms. diff --git a/docs/guide/analysis.ipynb b/docs/guide/analysis.ipynb index e8e69fa3..f3ab005e 100644 --- a/docs/guide/analysis.ipynb +++ b/docs/guide/analysis.ipynb @@ -7,6 +7,8 @@ "source": [ "# Quantum Error Correction Analysis Tools\n", "\n", + "*Want to follow along? {nb-download}`Download this notebook.`*\n", + "\n", "This page demonstrates how to analyse results from quantum error correction experiments using `Deltakit`. We will start from high-level experiment results and drill down to shot analysis.\n", "\n", "Let's start with some common code that we will use throughout the examples.\n", diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md index 466c1449..4eae6953 100644 --- a/docs/guide/authentication.md +++ b/docs/guide/authentication.md @@ -1,5 +1,7 @@ # Authentication +*Want to follow along? {download}`Download this notebook.`* + This document provides information on authentication aspects. When you install Deltakit, you install a family of Python packages. While most features are available immediately after installation, diff --git a/docs/guide/circuit_generation.md b/docs/guide/circuit_generation.md index ce6bd602..dd81d6f7 100644 --- a/docs/guide/circuit_generation.md +++ b/docs/guide/circuit_generation.md @@ -14,6 +14,8 @@ kernelspec: # QEC Experiment Circuit Generation +*Want to follow along? {download}`Download this notebook.`* + Compilation of error-corrected quantum programs is a crucial task. With advances in codes, decoders, and control systems, we know more and more about how quantum algorithms designed for perfect qubits should be translated into diff --git a/docs/guide/decoding.md b/docs/guide/decoding.md index 8179de51..dd429c43 100644 --- a/docs/guide/decoding.md +++ b/docs/guide/decoding.md @@ -14,6 +14,8 @@ kernelspec: # Decoding +*Want to follow along? {download}`Download this notebook.`* + In this guide, you will learn how to run a decoder on measurement results from a QEC experiment. To do this, you'll need to follow three steps: 1. Convert the raw measurement results into detector events and diff --git a/docs/guide/getting_started.md b/docs/guide/getting_started.md index 0c712d1d..f58fb49a 100644 --- a/docs/guide/getting_started.md +++ b/docs/guide/getting_started.md @@ -14,6 +14,8 @@ kernelspec: # Getting Started +*Want to follow along? {download}`Download this notebook.`* + Performing a QEC experiment with Deltakit typically involves four steps. 1. In the *circuit generation* step, you generate a quantum circuit to implement the experiment diff --git a/docs/guide/simulation.md b/docs/guide/simulation.md index 4b1bfd49..70e49559 100644 --- a/docs/guide/simulation.md +++ b/docs/guide/simulation.md @@ -14,6 +14,8 @@ kernelspec: # Simulation +*Want to follow along? {download}`Download this notebook.`* + ## 1. Simulation of Measurements Decoding a quantum error correction experiment requires measurement data. From 4c8f8b198da453e6136e832816822c243c6701ab Mon Sep 17 00:00:00 2001 From: Amirreza Safehian Date: Wed, 15 Oct 2025 14:25:14 +0100 Subject: [PATCH 18/26] feat(gtm): add google tag manager (#101) --- docs/_templates/layout.html | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/_templates/layout.html diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000..fc6b35bd --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,32 @@ + + +{% extends "!layout.html" %} {% block extrahead %} {{ super() }} + + + + +{% endblock %} {% block footer %} + + + +{{ super() }} {% endblock %} From 9fa9bfdb81b708b682d7549cc006af3c440dd17c Mon Sep 17 00:00:00 2001 From: Adrien Suau <12374487+nelimee@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:01:31 +0100 Subject: [PATCH 19/26] Change default CODEOWNER from @mdhaber to @nelimee (#115) Changing default code owner. --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index b839be0e..98922f9d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,7 +2,7 @@ # the repo. Unless a later match takes precedence, # @mdhaber will be requested for review when someone opens # a pull request. -* @mdhaber +* @nelimee # @KentonRL owns any file in deltakit-decode # directory anywhere in the repository. From 025465f414855817b894d8c5af030701ca8f79e1 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Fri, 31 Oct 2025 03:34:48 -0700 Subject: [PATCH 20/26] chore: add en-gb locale to typos (#70) * chore: add en-gb locale to typos * favorite -> favourite * artifact -> artefact * Allow Authorization due to its use for web-related fields * Exclude any license from typos check * Fix "depolarization" and ignore "color" * Fix remaining typos * neighbours -> neighbors for property * Fix tests --------- Co-authored-by: Adrien Suau <12374487+nelimee@users.noreply.github.com> --- CODE_OF_CONDUCT.md | 24 +++++++++---------- CONTRIBUTING.md | 6 ++--- SECURITY.md | 2 +- .../decoding_graphs/_decoding_graph.py | 8 +++---- .../decoding_graphs/_decoding_graph_tools.py | 2 +- .../test_decoding_hyper_multigraph.py | 2 +- .../test_decoding_hypergraph.py | 10 ++++---- .../test_nx_decoding_graphs.py | 10 ++++---- .../analysis/_decoder_manager.py | 6 ++--- .../utils/_decoding_graph_visualiser.py | 4 ++-- .../noise_sources/test_stim_noise_sources.py | 2 +- .../src/deltakit_explorer/_api/_api_client.py | 4 ++-- .../deltakit_explorer/analysis/_analysis.py | 2 +- .../_css/_css_code_experiment_circuit.py | 2 +- .../codes/_planar_code/__init__.py | 2 +- .../codes/_schedules/__init__.py | 2 +- .../qpu/_circuits/__init__.py | 2 +- .../deltakit_explorer/qpu/_noise/__init__.py | 2 +- .../src/deltakit_explorer/types/_types.py | 4 ++-- .../unit/circuits/test_tableau_functions.py | 2 +- .../noise_models/test_noise_parameters.py | 4 ++-- .../rotated_planar_quantum_memory_plots.ipynb | 2 +- .../simulation/leakage-aware_decoding.ipynb | 2 +- docs/guide/adding_noise.md | 2 +- docs/guide/analysis.ipynb | 4 ++-- docs/guide/getting_started.md | 10 ++++---- docs/release.md | 4 ++-- docs/setup.md | 2 +- pyproject.toml | 17 ++++++++++++- 29 files changed, 80 insertions(+), 65 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 0789a862..29a0c517 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,20 +14,20 @@ diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to a positive environment for our +Examples of behaviour that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +* Accepting responsibility and apologising to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community -Examples of unacceptable behavior include: +Examples of unacceptable behaviour include: -* The use of sexualized language or imagery, and sexual attention or +* The use of sexualised language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment @@ -39,8 +39,8 @@ Examples of unacceptable behavior include: ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, +acceptable behaviour and will take appropriate and fair corrective action in +response to any behaviour that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject @@ -58,7 +58,7 @@ representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be +Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported to the community leaders responsible for enforcement at deltakit@riverlane.com. All complaints will be reviewed and investigated promptly and fairly. @@ -73,19 +73,19 @@ the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction -**Community Impact**: Use of inappropriate language or other behavior deemed +**Community Impact**: Use of inappropriate language or other behaviour deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. +behaviour was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. -**Consequence**: A warning with consequences for continued behavior. No +**Consequence**: A warning with consequences for continued behaviour. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels @@ -95,7 +95,7 @@ permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. +sustained inappropriate behaviour. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behaviour, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b1b0d91..50065d81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ writing code!) from anyone. ## Types of Contributions ### Issues #### Bug reports -We define a "bug" as a discrepancy between documented and actual behavior or +We define a "bug" as a discrepancy between documented and actual behaviour or an *inaccurate* error message. (If it's not a "bug", see {ref}`contributing-enhancement-requests`.) First, check to see if the bug has already been reported on the [issue tracker](https://github.com/Deltakit/deltakit/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug). @@ -26,7 +26,7 @@ If so, leave a comment; if not, create a #### Enhancement Requests Other requests (besides bug reports) are also welcome! For instance, if the documentation needs improvement, if you disagree with -documented behavior, or if you are asking for a new feature, we'd appreciate +documented behaviour, or if you are asking for a new feature, we'd appreciate your thoughts. First, check to see if a similar request already has an [open issue](https://github.com/Deltakit/deltakit/issues?q=is%3Aissue%20state%3Aopen%20label%3Arequest). @@ -230,7 +230,7 @@ specification with the following exceptions: - Use `request` as the prefix for a request; more descriptive prefixes (`feat`/`perf`) are used for PRs/commits. -Other commit *types* that we recognize include `release` (for work relating to release tooling +Other commit *types* that we recognise include `release` (for work relating to release tooling and/or releases) and `dev` (for development work that doesn't fit in another category), but typically these types will only be used by package maintainers. diff --git a/SECURITY.md b/SECURITY.md index 2b86cf9b..8ef77378 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,7 +3,7 @@ Deltakit takes security seriously. If you discover a potential vulnerability, please report it privately using the [Report a Vulnerability](https://github.com/Deltakit/deltakit/security) feature on -our repository. To minimize risk while we investigate the issue, please do *not* +our repository. To minimise risk while we investigate the issue, please do *not* discuss the concern in a GitHub issue, pull request, or other public forum. In your private report, please include: diff --git a/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph.py b/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph.py index 9298f66e..d1902507 100644 --- a/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph.py +++ b/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph.py @@ -93,7 +93,7 @@ def incident_edges(self, detector: int) -> Iterator[AnyEdgeT]: @abstractmethod def neighbors(self, detector: int) -> Iterator[int]: - """Iterator for the neighbors of a given detector.""" + """Iterator for the neighbours of a given detector.""" @property @abstractmethod @@ -107,7 +107,7 @@ def detector_records(self) -> Mapping[int, DetectorRecord]: @property def boundaries(self) -> FrozenSet[int]: - """Return all detectors in this graph that are labeled as boundaries.""" + """Return all detectors in this graph that are labelled as boundaries.""" return frozenset() @abstractmethod @@ -519,7 +519,7 @@ def nodes(self) -> List[int]: @property def boundaries(self) -> FrozenSet[int]: - """Return all nodes of this graph that are labeled as boundaries.""" + """Return all nodes of this graph that are labelled as boundaries.""" return self._boundaries @cached_property @@ -529,7 +529,7 @@ def detector_records(self) -> Dict[int, DetectorRecord]: @property def adj(self) -> nx.coreviews.MultiAdjacencyView: """ - Graph adjacency object holding the neighbors of each node. + Graph adjacency object holding the neighbours of each node. The returned object is a mappable that holds for each node a dictionary containing all of its neighbour nodes and the associated edge diff --git a/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph_tools.py b/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph_tools.py index fa4c893c..85c785e6 100644 --- a/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph_tools.py +++ b/deltakit-core/src/deltakit_core/decoding_graphs/_decoding_graph_tools.py @@ -136,7 +136,7 @@ def graph_to_json( - logicals: List of sorted tuples for logical edges - boundary: Decoding graph boundary. None if it doesn't exist. - If full flag is True, we additionally serialize: + If full flag is True, we additionally serialise: - detector_records: dict containing detector metadata - edge_record: dict containing edge metadata diff --git a/deltakit-core/tests/unit/decoding_graphs/test_decoding_hyper_multigraph.py b/deltakit-core/tests/unit/decoding_graphs/test_decoding_hyper_multigraph.py index eaa27007..1681e0b3 100644 --- a/deltakit-core/tests/unit/decoding_graphs/test_decoding_hyper_multigraph.py +++ b/deltakit-core/tests/unit/decoding_graphs/test_decoding_hyper_multigraph.py @@ -239,7 +239,7 @@ def test_incident_edges_returns_expected_edges(self, detector, expected_edges): @pytest.mark.parametrize( "detector, expected_nodes", [(0, {1, 2, 6}), (6, {0, 2, 3})] ) - def test_neighbors_returns_expected_edges(self, detector, expected_nodes): + def test_neighbours_returns_expected_edges(self, detector, expected_nodes): graph = decoding_hyper_multigraph_with_multi_hyperedges() assert set(node for node in graph.neighbors(detector)) == expected_nodes diff --git a/deltakit-core/tests/unit/decoding_graphs/test_decoding_hypergraph.py b/deltakit-core/tests/unit/decoding_graphs/test_decoding_hypergraph.py index a59b9334..25319216 100644 --- a/deltakit-core/tests/unit/decoding_graphs/test_decoding_hypergraph.py +++ b/deltakit-core/tests/unit/decoding_graphs/test_decoding_hypergraph.py @@ -261,7 +261,7 @@ def test_incident_edges_return_expected_edges( assert incident_edges == expected_edge @pytest.mark.parametrize( - "hypergraph, expected_neighbors", + "hypergraph, expected_neighbours", [ ( decoding_hypergraph_without_hyperedges(), @@ -273,12 +273,12 @@ def test_incident_edges_return_expected_edges( ), ], ) - def test_neighbors_return_expected_nodes(self, hypergraph, expected_neighbors): - neighbors = { + def test_neighbours_return_expected_nodes(self, hypergraph, expected_neighbours): + neighbours = { syndrome: set(hypergraph.neighbors(syndrome)) - for syndrome in expected_neighbors.keys() + for syndrome in expected_neighbours.keys() } - assert neighbors == expected_neighbors + assert neighbours == expected_neighbours @pytest.mark.parametrize( "edge_data, detector_records, expected_boundary, expected_nx_edges", diff --git a/deltakit-core/tests/unit/decoding_graphs/test_nx_decoding_graphs.py b/deltakit-core/tests/unit/decoding_graphs/test_nx_decoding_graphs.py index a2b99c51..f89d4e8b 100644 --- a/deltakit-core/tests/unit/decoding_graphs/test_nx_decoding_graphs.py +++ b/deltakit-core/tests/unit/decoding_graphs/test_nx_decoding_graphs.py @@ -398,7 +398,7 @@ def test_edges_connected_to_boundary_not_in_no_boundary_view( assert all(edge not in graph.no_boundary_view.edges for edge in boundary_edges) @pytest.mark.parametrize( - "edges_nodes_graph, syndrome_bit, expected_neighbors", + "edges_nodes_graph, syndrome_bit, expected_neighbours", [ (edge_list_node_list_and_decoding_graph_layered(), 2, {0, 1, 3, 6}), (edge_list_node_list_and_decoding_multi_graph_layered(), 2, {0, 1, 3, 6}), @@ -406,12 +406,12 @@ def test_edges_connected_to_boundary_not_in_no_boundary_view( (edge_list_node_list_and_decoding_multi_graph_layered(), 0, {1, 2, 3, 4}), ], ) - def test_neighbor_nodes_found( - self, edges_nodes_graph, syndrome_bit, expected_neighbors + def test_neighbour_nodes_found( + self, edges_nodes_graph, syndrome_bit, expected_neighbours ): _, _, decoding_graph = edges_nodes_graph - neighbors = set(decoding_graph.neighbors(syndrome_bit)) - assert neighbors == expected_neighbors + neighbours = set(decoding_graph.neighbors(syndrome_bit)) + assert neighbours == expected_neighbours @pytest.mark.parametrize( "edges_nodes_graph, endpoints, expected_path", diff --git a/deltakit-decode/src/deltakit_decode/analysis/_decoder_manager.py b/deltakit-decode/src/deltakit_decode/analysis/_decoder_manager.py index 13f20516..b33ad697 100644 --- a/deltakit-decode/src/deltakit_decode/analysis/_decoder_manager.py +++ b/deltakit-decode/src/deltakit_decode/analysis/_decoder_manager.py @@ -132,10 +132,10 @@ def run_batch_shots_parallel(self, `pool`. Cap the number of processes created such that a minimum of `min_tasks_per_process` should be running per process. By default the ABC implementation is a naive serial execution of run_batch_shots which should - be overridden for better parallel resource utilization. This method will block + be overridden for better parallel resource utilisation. This method will block until it is finished. - Each process in the pool should be initialized with this decoder manager stored + Each process in the pool should be initialised with this decoder manager stored in the process global variables by calling `configure_pool`. The Manager authenticates that a worker processes has a consistent decoder manager with the main process by comparing the managers `_mp_token`. The pool should have its @@ -382,7 +382,7 @@ def run_batch_shots_parallel(self, inner_batch_limit = int(batch_limit) // processes if batch_limit is not None \ else batch_limit # Attempt to run batches in parallel. If the pool has caused a process to lose - # the decoder manager in its globals memory, then reinitialize processes with + # the decoder manager in its globals memory, then reinitialise processes with # the decoder manager object and run batches in the same process call. try: partial_worker = partial(_run_process, self._mp_token, diff --git a/deltakit-decode/src/deltakit_decode/utils/_decoding_graph_visualiser.py b/deltakit-decode/src/deltakit_decode/utils/_decoding_graph_visualiser.py index 4bcdb97a..f739fb3e 100644 --- a/deltakit-decode/src/deltakit_decode/utils/_decoding_graph_visualiser.py +++ b/deltakit-decode/src/deltakit_decode/utils/_decoding_graph_visualiser.py @@ -115,7 +115,7 @@ def get_plot_3d_traces( traces: List[go.Trace] = [] traces += self.get_node_traces() if categorise_edges: - traces += self.get_categorized_edges_traces() + traces += self.get_categorised_edges_traces() else: traces += self.get_edges_traces() if syndrome is not None: @@ -196,7 +196,7 @@ def get_edges_traces(self) -> List[go.Trace]: traces.append(edge_trace) return traces - def get_categorized_edges_traces(self) -> List[go.Trace]: + def get_categorised_edges_traces(self) -> List[go.Trace]: """Add edges of the base graph as lines to traces""" ( _, diff --git a/deltakit-decode/tests/noise_sources/test_stim_noise_sources.py b/deltakit-decode/tests/noise_sources/test_stim_noise_sources.py index b1e338ab..5a0993a8 100644 --- a/deltakit-decode/tests/noise_sources/test_stim_noise_sources.py +++ b/deltakit-decode/tests/noise_sources/test_stim_noise_sources.py @@ -191,7 +191,7 @@ def test_stim_circuits_can_be_manipulated_with_after_clifford_depolarisation( optioned_stim.permute_stim_circuit(clean_stim_circuit)) assert expected_circuit == circuit - def test_applying_before_round_data_depolarization_raises_not_implemented_exception(self): + def test_applying_before_round_data_depolarisation_raises_not_implemented_exception(self): with pytest.raises(NotImplementedError): OptionedStim(before_round_data_depolarisation=0.333) diff --git a/deltakit-explorer/src/deltakit_explorer/_api/_api_client.py b/deltakit-explorer/src/deltakit_explorer/_api/_api_client.py index 449d147f..46d1e4f0 100644 --- a/deltakit-explorer/src/deltakit_explorer/_api/_api_client.py +++ b/deltakit-explorer/src/deltakit_explorer/_api/_api_client.py @@ -164,7 +164,7 @@ def defect_rates( Args: detectors (DetectionEvents): DetectionEvents values from simulations to compute defect rates. - stim_circuit (str | stim.Circuit): Circuit to analyze. + stim_circuit (str | stim.Circuit): Circuit to analyse. request_id (str): Identifier of the request.""" raise NotImplementedError() @@ -179,7 +179,7 @@ def get_correlation_matrix_for_trimmed_data( """Get correlation matrix for detectors. Args: - detectors (DetectionEvents): Detector data to analyze. + detectors (DetectionEvents): Detector data to analyse. noise_floor_circuit (str | stim.Circuit): Circuit with the minimal noise. use_default_noise_model_edges (bool): Whether to use default noise model edges. request_id (str): Identifier of the request. diff --git a/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py b/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py index 8eb39caf..a942114d 100644 --- a/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py +++ b/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py @@ -220,7 +220,7 @@ def compute_logical_error_per_round( """ # Get the inputs as numpy arrays. - # Sanitization: also make sure that the inputs are sorted. + # Sanitisation: also make sure that the inputs are sorted. isort = np.argsort(num_rounds) num_rounds = np.asarray(num_rounds)[isort] num_failed_shots = np.asarray(num_failed_shots)[isort] diff --git a/deltakit-explorer/src/deltakit_explorer/codes/_css/_css_code_experiment_circuit.py b/deltakit-explorer/src/deltakit_explorer/codes/_css/_css_code_experiment_circuit.py index b191bc2d..4833f5f1 100644 --- a/deltakit-explorer/src/deltakit_explorer/codes/_css/_css_code_experiment_circuit.py +++ b/deltakit-explorer/src/deltakit_explorer/codes/_css/_css_code_experiment_circuit.py @@ -168,7 +168,7 @@ def _cloud_css_code_experiment_circuit( m_B_powers=css_code.m_B_powers, ) else: - raise ValueError("Unrecognized `css_code` type.") + raise ValueError("Unrecognised `css_code` type.") basis_gates: Optional[list[str]] = None if use_iswap_gates: basis_gates = [ diff --git a/deltakit-explorer/src/deltakit_explorer/codes/_planar_code/__init__.py b/deltakit-explorer/src/deltakit_explorer/codes/_planar_code/__init__.py index 03cfc3b4..c4ea50c4 100644 --- a/deltakit-explorer/src/deltakit_explorer/codes/_planar_code/__init__.py +++ b/deltakit-explorer/src/deltakit_explorer/codes/_planar_code/__init__.py @@ -2,7 +2,7 @@ """ This module provides classes to define quantum memory experiments for planar unrotated and rotated codes. It is not currently public; this -__init__.py file is a historical artifact and can be removed, adjusting +__init__.py file is a historical artefact and can be removed, adjusting imports within other `deltakit_explorer` modules accordingly. """ diff --git a/deltakit-explorer/src/deltakit_explorer/codes/_schedules/__init__.py b/deltakit-explorer/src/deltakit_explorer/codes/_schedules/__init__.py index 2508432e..f4ca119c 100644 --- a/deltakit-explorer/src/deltakit_explorer/codes/_schedules/__init__.py +++ b/deltakit-explorer/src/deltakit_explorer/codes/_schedules/__init__.py @@ -1,7 +1,7 @@ # (c) Copyright Riverlane 2020-2025. """ Classes and functions to encapsulate syndrome extraction schedules for Planar codes. -This module is not currently public; this __init__.py file is a historical artifact +This module is not currently public; this __init__.py file is a historical artefact and can be removed, adjusting imports within other `deltakit_explorer` modules accordingly. """ diff --git a/deltakit-explorer/src/deltakit_explorer/qpu/_circuits/__init__.py b/deltakit-explorer/src/deltakit_explorer/qpu/_circuits/__init__.py index 9e03bf02..4baea788 100644 --- a/deltakit-explorer/src/deltakit_explorer/qpu/_circuits/__init__.py +++ b/deltakit-explorer/src/deltakit_explorer/qpu/_circuits/__init__.py @@ -3,7 +3,7 @@ This module aggregates functions to manipulate `deltakit_circuit.Circuit` objects. These include native gate compilation, noise introduction, and various optimisations. -This module is not currently public; this __init__.py file is a historical artifact +This module is not currently public; this __init__.py file is a historical artefact and can be removed, adjusting imports within other `deltakit_explorer` modules accordingly. """ diff --git a/deltakit-explorer/src/deltakit_explorer/qpu/_noise/__init__.py b/deltakit-explorer/src/deltakit_explorer/qpu/_noise/__init__.py index ff6946d9..cf11e57d 100644 --- a/deltakit-explorer/src/deltakit_explorer/qpu/_noise/__init__.py +++ b/deltakit-explorer/src/deltakit_explorer/qpu/_noise/__init__.py @@ -1,5 +1,5 @@ # (c) Copyright Riverlane 2020-2025. -# This module is not currently public; this __init__.py file is a historical artifact +# This module is not currently public; this __init__.py file is a historical artefact # and can be removed, adjusting imports within other `deltakit_explorer` modules # accordingly. from deltakit_explorer.qpu._noise._noise_parameters import NoiseParameters diff --git a/deltakit-explorer/src/deltakit_explorer/types/_types.py b/deltakit-explorer/src/deltakit_explorer/types/_types.py index a80e6451..6673e05d 100644 --- a/deltakit-explorer/src/deltakit_explorer/types/_types.py +++ b/deltakit-explorer/src/deltakit_explorer/types/_types.py @@ -29,7 +29,7 @@ class JSONable: @staticmethod def _to_json(obj) -> Any: - """Convert dataclass or object to dict for JSON serialization.""" + """Convert dataclass or object to dict for JSON serialisation.""" if dataclasses.is_dataclass(obj) and not isinstance(obj, type): return JSONable._to_json(dataclasses.asdict(obj)) if isinstance(obj, dict): @@ -43,7 +43,7 @@ def _to_json(obj) -> Any: if obj is None: return None raise ValueError( - f"Object of type {type(obj)} is not a dataclass or serializable object." + f"Object of type {type(obj)} is not a dataclass or serialisable object." ) def to_json(self) -> dict: diff --git a/deltakit-explorer/tests/unit/circuits/test_tableau_functions.py b/deltakit-explorer/tests/unit/circuits/test_tableau_functions.py index 1e45e45c..e216c2e6 100644 --- a/deltakit-explorer/tests/unit/circuits/test_tableau_functions.py +++ b/deltakit-explorer/tests/unit/circuits/test_tableau_functions.py @@ -8891,7 +8891,7 @@ def test_shifts_following_layers_to_accommodate_new_unitaries( up_to_paulis, ): # when parametrizing over `up_to_paulis`, the dictionaries in `comp_data` - # and `layer_ind_lookup` are not re-initialized. + # and `layer_ind_lookup` are not re-initialised. # `_compile_reset_and_meas_to_native_gates` mutates them, and this would # cause problems. comp_data = deepcopy(comp_data) diff --git a/deltakit-explorer/tests/unit/noise_models/test_noise_parameters.py b/deltakit-explorer/tests/unit/noise_models/test_noise_parameters.py index 48e0bdde..617a91d6 100644 --- a/deltakit-explorer/tests/unit/noise_models/test_noise_parameters.py +++ b/deltakit-explorer/tests/unit/noise_models/test_noise_parameters.py @@ -83,7 +83,7 @@ def test_invalid_t1_t2(self): def test_typical_case(self): # From `Ghosh et al. `_: # - # "II.B: The asymmetric depolarization channel (ADC) is sucha a model, + # "II.B: The asymmetric depolarisation channel (ADC) is such a model, # where a decoherent qubit is assumed to suffer from discrete Pauli X # (bit-flip) errors, Z (phase flip) errors, or Y (both)..." # @@ -110,7 +110,7 @@ def test_t1_equals_t2(self): noise_context = NoiseContext(circuit, circuit.layers[0]) noise_channel_result = _idle_noise_from_t1_t2(t, t)(noise_context, t) - # "If T2 = T1, the ADC reduces to the symmetric depolarization channel." + # "If T2 = T1, the ADC reduces to the symmetric depolarisation channel." # In this case, $p_X = p_Y = p_Z = (1 - \exp(-t/t1)) / 4$. # `Depolarise1` applies a randomly chosen Pauli with a given probability $p / 3$, # so the appropriate $p$ to use is $3 * (1 - \exp(-t/t1)) / 4 = 0.75 * (1 - \exp(-t/t1)$ diff --git a/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb b/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb index 8566e3e5..97ac22ce 100644 --- a/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb +++ b/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb @@ -504,7 +504,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "652025e0-b4a7-4015-81e4-aa0e2583603e", "metadata": {}, "outputs": [], diff --git a/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb b/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb index d22ac6fb..93b09266 100644 --- a/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb +++ b/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb @@ -82,7 +82,7 @@ "\n", "- Orange boxes are Leakage channels;\n", "- Blue boxes are Relaxation channels;\n", - "- Pink boxes are Pauil noise channels of X error, 1-qubit depolarizing and 2-qubit depolarizing noise;\n", + "- Pink boxes are Pauil noise channels of X error, 1-qubit depolarising and 2-qubit depolarising noise;\n", "- Black boxes are Measurement or Reset gates;\n", "- White boxes are Hadamard gates;\n", "- Grey rectangles are Detectors." diff --git a/docs/guide/adding_noise.md b/docs/guide/adding_noise.md index 246bc88b..97f7e110 100644 --- a/docs/guide/adding_noise.md +++ b/docs/guide/adding_noise.md @@ -126,7 +126,7 @@ deltakit_toy_noisy.as_stim_circuit().diagram("timeline-html") ## `PhysicalNoise` class -On the other end of the modeling spectrum is a model with seven parameters. +On the other end of the modelling spectrum is a model with seven parameters. All these parameters are characteristics of the QPU and can be derived during device calibration. You can often find these numbers in the datasheets of hardware companies. diff --git a/docs/guide/analysis.ipynb b/docs/guide/analysis.ipynb index f3ab005e..1e831b5d 100644 --- a/docs/guide/analysis.ipynb +++ b/docs/guide/analysis.ipynb @@ -528,7 +528,7 @@ "id": "30474ff8", "metadata": {}, "source": [ - "In Quantum Memory experiments, you try to preserve a qubit in the state it was initialized.\n", + "In Quantum Memory experiments, you try to preserve a qubit in the state it was initialised.\n", "The longer you do this, the more physical errors accumulate, thus the greater the chance for the decoder to make a mistake.\n", "The theoretical model states that every error correction round increases the probability of a logical error by a factor known as the **logical error rate** $\\epsilon$.\n", "You may also find it under the name \"logical error probability per round\".\n", @@ -614,7 +614,7 @@ "\n", "The [Threshold theorem](https://en.wikipedia.org/wiki/Threshold_theorem) suggests that logical error rate should exponentially decrease with the size of the code,\n", "but beyond *some* physical noise level this law flips, and adding more qubits leads to even worse logical errors.\n", - "The point (amount of physical noise) where this happens is called a **threshold**. This number characterizes\n", + "The point (amount of physical noise) where this happens is called a **threshold**. This number characterises\n", "how ready particular codes and decoders are for the noise level of real hardware. The bigger the threshold, the better.\n", "\n", "To compute a threshold, you will have to run QEC experiments with different noise levels and code sizes." diff --git a/docs/guide/getting_started.md b/docs/guide/getting_started.md index f58fb49a..b76024a5 100644 --- a/docs/guide/getting_started.md +++ b/docs/guide/getting_started.md @@ -25,12 +25,12 @@ Performing a QEC experiment with Deltakit typically involves four steps. 3. In the *decoding* step, you choose a decoder to decode your measurement results. You can use both open-source decoders, like minimum weight perfect matching (MWPM), and propriety decoders, like Ambiguity Clustering (AC). -4. In the *analysis* step, you interpret the results of your QEC experiment, and calculate and visualize +4. In the *analysis* step, you interpret the results of your QEC experiment, and calculate and visualise the logical error probability and error suppression factor $\Lambda$. ## Circuit Generation -### Idealized Circuit +### Idealised Circuit To generate the underlying circuit for quantum memory experiments, there are several common quantum error correction codes you can use, including: - Rotated planar codes ({class}`RotatedPlanarCode `), @@ -70,7 +70,7 @@ To convert the `circuit` to [`stim`](https://github.com/quantumlib/Stim), you ca stim_circuit = circuit.as_stim_circuit() ``` -which is then easy to visualize: +which is then easy to visualise: ```{code-cell} ipython3 stim_circuit.diagram(type="timeline-svg") @@ -189,7 +189,7 @@ print(f"There were {n_fails} failures out of {n_shots} shots.") ``` ## Analysis -Deltakit also provides tools for summarizing the results of experiments. To calculate the LEP (Logical Error Probability) and its standard error, use the +Deltakit also provides tools for summarising the results of experiments. To calculate the LEP (Logical Error Probability) and its standard error, use the {meth}`calculate_lep_and_lep_stddev ` function. ```{code-cell} ipython3 @@ -209,7 +209,7 @@ np.testing.assert_allclose(lep, lep0) np.testing.assert_allclose(lep_stddev, lep_stddev0) ``` -Now you can start varying different parameters to see how that changes the logical error probability, such as the code distance. You can pass a list of decoder managers and number of shots to {class}`RunAllAnalysisEngine `, and use the `run` method to run your simulations. This method then returns a dataframe that summarizes the results. +Now you can start varying different parameters to see how that changes the logical error probability, such as the code distance. You can pass a list of decoder managers and number of shots to {class}`RunAllAnalysisEngine `, and use the `run` method to run your simulations. This method then returns a dataframe that summarises the results. ```{code-cell} ipython3 from deltakit.decode.analysis import RunAllAnalysisEngine diff --git a/docs/release.md b/docs/release.md index 8fe59922..554b3275 100644 --- a/docs/release.md +++ b/docs/release.md @@ -39,7 +39,7 @@ To perform a release: 5. Finally, the release manager triggers the stable release workflow on the tagged commit ("Build, test, and publish stable wheels" on GitHub Actions, [`stable_release.yml`](https://github.com/Deltakit/deltakit/blob/main/.github/workflows/stable_release.yml)). - This builds and publishes the artifacts to PyPI and GitHub, and it renders and uploads the documentation to + This builds and publishes the artefacts to PyPI and GitHub, and it renders and uploads the documentation to GitHub pages. The latest stable version of `deltakit` can be installed with `pip install deltakit`. @@ -85,7 +85,7 @@ serves to document the relevant changes.) To perform an alpha release, the release manager triggers the alpha release workflow on the desired commit ("Build, test, and publish alpha wheels" on GitHub Actions, [`alpha_release.yml`](https://github.com/Deltakit/deltakit/blob/main/.github/workflows/alpha_release.yml)). -This builds and publishes the artifacts to PyPI, and it associates a tag with the commit on GitHub. +This builds and publishes the artefacts to PyPI, and it associates a tag with the commit on GitHub. The latest alpha version of `deltakit` can be installed by using `pip` with the `--pre` option, specifying the desired version number; e.g., `pip install deltakit --pre 0.4.0a20250904222833`. diff --git a/docs/setup.md b/docs/setup.md index 52fc5404..ba2ae97c 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -10,7 +10,7 @@ pip install deltakit ### Installation in a virtual environment or Colab -In a terminal with your favorite distribution of Python/pip on the path, browse to a working folder for the virtual environment and run: +In a terminal with your favourite distribution of Python/pip on the path, browse to a working folder for the virtual environment and run: ::::{tab-set} :::{tab-item} macOS / Linux diff --git a/pyproject.toml b/pyproject.toml index 713ddbb5..00a728c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,6 +149,9 @@ skip = [".pixi", ".tox", ".venv"] exclude_dirs = ['.github', '.pixi', '.tox', '.venv', './tools/vale.py', 'tools/vale.py'] skips = ['B101', 'B311'] # B101 is just use of `assert`; 'B311' is use of random module +[tool.typos.default] +locale = 'en-gb' + [tool.typos.default.extend-words] # Don't correct "iy" - often used in the context of "probability of an IY operation" iy = "iy" @@ -160,10 +163,22 @@ interm = "interm" ket = "ket" ND = "ND" # part of NDArray Alise = "Alise" +DEPOLARIZE = "DEPOLARIZE" +depolarization = "depolarization" +Authorization = "Authorization" # Web-related field. +color = "color" # Used in matplotlib and stim APIs. +colors = "colors" +optimize = "optimize" # scipy.optimize +ax = "ax" # Typical naming for matplotlib Axes instances. +center = "center" +stabilizer = "stabilizer" +stabilizers = "stabilizers" +synchronize = "synchronize" # multiprocessing import +neighbors = "neighbors" [tool.typos.files] # Paths/patterns to exclude from spell checking -extend-exclude = ["docs/styles"] +extend-exclude = ["docs/styles", "**/LICENSE", "**/*.html", "licenses.confluence"] [tool.coverage.run] omit = [ From 01aed2e84da79db072e2ac8d74bfe5e560f8d89c Mon Sep 17 00:00:00 2001 From: Marco Ghibaudi <62563515+mghibaudi@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:25:57 +0000 Subject: [PATCH 21/26] Restoring full security check (#119) --- .github/workflows/static_code_analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/static_code_analysis.yml b/.github/workflows/static_code_analysis.yml index b98c024e..0ab021ac 100644 --- a/.github/workflows/static_code_analysis.yml +++ b/.github/workflows/static_code_analysis.yml @@ -61,5 +61,5 @@ jobs: if: ${{ always() }} - name: pip_audit - run: pixi run pip_audit --ignore-vuln GHSA-4xh5-x5gv-qwph + run: pixi run pip_audit if: ${{ always() }} From 46c62262f6a7e27da8b180005efb6522f67b85de Mon Sep 17 00:00:00 2001 From: Adrien Suau <12374487+nelimee@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:29:59 +0100 Subject: [PATCH 22/26] feat: support stim tags (#117) * Add tag to annotations * Add tag to gates * Fix circuit tests * Add tag to noise instructions * Add missing tag in __repr__ * Allow exporting tags to stim circuit * Testing tags * Add default value to tag in stim identifiers * Make QPU.add_noise_to_circuit publicly accessible * Remove commented out lines --- .../_annotations/_detector.py | 23 +++-- .../_annotations/_observable.py | 19 +++- .../_annotations/_shift_coordinates.py | 17 +++- .../src/deltakit_circuit/_gate_layer.py | 25 +++--- .../src/deltakit_circuit/_noise_layer.py | 11 ++- .../src/deltakit_circuit/_parse_stim.py | 82 +++++++++++++---- .../src/deltakit_circuit/_stim_identifiers.py | 2 + .../deltakit_circuit/gates/_abstract_gates.py | 48 +++++++--- .../gates/_measurement_gates.py | 6 +- .../_abstract_noise_channels.py | 90 ++++++++++++++----- .../noise_channels/_depolarising_noise.py | 13 ++- .../noise_channels/_pauli_noise.py | 20 +++-- .../tests/test_annotations/test_detector.py | 7 ++ .../tests/test_annotations/test_observable.py | 6 ++ .../test_shift_coordinates.py | 7 ++ .../test_gates/test_measurement_gates.py | 39 +++++--- .../tests/test_gates/test_one_qubit_gates.py | 16 +++- .../tests/test_gates/test_reset_gates.py | 16 +++- .../tests/test_gates/test_two_qubit_gates.py | 29 ++++-- .../test_correlated_noise.py | 8 ++ .../test_depolarising_noise.py | 8 ++ .../test_noise_channels/test_leakage_noise.py | 5 ++ .../test_noise_channels/test_pauli_noise.py | 43 +++++++++ .../test_stim_parsing/test_gate_parsing.py | 4 +- .../test_stim_parsing/test_noise_parsing.py | 5 +- .../test_stim_parsing/test_tag_parsing.py | 46 ++++++++++ .../src/deltakit_explorer/qpu/_qpu.py | 4 +- .../tests/unit/noise_models/test_sd6_noise.py | 2 +- 28 files changed, 484 insertions(+), 117 deletions(-) create mode 100644 deltakit-circuit/tests/test_stim_parsing/test_tag_parsing.py diff --git a/deltakit-circuit/src/deltakit_circuit/_annotations/_detector.py b/deltakit-circuit/src/deltakit_circuit/_annotations/_detector.py index 077edc8a..6403aaf6 100644 --- a/deltakit-circuit/src/deltakit_circuit/_annotations/_detector.py +++ b/deltakit-circuit/src/deltakit_circuit/_annotations/_detector.py @@ -20,6 +20,8 @@ class Detector: The measurements that this is the detectors of. coordinate: Iterable[float] | None An optional coordinate to associate with this detector. + tag: str | None + An optional instruction tag. """ stim_string = "DETECTOR" @@ -28,6 +30,7 @@ def __init__( self, measurements: MeasurementRecord | Iterable[MeasurementRecord], coordinate: Iterable[float] | None = None, + tag: str | None = None, ): self._measurements = ( frozenset((measurements,)) @@ -35,6 +38,11 @@ def __init__( else frozenset(measurements) ) self._coordinate = Coordinate(*coordinate) if coordinate is not None else None + self._tag = tag + + @property + def tag(self) -> str | None: + return self._tag @property def coordinate(self) -> Coordinate | None: @@ -82,10 +90,11 @@ def permute_stim_circuit(self, stim_circuit: stim.Circuit, _qubit_mapping=None): stim_targets = chain.from_iterable( record.stim_targets() for record in self.measurements ) - if (coordinate := self.coordinate) is None: - stim_circuit.append(self.stim_string, stim_targets) - else: - stim_circuit.append(self.stim_string, stim_targets, coordinate) + stim_arguments = self.coordinate if self.coordinate is not None else tuple() + stim_tag = self._tag if self._tag is not None else "" + stim_circuit.append( + self.stim_string, stim_targets, stim_arguments, tag=stim_tag + ) def __eq__(self, other: object) -> bool: return ( @@ -98,4 +107,8 @@ def __hash__(self) -> int: return hash((self._measurements, self._coordinate)) def __repr__(self) -> str: - return f"Detector({list(self.measurements)}, coordinate={self.coordinate})" + tag_repr = f"[{self._tag}]" if self._tag is not None else "" + return ( + f"Detector{tag_repr}({list(self.measurements)}, " + f"coordinate={self.coordinate})" + ) diff --git a/deltakit-circuit/src/deltakit_circuit/_annotations/_observable.py b/deltakit-circuit/src/deltakit_circuit/_annotations/_observable.py index 32b48d37..605db9b3 100644 --- a/deltakit-circuit/src/deltakit_circuit/_annotations/_observable.py +++ b/deltakit-circuit/src/deltakit_circuit/_annotations/_observable.py @@ -19,6 +19,8 @@ class Observable: Give a way of identifying this observable measurements: MeasurementRecord | Iterable[MeasurementRecord] The measurement records which identify the logical observable. + tag: str | None + An optional instruction tag. """ stim_string = "OBSERVABLE_INCLUDE" @@ -27,6 +29,7 @@ def __init__( self, observable_index: int, measurements: MeasurementRecord | Iterable[MeasurementRecord], + tag: str | None = None, ): if observable_index < 0: raise ValueError("Observable index cannot be negative.") @@ -36,6 +39,11 @@ def __init__( if isinstance(measurements, MeasurementRecord) else frozenset(measurements) ) + self._tag = tag + + @property + def tag(self) -> str | None: + return self._tag @property def measurements(self) -> FrozenSet[MeasurementRecord]: @@ -58,7 +66,10 @@ def permute_stim_circuit(self, stim_circuit: stim.Circuit, _qubit_mapping=None): stim_targets = chain.from_iterable( record.stim_targets() for record in self.measurements ) - stim_circuit.append(self.stim_string, stim_targets, self._observable_index) + stim_tag = self._tag if self._tag is not None else "" + stim_circuit.append( + self.stim_string, stim_targets, self._observable_index, tag=stim_tag + ) def __eq__(self, other: object) -> bool: return ( @@ -71,7 +82,11 @@ def __hash__(self) -> int: return hash((self._observable_index, self._measurements)) def __repr__(self) -> str: - return f"Observable({list(self.measurements)}, index={self._observable_index})" + tag_repr = f"[{self._tag}]" if self._tag is not None else "" + return ( + f"Observable{tag_repr}({list(self.measurements)}, " + f"index={self._observable_index})" + ) @property def observable_index(self) -> int: diff --git a/deltakit-circuit/src/deltakit_circuit/_annotations/_shift_coordinates.py b/deltakit-circuit/src/deltakit_circuit/_annotations/_shift_coordinates.py index bfbade09..e8b480d0 100644 --- a/deltakit-circuit/src/deltakit_circuit/_annotations/_shift_coordinates.py +++ b/deltakit-circuit/src/deltakit_circuit/_annotations/_shift_coordinates.py @@ -21,8 +21,17 @@ class ShiftCoordinates: the absolute coordinate. """ - def __init__(self, coordinate_shift: Iterable[int | float]): + def __init__( + self, + coordinate_shift: Iterable[int | float], + tag: str | None = None, + ): self._coordinate_shift = Coordinate(*coordinate_shift) + self._tag = tag + + @property + def tag(self) -> str | None: + return self._tag def permute_stim_circuit(self, stim_circuit: stim.Circuit, _qubit_mapping=None): """Updates stim_circuit with the single stim circuit which contains @@ -37,7 +46,8 @@ def permute_stim_circuit(self, stim_circuit: stim.Circuit, _qubit_mapping=None): _qubit_mapping : None Unused argument to keep interface with other layer classes clean. """ - stim_circuit.append("SHIFT_COORDS", [], self._coordinate_shift) + stim_tag = self._tag if self._tag is not None else "" + stim_circuit.append("SHIFT_COORDS", [], self._coordinate_shift, tag=stim_tag) def __eq__(self, other: object) -> bool: return ( @@ -49,4 +59,5 @@ def __hash__(self) -> int: return hash(self._coordinate_shift) def __repr__(self) -> str: - return f"ShiftCoordinates({self._coordinate_shift})" + tag_repr = f"[{self._tag}]" if self._tag is not None else "" + return f"ShiftCoordinates{tag_repr}({self._coordinate_shift})" diff --git a/deltakit-circuit/src/deltakit_circuit/_gate_layer.py b/deltakit-circuit/src/deltakit_circuit/_gate_layer.py index 7495eff2..42587c41 100644 --- a/deltakit-circuit/src/deltakit_circuit/_gate_layer.py +++ b/deltakit-circuit/src/deltakit_circuit/_gate_layer.py @@ -21,6 +21,9 @@ ) import stim + +from deltakit_circuit._noise_factory import GateReplacementPolicy +from deltakit_circuit._qubit_identifiers import Qubit, T, U from deltakit_circuit._qubit_mapping import default_qubit_mapping from deltakit_circuit._stim_identifiers import AppendArguments from deltakit_circuit.gates import MPP, _Gate, _MeasurementGate @@ -34,8 +37,6 @@ from deltakit_circuit.gates._one_qubit_gates import _OneQubitCliffordGate from deltakit_circuit.gates._reset_gates import _ResetGate from deltakit_circuit.gates._two_qubit_gates import _TwoQubitGate -from deltakit_circuit._noise_factory import GateReplacementPolicy -from deltakit_circuit._qubit_identifiers import Qubit, T, U _NonMeasurementGate = Union[_OneQubitCliffordGate, _ResetGate, _TwoQubitGate] @@ -207,15 +208,16 @@ def _collect_gates( """Collect all of the same gate types together.""" gate_args = [] unordered_gates: DefaultDict[ - Type[_NonMeasurementGate], List[stim.GateTarget] + tuple[Type[_NonMeasurementGate], str | None], List[stim.GateTarget] ] = defaultdict(list) for non_measurement_gate in self._non_measurement_gates: - unordered_gates[non_measurement_gate.__class__].extend( + key = (non_measurement_gate.__class__, non_measurement_gate.tag) + unordered_gates[key].extend( non_measurement_gate.stim_targets(qubit_mapping) ) - for unordered_gate, targets in unordered_gates.items(): + for (unordered_gate, tag), targets in unordered_gates.items(): gate_args.append( - AppendArguments(unordered_gate.stim_string, tuple(targets), (0,)) + AppendArguments(unordered_gate.stim_string, tuple(targets), (0,), tag) ) for measurement_gate in self._measurement_gates: gate_args.append( @@ -223,6 +225,7 @@ def _collect_gates( measurement_gate.stim_string, measurement_gate.stim_targets(qubit_mapping), (measurement_gate.probability,), + measurement_gate.tag, ) ) return gate_args @@ -251,13 +254,13 @@ def permute_stim_circuit( if qubit_mapping is None else qubit_mapping ) - for gate_string, targets, error_probability in self._collect_gates( + for gate_string, targets, error_probability, tag in self._collect_gates( qubit_mapping ): - if error_probability == (0,): - stim_circuit.append(gate_string, targets) - else: - stim_circuit.append(gate_string, targets, error_probability) + args = tuple() if error_probability == (0,) else error_probability + stim_circuit.append( + gate_string, targets, args, tag=tag if tag is not None else "" + ) def approx_equals( self, diff --git a/deltakit-circuit/src/deltakit_circuit/_noise_layer.py b/deltakit-circuit/src/deltakit_circuit/_noise_layer.py index a6403584..07ae8e46 100644 --- a/deltakit-circuit/src/deltakit_circuit/_noise_layer.py +++ b/deltakit-circuit/src/deltakit_circuit/_noise_layer.py @@ -135,14 +135,15 @@ def _collect_noise_channels( noise_channel.stim_targets(qubit_mapping) ) uncorrelated_noise = [ - AppendArguments(stim_string, tuple(targets), probabilities) - for (stim_string, probabilities), targets in grouped_noise.items() + AppendArguments(stim_string, tuple(targets), probabilities, tag) + for (stim_string, probabilities, tag), targets in grouped_noise.items() ] correlated_noise = [ AppendArguments( noise_channel.stim_string, noise_channel.stim_targets(qubit_mapping), noise_channel.probabilities, + noise_channel.tag, ) for noise_channel in self._correlated_noise_channels ] @@ -172,10 +173,12 @@ def permute_stim_circuit( if qubit_mapping is None else qubit_mapping ) - for stim_string, targets, probabilities in self._collect_noise_channels( + for stim_string, targets, probabilities, tag in self._collect_noise_channels( qubit_mapping ): - stim_circuit.append(stim_string, targets, probabilities) + stim_circuit.append( + stim_string, targets, probabilities, tag=tag if tag is not None else "" + ) def approx_equals( # noqa: PLR0911 self, diff --git a/deltakit-circuit/src/deltakit_circuit/_parse_stim.py b/deltakit-circuit/src/deltakit_circuit/_parse_stim.py index eb0d51ae..14cbf04e 100644 --- a/deltakit-circuit/src/deltakit_circuit/_parse_stim.py +++ b/deltakit-circuit/src/deltakit_circuit/_parse_stim.py @@ -99,6 +99,7 @@ def _classify_pauli_target( def _parse_single_qubit_gate_instruction( gate_class: Type[_OneQubitCliffordGate | _ResetGate], instruction_targets: Iterable[stim.GateTarget], + instruction_tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> List[GateLayer]: qubits = ( @@ -107,13 +108,15 @@ def _parse_single_qubit_gate_instruction( ) time_steps = group_targets(qubit for qubit in qubits) return [ - GateLayer(gate_class(qubit) for qubit in time_step) for time_step in time_steps + GateLayer(gate_class(qubit, tag=instruction_tag) for qubit in time_step) + for time_step in time_steps ] def _parse_two_qubit_gate_instruction( gate_class: Type[_TwoQubitGate], instruction_targets: Sequence[stim.GateTarget], + tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> GateLayer: targets: List[Qubit | SweepBit | MeasurementRecord] = [] @@ -124,13 +127,14 @@ def _parse_two_qubit_gate_instruction( targets.append(MeasurementRecord(target.value)) else: targets.append(qubit_mapping.get(target.value, Qubit(target.value))) - return GateLayer(gate_class.from_consecutive(targets)) + return GateLayer(gate_class.from_consecutive(targets, tag=tag)) def _parse_single_qubit_measurement( gate_class: Type[_OneQubitMeasurementGate], instruction_targets: Iterable[stim.GateTarget], instruction_arguments: Iterable[float], + tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> GateLayer: probability = next(iter(instruction_arguments), 0.0) @@ -139,6 +143,7 @@ def _parse_single_qubit_measurement( qubit_mapping.get(target.value, Qubit(target.value)), probability, invert=target.is_inverted_result_target, + tag=tag, ) for target in instruction_targets ) @@ -147,6 +152,7 @@ def _parse_single_qubit_measurement( def _parse_mpp_instruction( instruction_targets: Sequence[stim.GateTarget], instruction_arguments: Iterable[float], + tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> GateLayer: """Function for parsing a single MPP instruction. This algorithm is @@ -188,7 +194,9 @@ def _parse_mpp_instruction( else: qubit_identifiers.append(MeasurementPauliProduct(pauli_gates)) pauli_gates = [] - return GateLayer(MPP(qubit_id, probability) for qubit_id in qubit_identifiers) + return GateLayer( + MPP(qubit_id, probability, tag=tag) for qubit_id in qubit_identifiers + ) def _parse_single_qubit_noise_instruction( @@ -197,68 +205,74 @@ def _parse_single_qubit_noise_instruction( ], instruction_targets: Iterable[stim.GateTarget], probability: float, + tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> NoiseLayer: qubits = ( qubit_mapping.get(target.value, Qubit(target.value)) for target in instruction_targets ) - return NoiseLayer(noise_class(qubit, probability) for qubit in qubits) + return NoiseLayer(noise_class(qubit, probability, tag=tag) for qubit in qubits) def _parse_depolarise_2_noise_instruction( instruction_targets: Sequence[stim.GateTarget], probability: float, + tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> NoiseLayer: qubits = [ qubit_mapping.get(target.value, Qubit(target.value)) for target in instruction_targets ] - return NoiseLayer(Depolarise2.from_consecutive(qubits, probability)) + return NoiseLayer(Depolarise2.from_consecutive(qubits, probability, tag=tag)) def _parse_pauli_channel_1_instruction( instruction_targets: Iterable[stim.GateTarget], probabilities: Iterable[float], + tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> NoiseLayer: qubits = ( qubit_mapping.get(target.value, Qubit(target.value)) for target in instruction_targets ) - return NoiseLayer(PauliChannel1(qubit, *probabilities) for qubit in qubits) + return NoiseLayer(PauliChannel1(qubit, *probabilities, tag=tag) for qubit in qubits) def _parse_pauli_channel_2_instruction( instruction_targets: Iterable[stim.GateTarget], probabilities: Iterable[float], + tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> NoiseLayer: qubits = [ qubit_mapping.get(target.value, Qubit(target.value)) for target in instruction_targets ] - return NoiseLayer(PauliChannel2.from_consecutive(qubits, *probabilities)) + return NoiseLayer(PauliChannel2.from_consecutive(qubits, *probabilities, tag=tag)) def _parse_correlated_error_instruction( noise_class: Type[CorrelatedError | ElseCorrelatedError], instruction_targets: Iterable[stim.GateTarget], probability: float, + tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> NoiseLayer: pauli_product: PauliProduct = PauliProduct( cast(_PauliGate, _classify_pauli_target(target, qubit_mapping)) for target in instruction_targets ) - return NoiseLayer(noise_class(pauli_product, probability)) + return NoiseLayer(noise_class(pauli_product, probability, tag=tag)) def parse_stim_gate_instruction( deltakit_circuit_gate_class: Type[_Gate], instruction_targets: Sequence[stim.GateTarget], instruction_arguments: Sequence[float], + instruction_tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> GateLayer | Iterable[GateLayer]: """Parse a single instruction which is a gate into a gate layer. @@ -272,6 +286,8 @@ def parse_stim_gate_instruction( instruction_arguments : Sequence[float] The additional arguments to give to the gate. These are commonly just the probabilities for non-deterministic gates. + instruction_tag : (str | None) + Tag associated with the instruction. None if no tag was present. qubit_mapping : Mapping[int, Qubit] A mapping for qubits where coordinates have been specified in stim. The mapping maps each qubit's index to the respective @@ -291,22 +307,29 @@ def parse_stim_gate_instruction( deltakit_circuit_gate_class, (OneQubitCliffordGate, OneQubitResetGate) ): return _parse_single_qubit_gate_instruction( - deltakit_circuit_gate_class, instruction_targets, qubit_mapping + deltakit_circuit_gate_class, + instruction_targets, + instruction_tag, + qubit_mapping, ) if issubclass(deltakit_circuit_gate_class, TwoOperandGate): return _parse_two_qubit_gate_instruction( - deltakit_circuit_gate_class, instruction_targets, qubit_mapping + deltakit_circuit_gate_class, + instruction_targets, + instruction_tag, + qubit_mapping, ) if issubclass(deltakit_circuit_gate_class, OneQubitMeasurementGate): return _parse_single_qubit_measurement( deltakit_circuit_gate_class, instruction_targets, instruction_arguments, + instruction_tag, qubit_mapping, ) if issubclass(deltakit_circuit_gate_class, MPP): return _parse_mpp_instruction( - instruction_targets, instruction_arguments, qubit_mapping + instruction_targets, instruction_arguments, instruction_tag, qubit_mapping ) raise ValueError( f"Given gate class: '{deltakit_circuit_gate_class}' is not a " @@ -318,6 +341,7 @@ def parse_stim_noise_instruction( deltakit_circuit_noise_class: Type[_NoiseChannel], instruction_targets: Sequence[stim.GateTarget], instruction_arguments: Sequence[float], + instruction_tag: str | None, qubit_mapping: Mapping[int, Qubit], ) -> NoiseLayer: """Parse a single instruction which is a noise into a NoiseLayer. @@ -330,6 +354,8 @@ def parse_stim_noise_instruction( The stim instruction targets to act the noise channel on. instruction_arguments : Sequence[float] The probabilities which define the noise channel. + instruction_tag : (str | None) + Tag associated with the instruction. None if no tag was present. Returns ------- @@ -349,25 +375,30 @@ def parse_stim_noise_instruction( deltakit_circuit_noise_class, instruction_targets, instruction_arguments[0], + instruction_tag, qubit_mapping, ) if issubclass(deltakit_circuit_noise_class, Depolarise2): return _parse_depolarise_2_noise_instruction( - instruction_targets, instruction_arguments[0], qubit_mapping + instruction_targets, + instruction_arguments[0], + instruction_tag, + qubit_mapping, ) if issubclass(deltakit_circuit_noise_class, PauliChannel1): return _parse_pauli_channel_1_instruction( - instruction_targets, instruction_arguments, qubit_mapping + instruction_targets, instruction_arguments, instruction_tag, qubit_mapping ) if issubclass(deltakit_circuit_noise_class, PauliChannel2): return _parse_pauli_channel_2_instruction( - instruction_targets, instruction_arguments, qubit_mapping + instruction_targets, instruction_arguments, instruction_tag, qubit_mapping ) if issubclass(deltakit_circuit_noise_class, (CorrelatedError, ElseCorrelatedError)): return _parse_correlated_error_instruction( deltakit_circuit_noise_class, instruction_targets, instruction_arguments[0], + instruction_tag, qubit_mapping, ) raise ValueError( @@ -394,7 +425,10 @@ def parse_detector(instruction: stim.CircuitInstruction) -> Detector: for gate_target in instruction.targets_copy() ) coords = instruction.gate_args_copy() - return Detector(measurement_records, Coordinate(*coords) if coords != [] else None) + tag = instruction.tag if hasattr(instruction, "tag") else "" + return Detector( + measurement_records, Coordinate(*coords) if coords != [] else None, tag=tag + ) def parse_observable(instruction: stim.CircuitInstruction) -> Observable: @@ -410,12 +444,14 @@ def parse_observable(instruction: stim.CircuitInstruction) -> Observable: Observable A single Observable with all measurement records from the instruction. """ + tag = instruction.tag if hasattr(instruction, "tag") else "" return Observable( int(instruction.gate_args_copy()[0]), ( MeasurementRecord(gate_target.value) for gate_target in instruction.targets_copy() ), + tag=tag, ) @@ -432,7 +468,8 @@ def parse_shift_coords(instruction: stim.CircuitInstruction) -> ShiftCoordinates ShiftCoordinates A single shift coordinates which advances the detector coordinates. """ - return ShiftCoordinates(instruction.gate_args_copy()) + tag = instruction.tag if hasattr(instruction, "tag") else "" + return ShiftCoordinates(instruction.gate_args_copy(), tag=tag) def parse_circuit_instruction( @@ -462,13 +499,22 @@ def parse_circuit_instruction( instruction_name = instruction.name instruction_targets = instruction.targets_copy() instruction_arguments = instruction.gate_args_copy() + instruction_tag = instruction.tag if hasattr(instruction, "tag") else None if (gate_class := GATE_MAPPING.get(instruction_name, None)) is not None: return parse_stim_gate_instruction( - gate_class, instruction_targets, instruction_arguments, qubit_mapping + gate_class, + instruction_targets, + instruction_arguments, + instruction_tag, + qubit_mapping, ) if (noise_class := NOISE_CHANNEL_MAPPING.get(instruction_name, None)) is not None: return parse_stim_noise_instruction( - noise_class, instruction_targets, instruction_arguments, qubit_mapping + noise_class, + instruction_targets, + instruction_arguments, + instruction_tag, + qubit_mapping, ) if instruction_name == "DETECTOR": return parse_detector(instruction) diff --git a/deltakit-circuit/src/deltakit_circuit/_stim_identifiers.py b/deltakit-circuit/src/deltakit_circuit/_stim_identifiers.py index 1eaa75dc..05260cde 100644 --- a/deltakit-circuit/src/deltakit_circuit/_stim_identifiers.py +++ b/deltakit-circuit/src/deltakit_circuit/_stim_identifiers.py @@ -13,6 +13,7 @@ class NoiseStimIdentifier(NamedTuple): stim_string: str probabilities: Tuple[float, ...] + tag: str | None = None class AppendArguments(NamedTuple): @@ -23,3 +24,4 @@ class AppendArguments(NamedTuple): stim_string: str stim_targets: Tuple[stim.GateTarget, ...] arguments: Tuple[float, ...] + tag: str | None = None diff --git a/deltakit-circuit/src/deltakit_circuit/gates/_abstract_gates.py b/deltakit-circuit/src/deltakit_circuit/gates/_abstract_gates.py index c1772d43..405fe477 100644 --- a/deltakit-circuit/src/deltakit_circuit/gates/_abstract_gates.py +++ b/deltakit-circuit/src/deltakit_circuit/gates/_abstract_gates.py @@ -42,6 +42,14 @@ class Gate(ABC, Generic[T]): stim_string: ClassVar[str] + def __init__(self, tag: str | None = None) -> None: + super().__init__() + self._tag = tag + + @property + def tag(self) -> str | None: + return self._tag + @property @abstractmethod def qubits(self) -> Tuple[Qubit[T], ...]: @@ -93,7 +101,8 @@ class OneQubitGate(Gate[T]): The qubit that this gate acts on. """ - def __init__(self, qubit: Qubit[T] | T): + def __init__(self, qubit: Qubit[T] | T, tag: str | None = None): + super().__init__(tag) self._qubit = Qubit(qubit) if not isinstance(qubit, Qubit) else qubit @property @@ -115,7 +124,8 @@ def stim_targets( return (stim.GateTarget(qubit_mapping[self.qubit]),) def __repr__(self) -> str: - return f"{self.stim_string}({self.qubit})" + tag_repr = f"[{self._tag}]" if self._tag is not None else "" + return f"{self.stim_string}{tag_repr}({self.qubit})" def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and self.qubit == other.qubit @@ -172,9 +182,13 @@ class OneQubitMeasurementGate(OneQubitGate[T]): basis: ClassVar[PauliBasis | None] def __init__( - self, qubit: Qubit[T] | T, probability: float = 0.0, invert: bool = False + self, + qubit: Qubit[T] | T, + probability: float = 0.0, + invert: bool = False, + tag: str | None = None, ): - super().__init__(qubit) + super().__init__(qubit, tag) if not 0 <= probability <= 1: raise ValueError("Probability must be between zero and one.") self._probability = probability @@ -252,9 +266,11 @@ def __invert__(self) -> OneQubitMeasurementGate[T]: ) def __repr__(self) -> str: + tag_repr = f"[{self._tag}]" if self._tag is not None else "" return ( f"{'!' if self.is_inverted else ''}" - f"{self.stim_string}({self.qubit}, probability={self.probability})" + f"{self.stim_string}{tag_repr}({self.qubit}, " + f"probability={self.probability})" ) @@ -284,7 +300,8 @@ class TwoOperandGate(Gate, Generic[UT, VT]): stim_string: ClassVar[str] - def __init__(self, operand1: UT | T, operand2: VT | T): + def __init__(self, operand1: UT | T, operand2: VT | T, tag: str | None = None): + super().__init__(tag) operand1 = cast( UT, operand1 @@ -327,7 +344,7 @@ def stim_targets( @classmethod def from_consecutive( - cls: Type[TwoOperandGateT], pairs: Sequence[UT | VT | T] + cls: Type[TwoOperandGateT], pairs: Sequence[UT | VT | T], tag: str | None = None ) -> Generator[TwoOperandGateT, None, None]: """Yield an class instance for each pair in a flattened sequence of data. @@ -336,6 +353,8 @@ def from_consecutive( ---------- pairs : Sequence[UT | VT | T] The flat sequence of data. Length must be a multiple of 2. + tag : str | None + Optional tag for the constructed gate. Yields ------ @@ -347,10 +366,11 @@ def from_consecutive( "Two qubit gates can only be constructed from an even number of qubits" ) for control, target in zip(pairs[::2], pairs[1::2], strict=True): - yield cls(control, target) + yield cls(control, target, tag=tag) def __repr__(self) -> str: - return f"{self.stim_string}({self._operand1}, {self._operand2})" + tag_repr = f"[{self._tag}]" if self._tag is not None else "" + return f"{self.stim_string}{tag_repr}({self._operand1}, {self._operand2})" class SymmetricTwoQubitGate(TwoOperandGate[Qubit[T], Qubit[T]]): @@ -414,9 +434,9 @@ class ControlledGate(TwoOperandGate[UT, VT]): The string that stim associates with this gate. """ - def __init__(self, control: UT | T, target: VT | T): + def __init__(self, control: UT | T, target: VT | T, tag: str | None = None): # pylint: disable=useless-super-delegation - super().__init__(control, target) + super().__init__(control, target, tag) @property def qubits(self) -> Tuple[Qubit[T], ...]: @@ -448,4 +468,8 @@ def __hash__(self) -> int: return hash((self.__class__, self.control, self.target)) def __repr__(self) -> str: - return f"{self.stim_string}(control={self.control}, target={self.target})" + tag_repr = f"[{self._tag}]" if self._tag is not None else "" + return ( + f"{self.stim_string}{tag_repr}(control={self.control}, " + f"target={self.target})" + ) diff --git a/deltakit-circuit/src/deltakit_circuit/gates/_measurement_gates.py b/deltakit-circuit/src/deltakit_circuit/gates/_measurement_gates.py index f8f2ada0..d3289522 100644 --- a/deltakit-circuit/src/deltakit_circuit/gates/_measurement_gates.py +++ b/deltakit-circuit/src/deltakit_circuit/gates/_measurement_gates.py @@ -344,7 +344,9 @@ def __init__( MeasurementPauliProduct[T], ], probability: float = 0.0, + tag: str | None = None, ): + super().__init__(tag) if not 0 <= probability <= 1: raise ValueError("Probability must be between zero and one.") self._probability = probability @@ -423,8 +425,10 @@ def __hash__(self) -> int: return hash((self.__class__, self._pauli_product, self._probability)) def __repr__(self) -> str: + tag_repr = f"[{self._tag}]" if self._tag is not None else "" return ( - f"{self.stim_string}({self.pauli_product}, probability={self.probability})" + f"{self.stim_string}{tag_repr}({self.pauli_product}, " + f"probability={self.probability})" ) diff --git a/deltakit-circuit/src/deltakit_circuit/noise_channels/_abstract_noise_channels.py b/deltakit-circuit/src/deltakit_circuit/noise_channels/_abstract_noise_channels.py index 9c4f7e50..95a00ff8 100644 --- a/deltakit-circuit/src/deltakit_circuit/noise_channels/_abstract_noise_channels.py +++ b/deltakit-circuit/src/deltakit_circuit/noise_channels/_abstract_noise_channels.py @@ -43,6 +43,14 @@ class NoiseChannel(ABC, Generic[T]): stim_string: ClassVar[str] + def __init__(self, *args, tag: str | None = None, **kwargs) -> None: + super().__init__() + self._tag = tag + + @property + def tag(self) -> str | None: + return self._tag + @property @abstractmethod def qubits(self) -> Tuple[Qubit[T], ...]: @@ -144,8 +152,10 @@ class OneProbabilityNoiseChannel(NoiseChannel[T]): The probability that this error occurs. """ - def __init__(self, probability: float, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) + def __init__( + self, probability: float, *args, tag: str | None = None, **kwargs + ) -> None: + super().__init__(*args, tag=tag, **kwargs) if not 0 <= probability <= 1: raise ProbabilityError() self._probability = probability @@ -161,7 +171,9 @@ def probabilities(self) -> Tuple[float]: @property def stim_identifier(self) -> NoiseStimIdentifier: - return NoiseStimIdentifier(self.__class__.stim_string, (self.probability,)) + return NoiseStimIdentifier( + self.__class__.stim_string, (self.probability,), self.tag + ) class MultiProbabilityNoiseChannel(NoiseChannel[T]): @@ -173,8 +185,10 @@ class MultiProbabilityNoiseChannel(NoiseChannel[T]): The string that stim associates to this gate. """ - def __init__(self, probabilities: Tuple[float, ...], *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__( + self, probabilities: Tuple[float, ...], *args, tag: str | None = None, **kwargs + ): + super().__init__(*args, tag=tag, **kwargs) if not all(0 <= probability <= 1 for probability in probabilities): raise ProbabilityError() if sum(probabilities) > 1: @@ -188,7 +202,9 @@ def probabilities(self) -> Tuple[float, ...]: @property def stim_identifier(self) -> NoiseStimIdentifier: - return NoiseStimIdentifier(self.__class__.stim_string, self.probabilities) + return NoiseStimIdentifier( + self.__class__.stim_string, self.probabilities, self.tag + ) OneQubitNoiseChannelT = TypeVar("OneQubitNoiseChannelT", bound="OneQubitNoiseChannel") @@ -208,8 +224,8 @@ class OneQubitNoiseChannel(NoiseChannel[T]): The qubit that this noise channel error acts on. """ - def __init__(self, qubit: Qubit[T] | T, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, qubit: Qubit[T] | T, *args, tag: str | None = None, **kwargs): + super().__init__(*args, tag=tag, **kwargs) self._qubit = Qubit(qubit) if not isinstance(qubit, Qubit) else qubit @property @@ -232,13 +248,16 @@ def stim_targets( @classmethod def generator_from_prob( - cls: Type[OneQubitNoiseChannelT], *gate_args, **gate_kwargs + cls: Type[OneQubitNoiseChannelT], + *gate_args, + tag: str | None = None, + **gate_kwargs, ) -> Callable[[Iterable[Qubit[T]] | T], List[OneQubitNoiseChannelT]]: """Return a classmethod that can be used to create a noise channel with a predetermined probability""" def inner_gen(qubits) -> List[OneQubitNoiseChannelT]: - return [cls(qubit, *gate_args, **gate_kwargs) for qubit in qubits] + return [cls(qubit, *gate_args, tag=tag, **gate_kwargs) for qubit in qubits] return inner_gen @@ -262,8 +281,15 @@ class TwoQubitNoiseChannel(NoiseChannel[T]): The second qubit in the noise channel. """ - def __init__(self, qubit1: Qubit[T] | T, qubit2: Qubit[T] | T, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__( + self, + qubit1: Qubit[T] | T, + qubit2: Qubit[T] | T, + *args, + tag: str | None = None, + **kwargs, + ): + super().__init__(tag, *args, tag=tag, **kwargs) qubit1 = Qubit(qubit1) if not isinstance(qubit1, Qubit) else qubit1 qubit2 = Qubit(qubit2) if not isinstance(qubit2, Qubit) else qubit2 if qubit1 == qubit2: @@ -302,13 +328,18 @@ def stim_targets( @classmethod def generator_from_prob( - cls: Type[TwoQubitNoiseChannelT], *gate_args, **gate_kwargs + cls: Type[TwoQubitNoiseChannelT], + *gate_args, + tag: str | None = None, + **gate_kwargs, ) -> Callable[[Sequence[Qubit[T] | T]], List[TwoQubitNoiseChannelT]]: """Return a classmethod that can be used to create a noise channel with a predetermined probability""" def inner_gen(qubits: Sequence[Qubit[T] | T]) -> List[TwoQubitNoiseChannelT]: - return list(cls.from_consecutive(qubits, *gate_args, **gate_kwargs)) + return list( + cls.from_consecutive(qubits, *gate_args, tag=tag, **gate_kwargs) + ) return inner_gen @@ -317,6 +348,7 @@ def from_consecutive( cls: Type[TwoQubitNoiseChannelT], pairs: Sequence[Qubit[T] | T], *gate_args, + tag: str | None = None, **gate_kwargs, ) -> Generator[TwoQubitNoiseChannelT, None, None]: """Yield a class instance for each pair in a flattened sequence of @@ -326,6 +358,8 @@ def from_consecutive( ---------- pairs : Sequence[Qubit[T] | T] The flat sequence of data. Length must be a multiple of 2. + tag : str | None + An optional tag for the instruction. Yields ------ @@ -338,7 +372,7 @@ def from_consecutive( "constructed from an even number of qubits" ) for qubit1, qubit2 in zip(pairs[::2], pairs[1::2], strict=True): - yield cls(qubit1, qubit2, *gate_args, **gate_kwargs) + yield cls(qubit1, qubit2, *gate_args, tag=tag, **gate_kwargs) NC = TypeVar("NC", bound="OneQubitOneProbabilityNoiseChannel") @@ -363,8 +397,8 @@ class OneQubitOneProbabilityNoiseChannel( The probability that this error occurs. """ - def __init__(self, qubit: Qubit[T] | T, probability: float): - super().__init__(qubit, probability) + def __init__(self, qubit: Qubit[T] | T, probability: float, tag: str | None = None): + super().__init__(qubit, probability, tag=tag) def approx_equals( self, other: object, *, rel_tol: float = 1e-9, abs_tol: float = 0 @@ -388,7 +422,11 @@ def __hash__(self) -> int: return hash((self.__class__, self.qubit, self.probability)) def __repr__(self) -> str: - return f"{self.stim_string}({self.qubit}, probability={self.probability})" + tag_repr = f"[{self._tag}]" if self._tag is not None else "" + return ( + f"{self.stim_string}{tag_repr}({self.qubit}, " + f"probability={self.probability})" + ) PPN = TypeVar("PPN", bound="PauliProductNoise") @@ -416,8 +454,9 @@ def __init__( self, pauli_product: _PauliGate | Iterable[_PauliGate] | PauliProduct[T], probability: float, + tag: str | None = None, ): - super().__init__(probability) + super().__init__(probability, tag=tag) self._pauli_product = ( pauli_product if isinstance(pauli_product, PauliProduct) @@ -426,7 +465,10 @@ def __init__( @classmethod def generator_from_prob( - cls: Type[PPN], pauli_gate_t: Type[_PauliGate], probability: float + cls: Type[PPN], + pauli_gate_t: Type[_PauliGate], + probability: float, + tag: str | None = None, ) -> Callable[[Sequence[Qubit[T] | T]], Sequence[PPN]]: """Return a classmethod that can be used to create a noise channel with a predetermined probability""" @@ -434,7 +476,9 @@ def generator_from_prob( def inner_gen(qubits: Sequence[Qubit[T] | T]) -> List[PPN]: return [ cls( - PauliProduct([pauli_gate_t(qubit) for qubit in qubits]), probability + PauliProduct([pauli_gate_t(qubit) for qubit in qubits]), + probability, + tag=tag, ) ] @@ -479,6 +523,8 @@ def __hash__(self) -> int: return hash((self._pauli_product, self._probability)) def __repr__(self) -> str: + tag_repr = f"[{self._tag}]" if self._tag is not None else "" return ( - f"{self.stim_string}({self.pauli_product}, probability={self.probability})" + f"{self.stim_string}{tag_repr}({self.pauli_product}, " + f"probability={self.probability})" ) diff --git a/deltakit-circuit/src/deltakit_circuit/noise_channels/_depolarising_noise.py b/deltakit-circuit/src/deltakit_circuit/noise_channels/_depolarising_noise.py index 969e45a4..b039afd4 100644 --- a/deltakit-circuit/src/deltakit_circuit/noise_channels/_depolarising_noise.py +++ b/deltakit-circuit/src/deltakit_circuit/noise_channels/_depolarising_noise.py @@ -73,8 +73,14 @@ class Depolarise2(OneProbabilityNoiseChannel[T], TwoQubitNoiseChannel[T]): stim_string: ClassVar[str] = "DEPOLARIZE2" - def __init__(self, qubit1: Qubit[T] | T, qubit2: Qubit[T] | T, probability: float): - super().__init__(qubit1=qubit1, qubit2=qubit2, probability=probability) + def __init__( + self, + qubit1: Qubit[T] | T, + qubit2: Qubit[T] | T, + probability: float, + tag: str | None = None, + ): + super().__init__(qubit1=qubit1, qubit2=qubit2, probability=probability, tag=tag) def approx_equals( self, other: object, *, rel_tol: float = 1e-9, abs_tol: float = 0 @@ -98,8 +104,9 @@ def __hash__(self) -> int: return hash((self.__class__, self._qubit1, self._qubit2, self.probability)) def __repr__(self) -> str: + tag_repr = f"[{self.tag}]" if self.tag is not None else "" return ( - f"{self.stim_string}" + f"{self.stim_string}{tag_repr}" f"(qubit1={self._qubit1}, qubit2={self._qubit2}, " f"probability={self.probability})" ) diff --git a/deltakit-circuit/src/deltakit_circuit/noise_channels/_pauli_noise.py b/deltakit-circuit/src/deltakit_circuit/noise_channels/_pauli_noise.py index 513f34ad..ba4b8a49 100644 --- a/deltakit-circuit/src/deltakit_circuit/noise_channels/_pauli_noise.py +++ b/deltakit-circuit/src/deltakit_circuit/noise_channels/_pauli_noise.py @@ -102,9 +102,14 @@ class PauliChannel1(OneQubitNoiseChannel[T], MultiProbabilityNoiseChannel[T]): stim_string: ClassVar[str] = "PAULI_CHANNEL_1" def __init__( - self, qubit: Qubit[T] | T, p_x: float = 0.0, p_y: float = 0.0, p_z: float = 0.0 + self, + qubit: Qubit[T] | T, + p_x: float = 0.0, + p_y: float = 0.0, + p_z: float = 0.0, + tag: str | None = None, ): - super().__init__(qubit=qubit, probabilities=(p_x, p_y, p_z)) + super().__init__(qubit=qubit, probabilities=(p_x, p_y, p_z), tag=tag) self.p_x = p_x self.p_y = p_y self.p_z = p_z @@ -135,8 +140,9 @@ def __hash__(self) -> int: return hash((self.__class__, self.qubit, self.probabilities)) def __repr__(self) -> str: + tag_repr = f"[{self.tag}]" if self.tag is not None else "" return ( - f"{self.stim_string}({self.qubit}, p_x={self.p_x}, " + f"{self.stim_string}{tag_repr}({self.qubit}, p_x={self.p_x}, " f"p_y={self.p_y}, p_z={self.p_z})" ) @@ -228,6 +234,7 @@ def __init__( # noqa: PLR0913 p_zx: float = 0.0, p_zy: float = 0.0, p_zz: float = 0.0, + tag: str | None = None, ): probabilities = ( p_ix, @@ -246,7 +253,9 @@ def __init__( # noqa: PLR0913 p_zy, p_zz, ) - super().__init__(qubit1=qubit1, qubit2=qubit2, probabilities=probabilities) + super().__init__( + qubit1=qubit1, qubit2=qubit2, probabilities=probabilities, tag=tag + ) self.p_ix = p_ix self.p_iy = p_iy self.p_iz = p_iz @@ -274,8 +283,9 @@ def __hash__(self) -> int: return hash((self.__class__, self._qubit1, self._qubit2, self.probabilities)) def __repr__(self) -> str: + tag_repr = f"[{self.tag}]" if self.tag is not None else "" return ( - f"{self.stim_string}" + f"{self.stim_string}{tag_repr}" f"(qubit1={self._qubit1}, qubit2={self._qubit2}, " f"p_ix={self.p_ix}, p_iy={self.p_iy}, p_iz={self.p_iz}, " f"p_xi={self.p_xi}, p_xx={self.p_xx}, p_xy={self.p_xy}, " diff --git a/deltakit-circuit/tests/test_annotations/test_detector.py b/deltakit-circuit/tests/test_annotations/test_detector.py index 1925a6fa..ac5360a9 100644 --- a/deltakit-circuit/tests/test_annotations/test_detector.py +++ b/deltakit-circuit/tests/test_annotations/test_detector.py @@ -132,6 +132,13 @@ def test_passing_iterable_for_coordinates_converts_it_to_coordinate_class( Detector(MeasurementRecord(-1), iterable).coordinate, Coordinate ) + @pytest.mark.parametrize("tag", [None, "", "sjkdhf", "λ", "leaky<0>"]) + def test_tag_on_instruction(self, tag: str | None) -> None: + detector = Detector( + [MeasurementRecord(-1), MeasurementRecord(-2)], Coordinate(0, 1, 2), tag=tag + ) + assert detector.tag == tag + class TestCoordinateTransforms: def test_detector_with_none_coordinate_maps_to_none(self): diff --git a/deltakit-circuit/tests/test_annotations/test_observable.py b/deltakit-circuit/tests/test_annotations/test_observable.py index 59772364..bb9d22e2 100644 --- a/deltakit-circuit/tests/test_annotations/test_observable.py +++ b/deltakit-circuit/tests/test_annotations/test_observable.py @@ -87,3 +87,9 @@ def test_observable_index_write_protected(): observable = Observable(0, MeasurementRecord(-1)) with pytest.raises(AttributeError): observable.observable_index = 4 + + +@pytest.mark.parametrize("tag", [None, "", "sjkdhf", "λ", "leaky<0>"]) +def test_tag_on_instruction(tag: str | None) -> None: + obs = Observable(0, [MeasurementRecord(-1), MeasurementRecord(-2)], tag=tag) + assert obs.tag == tag diff --git a/deltakit-circuit/tests/test_annotations/test_shift_coordinates.py b/deltakit-circuit/tests/test_annotations/test_shift_coordinates.py index a3a60510..6b2edbde 100644 --- a/deltakit-circuit/tests/test_annotations/test_shift_coordinates.py +++ b/deltakit-circuit/tests/test_annotations/test_shift_coordinates.py @@ -1,4 +1,5 @@ # (c) Copyright Riverlane 2020-2025. +import pytest import stim from deltakit_circuit import ShiftCoordinates @@ -18,3 +19,9 @@ def test_shift_coordinate_instances_are_not_equal_if_their_shifts_are_different( def test_repr_of_shift_coordinates_matches_expected_representation(): assert repr(ShiftCoordinates((0, 1, 2))) == "ShiftCoordinates(Coordinate(0, 1, 2))" + + +@pytest.mark.parametrize("tag", [None, "", "sjkdhf", "λ", "leaky<0>"]) +def test_tag_on_instruction(tag: str | None) -> None: + instr = ShiftCoordinates((0, 1, 2), tag=tag) + assert instr.tag == tag diff --git a/deltakit-circuit/tests/test_gates/test_measurement_gates.py b/deltakit-circuit/tests/test_gates/test_measurement_gates.py index 284e544c..856b288a 100644 --- a/deltakit-circuit/tests/test_gates/test_measurement_gates.py +++ b/deltakit-circuit/tests/test_gates/test_measurement_gates.py @@ -1,5 +1,6 @@ # (c) Copyright Riverlane 2020-2025. from itertools import permutations +import itertools import pytest from deltakit_circuit import ( @@ -53,27 +54,37 @@ def test_one_qubit_measurement_gate_bases_match_expected_basis( @pytest.mark.parametrize( - "measurement_gate_class", gates.MEASUREMENT_GATES - {gates.MPP} + "measurement_gate_class,tag,probability", + itertools.product( + gates.MEASUREMENT_GATES - {gates.MPP}, + [None, "", "sjkdhf", "λ", "leaky<0>"], + [0.0, 0.02], + ), ) -@pytest.mark.parametrize("probability", [0.0, 0.02]) def test_repr_of_non_inverted_one_qubit_measurement_gate_matches_expected_representation( - measurement_gate_class, probability -): + measurement_gate_class, tag: str | None, probability: float +) -> None: + tag_repr = f"[{tag}]" if tag is not None else "" assert ( - repr(measurement_gate_class(Qubit(3), probability, False)) - == f"{measurement_gate_class.stim_string}(Qubit(3), probability={probability})" + repr(measurement_gate_class(Qubit(3), probability, False, tag=tag)) + == f"{measurement_gate_class.stim_string}{tag_repr}(Qubit(3), probability={probability})" ) @pytest.mark.parametrize( - "measurement_gate_class", gates.MEASUREMENT_GATES - {gates.MPP} + "measurement_gate_class,tag", + itertools.product( + gates.MEASUREMENT_GATES - {gates.MPP}, + [None, "", "sjkdhf", "λ", "leaky<0>"], + ), ) def test_repr_of_inverted_one_qubit_measurement_gate_matches_expected_representation( - measurement_gate_class, -): + measurement_gate_class, tag: str | None +) -> None: + tag_repr = f"[{tag}]" if tag is not None else "" assert ( - repr(measurement_gate_class(Qubit(3), 0.1, True)) - == f"!{measurement_gate_class.stim_string}(Qubit(3), probability=0.1)" + repr(measurement_gate_class(Qubit(3), 0.1, True, tag=tag)) + == f"!{measurement_gate_class.stim_string}{tag_repr}(Qubit(3), probability=0.1)" ) @@ -88,6 +99,12 @@ def test_repr_of_inverted_one_qubit_measurement_gate_matches_expected_representa gates.MPP(MeasurementPauliProduct([PauliY(0), PauliZ(1)]), 0.02), "MPP([PauliY(Qubit(0)), PauliZ(Qubit(1))], probability=0.02)", ), + ( + gates.MPP( + MeasurementPauliProduct([PauliY(0), PauliZ(1)]), 0.02, tag="λ<0>" + ), + "MPP[λ<0>]([PauliY(Qubit(0)), PauliZ(Qubit(1))], probability=0.02)", + ), ], ) def test_repr_of_mpp_gate_matches_expected_representation( diff --git a/deltakit-circuit/tests/test_gates/test_one_qubit_gates.py b/deltakit-circuit/tests/test_gates/test_one_qubit_gates.py index 2f95086c..1a04b47e 100644 --- a/deltakit-circuit/tests/test_gates/test_one_qubit_gates.py +++ b/deltakit-circuit/tests/test_gates/test_one_qubit_gates.py @@ -1,5 +1,6 @@ # (c) Copyright Riverlane 2020-2025. from itertools import permutations +import itertools import pytest import stim @@ -33,9 +34,18 @@ def test_one_qubit_gate_stim_string_matches_expected_string( assert one_qubit_gate.stim_string == expected_string -@pytest.mark.parametrize("one_qubit_gate", gates.ONE_QUBIT_GATES) -def test_one_qubit_gates_repr_matches_expected_representation(one_qubit_gate): - assert repr(one_qubit_gate(Qubit(0))) == f"{one_qubit_gate.stim_string}(Qubit(0))" +@pytest.mark.parametrize( + "one_qubit_gate,tag", + itertools.product(gates.ONE_QUBIT_GATES, [None, "", "sjkdhf", "λ", "leaky<0>"]), +) +def test_one_qubit_gates_repr_matches_expected_representation( + one_qubit_gate, tag: str | None +) -> None: + tag_repr = f"[{tag}]" if tag is not None else "" + assert ( + repr(one_qubit_gate(Qubit(0), tag=tag)) + == f"{one_qubit_gate.stim_string}{tag_repr}(Qubit(0))" + ) @pytest.mark.parametrize("one_qubit_gate", gates.ONE_QUBIT_GATES) diff --git a/deltakit-circuit/tests/test_gates/test_reset_gates.py b/deltakit-circuit/tests/test_gates/test_reset_gates.py index 42107de4..fe76df42 100644 --- a/deltakit-circuit/tests/test_gates/test_reset_gates.py +++ b/deltakit-circuit/tests/test_gates/test_reset_gates.py @@ -1,5 +1,6 @@ # (c) Copyright Riverlane 2020-2025. from itertools import permutations +import itertools import pytest import stim @@ -31,9 +32,18 @@ def test_reset_gate_basis_is_expected_basis(reset_gate, expected_basis): assert reset_gate.basis == expected_basis -@pytest.mark.parametrize("reset_gate", gates.RESET_GATES) -def test_repr_of_reset_gate_matches_the_expected_representation(reset_gate): - assert repr(reset_gate(Qubit(3))) == f"{reset_gate.stim_string}(Qubit(3))" +@pytest.mark.parametrize( + "reset_gate,tag", + itertools.product(gates.RESET_GATES, [None, "", "sjkdhf", "λ", "leaky<0>"]), +) +def test_repr_of_reset_gate_matches_the_expected_representation( + reset_gate, tag: str | None +) -> None: + tag_repr = f"[{tag}]" if tag is not None else "" + assert ( + repr(reset_gate(Qubit(3), tag=tag)) + == f"{reset_gate.stim_string}{tag_repr}(Qubit(3))" + ) @pytest.mark.parametrize("reset_gate", gates.RESET_GATES) diff --git a/deltakit-circuit/tests/test_gates/test_two_qubit_gates.py b/deltakit-circuit/tests/test_gates/test_two_qubit_gates.py index a60a5fd7..fe6497fd 100644 --- a/deltakit-circuit/tests/test_gates/test_two_qubit_gates.py +++ b/deltakit-circuit/tests/test_gates/test_two_qubit_gates.py @@ -1,5 +1,6 @@ # (c) Copyright Riverlane 2020-2025. from itertools import chain, permutations, product +import itertools import pytest import stim @@ -125,19 +126,31 @@ def test_classically_targeted_gate_from_consecutive_gives_expected_target( ) -@pytest.mark.parametrize("two_qubit_gate", CONTROLLED_GATES) -def test_repr_of_controlled_gates_matches_expected_representation(two_qubit_gate): +@pytest.mark.parametrize( + "two_qubit_gate,tag", + itertools.product(CONTROLLED_GATES, [None, "", "sjkdhf", "λ", "leaky<0>"]), +) +def test_repr_of_controlled_gates_matches_expected_representation( + two_qubit_gate, tag: str | None +) -> None: + tag_repr = f"[{tag}]" if tag is not None else "" assert ( - repr(two_qubit_gate(Qubit(1), Qubit(2))) - == f"{two_qubit_gate.stim_string}(control=Qubit(1), target=Qubit(2))" + repr(two_qubit_gate(Qubit(1), Qubit(2), tag=tag)) + == f"{two_qubit_gate.stim_string}{tag_repr}(control=Qubit(1), target=Qubit(2))" ) -@pytest.mark.parametrize("two_qubit_gate", UNCONTROLLED_GATES) -def test_repr_of_uncontrolled_gate_matches_expected_representation(two_qubit_gate): +@pytest.mark.parametrize( + "two_qubit_gate,tag", + itertools.product(UNCONTROLLED_GATES, [None, "", "sjkdhf", "λ", "leaky<0>"]), +) +def test_repr_of_uncontrolled_gate_matches_expected_representation( + two_qubit_gate, tag: str | None +) -> None: + tag_repr = f"[{tag}]" if tag is not None else "" assert ( - repr(two_qubit_gate(Qubit(1), Qubit(0))) - == f"{two_qubit_gate.stim_string}(Qubit(1), Qubit(0))" + repr(two_qubit_gate(Qubit(1), Qubit(0), tag=tag)) + == f"{two_qubit_gate.stim_string}{tag_repr}(Qubit(1), Qubit(0))" ) diff --git a/deltakit-circuit/tests/test_noise_channels/test_correlated_noise.py b/deltakit-circuit/tests/test_noise_channels/test_correlated_noise.py index c4b3d5fe..75d9b5f9 100644 --- a/deltakit-circuit/tests/test_noise_channels/test_correlated_noise.py +++ b/deltakit-circuit/tests/test_noise_channels/test_correlated_noise.py @@ -44,6 +44,10 @@ def test_correlated_error_stim_string_matches_expected_string( CorrelatedError(PauliX(Qubit(0)), probability=0.002), "CORRELATED_ERROR([PauliX(Qubit(0))], probability=0.002)", ), + ( + CorrelatedError(PauliX(Qubit(0)), probability=0.002, tag="λ<0>"), + "CORRELATED_ERROR[λ<0>]([PauliX(Qubit(0))], probability=0.002)", + ), ( ElseCorrelatedError( PauliProduct([PauliZ(Qubit(0)), PauliY(Qubit(1))]), 0.02 @@ -54,6 +58,10 @@ def test_correlated_error_stim_string_matches_expected_string( ElseCorrelatedError(PauliX(Qubit(0)), probability=0.002), "ELSE_CORRELATED_ERROR([PauliX(Qubit(0))], probability=0.002)", ), + ( + ElseCorrelatedError(PauliX(Qubit(0)), probability=0.002, tag="λ<0>"), + "ELSE_CORRELATED_ERROR[λ<0>]([PauliX(Qubit(0))], probability=0.002)", + ), ], ) def test_repr_of_correlated_noise_matches_expected_representation( diff --git a/deltakit-circuit/tests/test_noise_channels/test_depolarising_noise.py b/deltakit-circuit/tests/test_noise_channels/test_depolarising_noise.py index 05a33046..8c086eb2 100644 --- a/deltakit-circuit/tests/test_noise_channels/test_depolarising_noise.py +++ b/deltakit-circuit/tests/test_noise_channels/test_depolarising_noise.py @@ -114,10 +114,18 @@ def test_stim_identifier_matches_expected_stim_identifier(noise_channel): "depolarising_noise, expected_repr", [ (Depolarise1(Qubit(0), 0.001), "DEPOLARIZE1(Qubit(0), probability=0.001)"), + ( + Depolarise1(Qubit(0), 0.001, tag="λ<0>"), + "DEPOLARIZE1[λ<0>](Qubit(0), probability=0.001)", + ), ( Depolarise2(Qubit(0), Qubit(1), 0.01), "DEPOLARIZE2(qubit1=Qubit(0), qubit2=Qubit(1), probability=0.01)", ), + ( + Depolarise2(Qubit(0), Qubit(1), 0.01, tag="λ<0>"), + "DEPOLARIZE2[λ<0>](qubit1=Qubit(0), qubit2=Qubit(1), probability=0.01)", + ), ], ) def test_repr_of_depolarising_noise_matches_expected_representation( diff --git a/deltakit-circuit/tests/test_noise_channels/test_leakage_noise.py b/deltakit-circuit/tests/test_noise_channels/test_leakage_noise.py index 56833be8..245a1eb9 100644 --- a/deltakit-circuit/tests/test_noise_channels/test_leakage_noise.py +++ b/deltakit-circuit/tests/test_noise_channels/test_leakage_noise.py @@ -87,6 +87,11 @@ def test_stim_identifier_matches_expected_stim_identifier(noise_channel): [ (Leakage(Qubit(0), 0.001), "LEAKAGE(Qubit(0), probability=0.001)"), (Relax(Qubit(0), 0.01), "RELAX(Qubit(0), probability=0.01)"), + ( + Leakage(Qubit(0), 0.001, tag="λ<0>"), + "LEAKAGE[λ<0>](Qubit(0), probability=0.001)", + ), + (Relax(Qubit(0), 0.01, tag="λ<0>"), "RELAX[λ<0>](Qubit(0), probability=0.01)"), ], ) def test_repr_of_leakage_noise_matches_expected_representation( diff --git a/deltakit-circuit/tests/test_noise_channels/test_pauli_noise.py b/deltakit-circuit/tests/test_noise_channels/test_pauli_noise.py index fa281dae..802253a6 100644 --- a/deltakit-circuit/tests/test_noise_channels/test_pauli_noise.py +++ b/deltakit-circuit/tests/test_noise_channels/test_pauli_noise.py @@ -101,6 +101,49 @@ def test_stim_identifier_matches_expected_identifier_for_single_probability_nois "p_yi=0.008, p_yx=0.009, p_yy=0.01, p_yz=0.011, " "p_zi=0.012, p_zx=0.013, p_zy=0.014, p_zz=0.015)", ), + ( + PauliXError(Qubit(0), 0.02, tag="λ<0>"), + "X_ERROR[λ<0>](Qubit(0), probability=0.02)", + ), + ( + PauliYError(Qubit(1), 0.01, tag="λ<0>"), + "Y_ERROR[λ<0>](Qubit(1), probability=0.01)", + ), + ( + PauliZError(Qubit(2), 0.03, tag="λ<0>"), + "Z_ERROR[λ<0>](Qubit(2), probability=0.03)", + ), + ( + PauliChannel1(Qubit(0), 0.01, 0.02, 0.03, tag="λ<0>"), + "PAULI_CHANNEL_1[λ<0>](Qubit(0), p_x=0.01, p_y=0.02, p_z=0.03)", + ), + ( + PauliChannel2( + Qubit(0), + Qubit(1), + 0.001, + 0.002, + 0.003, + 0.004, + 0.005, + 0.006, + 0.007, + 0.008, + 0.009, + 0.01, + 0.011, + 0.012, + 0.013, + 0.014, + 0.015, + tag="λ<0>", + ), + "PAULI_CHANNEL_2[λ<0>](qubit1=Qubit(0), qubit2=Qubit(1), " + "p_ix=0.001, p_iy=0.002, p_iz=0.003, " + "p_xi=0.004, p_xx=0.005, p_xy=0.006, p_xz=0.007, " + "p_yi=0.008, p_yx=0.009, p_yy=0.01, p_yz=0.011, " + "p_zi=0.012, p_zx=0.013, p_zy=0.014, p_zz=0.015)", + ), ], ) def test_pauli_noise_channel_repr_matches_expected_representation( diff --git a/deltakit-circuit/tests/test_stim_parsing/test_gate_parsing.py b/deltakit-circuit/tests/test_stim_parsing/test_gate_parsing.py index 3e0d4734..e0cff872 100644 --- a/deltakit-circuit/tests/test_stim_parsing/test_gate_parsing.py +++ b/deltakit-circuit/tests/test_stim_parsing/test_gate_parsing.py @@ -11,7 +11,7 @@ def test_probability_is_added_on_measurement_gates(): probability = 0.001 layer = parse_stim_gate_instruction( - sp.gates.MZ, [stim.GateTarget(0)], [probability], qubit_mapping={} + sp.gates.MZ, [stim.GateTarget(0)], [probability], "", qubit_mapping={} ) assert list(layer.gates)[0].probability == probability @@ -29,7 +29,7 @@ def test_error_is_raised_when_noise_class_is_passed_to_gate_parser(): "valid deltakit_circuit gate.", ): parse_stim_gate_instruction( - sp.noise_channels.PauliXError, [0, 1], [], qubit_mapping={} + sp.noise_channels.PauliXError, [0, 1], [], "", qubit_mapping={} ) diff --git a/deltakit-circuit/tests/test_stim_parsing/test_noise_parsing.py b/deltakit-circuit/tests/test_stim_parsing/test_noise_parsing.py index 7a123708..913f5662 100644 --- a/deltakit-circuit/tests/test_stim_parsing/test_noise_parsing.py +++ b/deltakit-circuit/tests/test_stim_parsing/test_noise_parsing.py @@ -11,6 +11,7 @@ def test_probability_is_added_on_noise_channels(): sp.noise_channels.PauliXError, [stim.GateTarget(0)], [probability], + instruction_tag="", qubit_mapping={}, ) assert layer.noise_channels[0].probability == probability @@ -22,7 +23,9 @@ def test_error_is_raised_when_gate_class_is_passed_to_the_noise_parser(): match=r"Given noise class:.*is not a " "valid deltakit_circuit noise channel.", ): - parse_stim_noise_instruction(sp.gates.X, [0, 2, 3], [0.01], qubit_mapping={}) + parse_stim_noise_instruction( + sp.gates.X, [0, 2, 3], [0.01], instruction_tag="", qubit_mapping={} + ) @pytest.mark.parametrize( diff --git a/deltakit-circuit/tests/test_stim_parsing/test_tag_parsing.py b/deltakit-circuit/tests/test_stim_parsing/test_tag_parsing.py new file mode 100644 index 00000000..a7ee5642 --- /dev/null +++ b/deltakit-circuit/tests/test_stim_parsing/test_tag_parsing.py @@ -0,0 +1,46 @@ +import itertools + +import pytest +import stim + +from deltakit_circuit._circuit import Circuit +from deltakit_circuit._qubit_identifiers import Coordinate, Qubit +from deltakit_circuit.gates._measurement_gates import ( + HERALD_LEAKAGE_EVENT, + MEASUREMENT_GATES, + MPP, +) +from deltakit_circuit.gates._one_qubit_gates import ONE_QUBIT_GATES +from deltakit_circuit.gates._reset_gates import RESET_GATES +from deltakit_circuit.gates._two_qubit_gates import TWO_QUBIT_GATES +from deltakit_circuit.noise_channels._correlated_noise import ALL_CORRELATED_NOISE + + +@pytest.fixture +def qubit_mapping() -> dict[int, Qubit]: + return {i: Qubit(Coordinate(i, i)) for i in range(3)} + + +@pytest.mark.parametrize( + "instr_template,tag", + itertools.product( + [ + *(f"{sqg.stim_string}[{{tag}}] 0" for sqg in ONE_QUBIT_GATES), + *(f"{tqg.stim_string}[{{tag}}] 0 1" for tqg in TWO_QUBIT_GATES), + *( + f"{mg.stim_string}[{{tag}}] 0" + for mg in (MEASUREMENT_GATES - {MPP, HERALD_LEAKAGE_EVENT}) + ), + *(f"{rg.stim_string}[{{tag}}] 0" for rg in RESET_GATES), + *(f"{cng.stim_string}[{{tag}}](0.1) X1 Z2" for cng in ALL_CORRELATED_NOISE), + ], + ["", "sjkdhf", "λ", "leaky<0>"], + ), +) +def test_parse_tagged_instruction( + instr_template: str, tag: str, qubit_mapping: dict[int, Qubit] +) -> None: + instr_str = instr_template.format(tag=tag) + stim_circuit = stim.Circuit(instr_str) + circuit = Circuit.from_stim_circuit(stim_circuit) + assert circuit.as_stim_circuit() == stim_circuit diff --git a/deltakit-explorer/src/deltakit_explorer/qpu/_qpu.py b/deltakit-explorer/src/deltakit_explorer/qpu/_qpu.py index 5d8aa3d7..b8d90d58 100644 --- a/deltakit-explorer/src/deltakit_explorer/qpu/_qpu.py +++ b/deltakit-explorer/src/deltakit_explorer/qpu/_qpu.py @@ -243,7 +243,7 @@ def get_circuit_execution_time(self, circuit: Circuit) -> float: total_time += schedule.previous_layer_times[-1] return total_time * circuit.iterations - def _add_noise_to_circuit(self, circuit: Circuit) -> Circuit: + def add_noise_to_circuit(self, circuit: Circuit) -> Circuit: """ Add noise to the supplied circuit based on the QPU's noise model. @@ -357,7 +357,7 @@ def compile_and_add_noise_to_circuit( Compiled circuit with noise added. """ return remove_identities( - self._add_noise_to_circuit( + self.add_noise_to_circuit( self.compile_circuit(circuit, remove_paulis) ) ) diff --git a/deltakit-explorer/tests/unit/noise_models/test_sd6_noise.py b/deltakit-explorer/tests/unit/noise_models/test_sd6_noise.py index 27e74fe8..c1296bba 100644 --- a/deltakit-explorer/tests/unit/noise_models/test_sd6_noise.py +++ b/deltakit-explorer/tests/unit/noise_models/test_sd6_noise.py @@ -159,7 +159,7 @@ def test_fixed_circuit_reset(self, noise_model): noise_model=noise_model, native_gates_and_times=native_gates, ) - noisy_circuit = qpu._add_noise_to_circuit(circuit) + noisy_circuit = qpu.add_noise_to_circuit(circuit) assert expected_noisy_circuit == noisy_circuit def test_sd6_noise_str(self): From 139b0643763afd8913527768a759a8c8113cfd1d Mon Sep 17 00:00:00 2001 From: Adrien Suau <12374487+nelimee@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:21:02 +0100 Subject: [PATCH 23/26] Ignore files generated by docs/guide/simulation.md (#118) Co-authored-by: Marco Ghibaudi <62563515+mghibaudi@users.noreply.github.com> --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index c865c10f..56e4f732 100644 --- a/.gitignore +++ b/.gitignore @@ -232,3 +232,8 @@ docs/explorer/simulation/ # mac .DS_Store + +# Generated by `simulation.md` +docs/**/measurements.01 +docs/**/dets_from_circuit_sampler.b8 +docs/**/dets_from_dem_sampler.01 From 17472be78c7abafb02cf00c6e532b0fd1be0d6a9 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Fri, 31 Oct 2025 06:24:40 -0700 Subject: [PATCH 24/26] docs: replace 'logical error rate' with 'logical error probability per round' (#108) * docs: replace 'logical error rate' with 'logical error probability per round' * Apply suggestions from code review * LER => LEP * Fix test * Fix documentation --------- Co-authored-by: Adrien Suau <12374487+nelimee@users.noreply.github.com> --- .../deltakit_explorer/analysis/_analysis.py | 103 +++++++++--------- .../tests/analysis/test_analysis.py | 10 +- .../google_qmem_experiment/decode.ipynb | 8 +- .../rotated_planar_quantum_memory_plots.ipynb | 32 +++--- .../notebooks/simulation/ac_decoding.ipynb | 2 +- .../simulation/leakage-aware_decoding.ipynb | 2 +- docs/guide/analysis.ipynb | 16 +-- docs/guide/decoding.md | 2 +- docs/guide/getting_started.md | 2 +- tests/qmem/test_qmem.py | 19 ++-- 10 files changed, 102 insertions(+), 94 deletions(-) diff --git a/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py b/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py index a942114d..b2fa2c06 100644 --- a/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py +++ b/deltakit-explorer/src/deltakit_explorer/analysis/_analysis.py @@ -25,7 +25,7 @@ def get_exp_fit( npt.NDArray[np.float64], ]: """ - Implement logical error rate fit as described in + Implement logical error probability per round fit as described in https://arxiv.org/pdf/2310.05900.pdf (p.40) and https://arxiv.org/pdf/2207.06431.pdf (p.21). The first round (`r=0`) data points are excluded as the error suppression is stronger there @@ -46,7 +46,7 @@ def get_exp_fit( Returns: Tuple[float, npt.NDArray, npt.NDArray, npt.NDArray]: A tuple consisting of - - an epsilon parameter value (error rate per round); + - an epsilon parameter value (error probability per round); - an interpolated graph (2 value), computed with this value; - error bars value. @@ -118,15 +118,15 @@ def get_exp_fit( raise @dataclass(frozen=True) -class LogicalErrorRatePerRoundResults: +class LogicalErrorProbabilityPerRoundResults: """Named-tuple-like class containing computation results from :func:`compute_logical_error_per_round`. Attributes: leppr (float): Logical Error Probability Per Round (LEPPR). leppr_stddev (float): LEPPR standard deviation. - spam_error (float): computed SPAM error-rate. - spam_error_stddev (float): SPAM error-rate standard deviation. + spam_error (float): computed SPAM error probability. + spam_error_stddev (float): SPAM error probability standard deviation. leppr_stddev_propagated (float): standard deviation due to the propagation of individual logical error probabilities standard deviations used to estimate the LEPPR. @@ -134,9 +134,9 @@ class LogicalErrorRatePerRoundResults: LEPPR computation. spam_error_stddev_propagated (float): standard deviation due to the propagation of individual logical error probabilities standard deviations used to - estimate the SPAM error-rate. + estimate the SPAM error probability. spam_error_stddev_fit (float): standard deviation due to the linear fit involved - in SPAM error-rate computation. + in SPAM error probability computation. Note: attributes ending in ``_stddev_propagated`` or ``_stddev_fit`` are internal @@ -160,9 +160,9 @@ def compute_logical_error_per_round( num_rounds: npt.NDArray[np.int_] | Sequence[int], *, force_include_single_round: bool = False, -) -> LogicalErrorRatePerRoundResults: - """Compute the logical error-rate per round from different logical error-rate - computations. +) -> LogicalErrorProbabilityPerRoundResults: + """Compute the logical error probability per round from different logical error + probability computations. This function implements the method described in: @@ -170,8 +170,8 @@ def compute_logical_error_per_round( 2. https://arxiv.org/pdf/2207.06431.pdf (p.21) 3. https://arxiv.org/pdf/2505.09684.pdf (p.8) - to recover an estimator of the logical error-rate per round from the estimated - values of logical error-rates for several round durations. + to recover an estimator of the logical error probability per round from the + estimated values of logical error probabilities for several round durations. Args: num_failed_shots (npt.NDArray[np.int_] | Sequence[int]): @@ -194,8 +194,8 @@ def compute_logical_error_per_round( error is assumed to be ``0`` and an estimation will still be returned. Heuristically, to increase the returned estimation precision, you should try - to provide data for rounds such that the estimated logical error-rate for - the number of rounds ``max(num_rounds)`` is approximately ``0.4``. This + to provide data for rounds such that the estimated logical error probability + for the number of rounds ``max(num_rounds)`` is approximately ``0.4``. This ``0.4`` value has been set to reduce fitting errors. force_include_single_round (bool): if ``True``, data obtained from 1-round experiment will be used in the @@ -260,27 +260,27 @@ def compute_logical_error_per_round( "shots to have at least one error, else the problem is ill-formed." ) - logical_error_rates = num_failed_shots / num_shots - fidelities = 1 - 2 * logical_error_rates + logical_error_probabilities = num_failed_shots / num_shots + fidelities = 1 - 2 * logical_error_probabilities if np.any(fidelities <= 0): raise RuntimeError( - "Got estimations of logical error-rates above 0.5. That is not supported " - "by this function. Please reduce your number of rounds. Estimated logical " - f"error-rates: {logical_error_rates}." + "Got estimations of logical error probability above 0.5. That is not " + "supported by this function. Please reduce your number of rounds. " + f"Estimated logical error probabilities: {logical_error_probabilities}." ) # Check if the heuristic guideline on the number of rounds is verified. - max_logical_error_rate = np.max(logical_error_rates) - if max_logical_error_rate < 0.2: + max_logical_error_probability = np.max(logical_error_probabilities) + if max_logical_error_probability < 0.2: warnings.warn( - f"The maximum estimated logical error-rate ({max_logical_error_rate}) is " - "below 0.2. The returned estimation might be better if you add data with " - "more rounds such that the maximum estimated logical error-rate is closer " - "to 0.4." + "The maximum estimated logical error probability " + f"({max_logical_error_probability}) is below 0.2. The returned estimation " + "might be better if you add data with more rounds such that the maximum " + "estimated logical error probability is closer to 0.4." ) # We want to do a linear regression on the log values of fidelity, and obtain the - # per-round error rate like that. + # per-round error probability like that. # Applying the logarithm function will change non-uniformly the standard deviation # of each variable, which makes the standard linear regression estimator biased. The # best linear unbiased estimator in that case is obtained by solving a weighted @@ -291,7 +291,7 @@ def compute_logical_error_per_round( # We approximate the standard deviation with an error propagation analysis. This # method has been tested against scipy and returns similar results. # Alias for more readability - pl = logical_error_rates + pl = logical_error_probabilities pl_stddev = np.sqrt(pl * (1 - pl) / num_shots) logfidelities_stddev = 2 * pl_stddev / fidelities @@ -308,8 +308,8 @@ def compute_logical_error_per_round( # quantity, so make it very small. logfidelities_stddev = np.hstack([[1e-12], logfidelities_stddev]) - # Note that the covariance matrix is used later to estimate the logical error-rate - # per round standard deviation. + # Note that the covariance matrix is used later to estimate the logical error + # probability per round standard deviation. (slope, offset), cov = curve_fit( lambda x, s, o: s * x + o, num_rounds, @@ -345,8 +345,8 @@ def compute_logical_error_per_round( ) # Following https://arxiv.org/pdf/2505.09684v1 (Methods - Extracting logical error - # per cycle, page 8) we estimate the variance on the logical error-rate per cycle - # (named Perrc below) using the formula + # per cycle, page 8) we estimate the variance on the logical error probability per + # round (named Perrc below) using the formula # sigma(Perrc) = (1 - Perrc) * sigma(slope) # The standard deviation on the linear fit parameters can be obtained through the # covariance matrix diagonal entries. @@ -358,7 +358,7 @@ def compute_logical_error_per_round( # Else estimated_spam_error = float((1 - np.exp(offset)) / 2) estimated_spam_error_stddev = (1 - 2 * estimated_spam_error) * offset_stddev / 2 - return LogicalErrorRatePerRoundResults( + return LogicalErrorProbabilityPerRoundResults( estimated_logical_error_per_round, estimated_logical_error_per_round_stddev, estimated_spam_error, @@ -377,18 +377,18 @@ def simulate_different_round_numbers_for_lep_per_round_estimation( heuristic_logical_error_lower_bound: float = 0.25, heuristic_logical_error_upper_bound: float = 0.45, ) -> tuple[npt.NDArray[np.int_], npt.NDArray[np.int_], npt.NDArray[np.int_]]: - """Compute QEC results to estimate the logical error-rate per round. + """Compute QEC results to estimate the logical error probability per round. - This function aims at encapsulating the practical knowledge about logical error-rate - per round computation to help any user computing the required logical error-rates - for useful number of rounds. + This function aims at encapsulating the practical knowledge about logical error + probability per round computation to help any user computing the required logical + error probabilities for useful number of rounds. It repeatedly calls ``simulator`` with a number of rounds growing according to ``next_round_number_func``, starting from ``initial_round_number``, - until the logical error-rate is above ``heuristic_logical_error_lower_bound``. If - the final step returned a logical error-rate above - ``heuristic_logical_error_upper_bound``, the algorithm then goes backward and - replaces that last value with the first one under that limit. + until the logical error probability is above + ``heuristic_logical_error_lower_bound``. If the final step returned a logical error + probability above ``heuristic_logical_error_upper_bound``, the algorithm then goes + backward and replaces that last value with the first one under that limit. Args: simulator (Callable[[int], tuple[int, int]]): @@ -405,15 +405,16 @@ def simulate_different_round_numbers_for_lep_per_round_estimation( maximum_round_number (int): if set, this function will stop once the next number of rounds (computed with ``next_round_number_func``) is above that threshold. If not set, only the other stopping criterions apply. - heuristic_logical_error_lower_bound (float): minimum target logical error-rate - for the final round. Might not be verified by the return of this function if - ``maximum_round_number`` is set and reached before that minimum threshold. - heuristic_logical_error_upper_bound (float): maximal target logical error-rate - for the final round. Should be set sufficiently below ``0.5`` such that the - uncertainties (mostly due to finite sampling) on the computed logical - error-rate (LER) are low enough to not introduce a plateau in the log-plot - of the fidelity log(F) = log(1 - 2*LER). Experimentally, ``0.45`` seems to - check that. + heuristic_logical_error_lower_bound (float): minimum target logical error + probability for the final round. Might not be verified by the return of this + function if ``maximum_round_number`` is set and reached before that minimum + threshold. + heuristic_logical_error_upper_bound (float): maximal target logical error + probability for the final round. Should be set sufficiently below ``0.5`` + such that the uncertainties (mostly due to finite sampling) on the computed + logical error probability (LEP) are low enough to not introduce a plateau in + the log-plot of the fidelity log(F) = log(1 - 2*LEP). Experimentally, + ``0.45`` seems to check that. Returns: tuple[npt.NDArray[np.int_], npt.NDArray[np.int_], npt.NDArray[np.int_]]: @@ -455,7 +456,7 @@ def perfect_simulator(num_rounds: int) -> tuple[int, int]: # Generate experiments until the number of repetitions is large enough (which is # heuristically determined as - # ``logical error-rate > heuristic_logical_error_lower_bound``). + # ``logical error probability > heuristic_logical_error_lower_bound``). while (nfails[-1] / nshots[-1]) < heuristic_logical_error_lower_bound: new_round_number = next_round_number_func(nrounds[-1]) if new_round_number > maximum_round_number: @@ -465,7 +466,7 @@ def perfect_simulator(num_rounds: int) -> tuple[int, int]: nfails.append(nfail) nshots.append(nshot) - # We do not want to include logical error-rates above + # We do not want to include logical error probabilities above # ``heuristic_logical_error_upper_bound``. # We go back using smaller steps until we find a last point that is over # ``heuristic_logical_error_lower_bound`` but under diff --git a/deltakit-explorer/tests/analysis/test_analysis.py b/deltakit-explorer/tests/analysis/test_analysis.py index 68e6499f..ac968966 100644 --- a/deltakit-explorer/tests/analysis/test_analysis.py +++ b/deltakit-explorer/tests/analysis/test_analysis.py @@ -92,7 +92,7 @@ def test_raises_when_no_fails(self): def test_raises_when_too_many_fails(self): shots = 100_000 - message = "^Got estimations of logical error-rates above 0.5.*" + message = "^Got estimations of logical error probability above 0.5.*" with pytest.raises(RuntimeError, match=message): compute_logical_error_per_round( [shots // 2 + 1] * 3, [shots] * 3, [2, 4, 6] @@ -100,14 +100,18 @@ def test_raises_when_too_many_fails(self): def test_warn_when_max_lep_is_too_small(self): shots = 100_000 - message = r"^The maximum estimated logical error-rate \([^\)]+\) is below 0.2.*" + message = ( + "^The maximum estimated logical error probability " + r"\([^\)]+\) is below 0.2.*" + ) with pytest.warns(UserWarning, match=message): compute_logical_error_per_round([2460, 4343, 6151], [shots] * 3, [2, 4, 6]) def test_warn_when_linear_fit_is_bad(self): f_0 = 1 - 0.01 rounds = np.arange(2, 61, 5) - # Non-constant logical error-rate per round that should trigger the R2 check. + # Non-constant logical error probability per round that should trigger the R2 + # check. leppr = np.array( [ 0.00485509, diff --git a/docs/examples/notebooks/google_qmem_experiment/decode.ipynb b/docs/examples/notebooks/google_qmem_experiment/decode.ipynb index 4ee4728b..373e7fa7 100644 --- a/docs/examples/notebooks/google_qmem_experiment/decode.ipynb +++ b/docs/examples/notebooks/google_qmem_experiment/decode.ipynb @@ -352,7 +352,7 @@ "source": [ "### Plot decoding against rounds\n", "\n", - "Here, we plot the logical error probabilities for each experiment. From these, we can derive the logical error rate, a measure of the logical performance per round. " + "Here, we plot the logical error probabilities for each experiment. From these, we can derive the logical error probability per round, a measure of the logical performance per round." ] }, { @@ -434,7 +434,7 @@ ")\n", "plt.plot(rounds_fit, probability_fit,\n", " color=\"#00968f\", lw=2,\n", - " label=f\"logical error rate ε={eps:6f}\")\n", + " label=f\"logical error probability per round ε={eps:6f}\")\n", "plt.errorbar(all_rounds, probs, y_error_bars,\n", " marker=\".\", mec=\"#00968f\", mfc=\"#00968f\", color=\"#00968f\",\n", " capsize=6, capthick=1, elinewidth=3, ecolor=\"#00968f\",\n", @@ -539,7 +539,7 @@ "# plot the noise model decoding results\n", "plt.plot(rounds_fit, probability_fit,\n", " color=\"#00968f\", lw=2,\n", - " label=f\"logical error rate ε={eps:6f}\")\n", + " label=f\"logical error probability per round ε={eps:6f}\")\n", "plt.errorbar(all_rounds, probs, y_error_bars,\n", " marker=\".\", mec=\"#00968f\", mfc=\"#00968f\", color=\"#00968f\",\n", " capsize=4, capthick=1, elinewidth=2, ecolor=\"#00968f\",\n", @@ -563,7 +563,7 @@ "\n", "plt.plot(exp_rounds_fit, exp_probability_fit,\n", " color=\"#ff7500\", lw=2,\n", - " label=f\"logical error rate ε={exp_eps:6f}\")\n", + " label=f\"logical error probability per round ε={exp_eps:6f}\")\n", "plt.errorbar(all_rounds, exp_probs, exp_y_error_bars,\n", " marker=\".\", mec=\"#ff7500\", mfc=\"#ff7500\", color=\"#ff7500\",\n", " capsize=4, capthick=1, elinewidth=2, ecolor=\"#ff7500\",\n", diff --git a/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb b/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb index 97ac22ce..a1dcf623 100644 --- a/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb +++ b/docs/examples/notebooks/qmem/rotated_planar_quantum_memory_plots.ipynb @@ -86,14 +86,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "54fd7753-2d23-447f-8127-92cd373a822e", "metadata": {}, "outputs": [], "source": [ "# This value determines the maximum number of samples to take from the circuit.\n", "# A higher number of shots will provide a more accurate value of the\n", - "# logical error rate.\n", + "# logical error probability.\n", "max_shots = 1e7\n", "\n", "# File name to save output data.\n", @@ -229,7 +229,7 @@ "This function uses the `rotated_planar_xmem_noiseless` function to create the relevant code object and noiseless circuit, \n", "and then compiles the noiseless circuit into the correct gate set with the input noise model. \n", "\n", - "This is the circuit we will be sampling from to get a logical error rate." + "This is the circuit we will be sampling from to get a logical error probability." ] }, { @@ -296,12 +296,12 @@ "id": "1501fcb6", "metadata": {}, "source": [ - "We will be using the MWPM decoder implemented in the `pymatching` python package. We have defined a function to construct the decoder and wrap it into a decoder manager. Given an input circuit, the decoder manager has methods to run many samples of errors through the decoder and give an approximate value of the logical error rate." + "We will be using the MWPM decoder implemented in the `pymatching` python package. We have defined a function to construct the decoder and wrap it into a decoder manager. Given an input circuit, the decoder manager has methods to run many samples of errors through the decoder and give an approximate value of the logical error probability." ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "3a76bf53-9d95-4ef6-9382-7e2119cbceae", "metadata": {}, "outputs": [], @@ -309,7 +309,7 @@ "def experiment_decoder_manager(\n", " x_distance: int, z_distance: int, num_rounds: int, physical_error_rate: float\n", ") -> StimDecoderManager:\n", - " \"\"\"This function will combine all previous functions to return a single value: the logical error rate.\n", + " \"\"\"This function will combine all previous functions to return a single value: the logical error probability.\n", "\n", " Parameters\n", " ----------\n", @@ -325,7 +325,7 @@ " Returns\n", " -------\n", " float\n", - " The logical error rate for this code instance.\n", + " The logical error probability for this code instance.\n", "\n", " \"\"\"\n", "\n", @@ -372,7 +372,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "09fa4ea6", "metadata": {}, "outputs": [ @@ -392,8 +392,8 @@ "pt1pct1_shots, pt1pct1_fails = experiment_decoder_manager(\n", " x_distance=3, z_distance=3, num_rounds=3, physical_error_rate=0.001\n", ").run_batch_shots(1e5)\n", - "print(f\"Logical error rate with physical error 1%: {pct1_fails/pct1_shots}\")\n", - "print(f\"Logical error rate with physical error .1%: {pt1pct1_fails/pt1pct1_shots}\")" + "print(f\"Logical error probability with physical error 1%: {pct1_fails/pct1_shots}\")\n", + "print(f\"Logical error probability with physical error .1%: {pt1pct1_fails/pt1pct1_shots}\")" ] }, { @@ -497,9 +497,9 @@ "source": [ "Now that the experiment has completed, we can plot the results. \n", "\n", - "Importantly, we have calculated the total logical error rate given a $d$-distance code and $d$ rounds of quantum error correction. \n", - "The work from Paler plots a _per round_ logical error rate, and so we divide our outcomes by the number of QEC rounds to achieve\n", - "a per round logical error rate. More information can be found in the documentation. " + "Importantly, we have calculated the total logical error probability given a $d$-distance code and $d$ rounds of quantum error correction.\n", + "The work from Paler plots a _per round_ logical error probability, and so we divide our outcomes by the number of QEC rounds to achieve\n", + "a per round logical error probability. More information can be found in the documentation." ] }, { @@ -516,13 +516,13 @@ "\n", "\n", "def plot_per_round_logical_error(distance: int) -> None:\n", - " \"\"\"Plot the logical error rate of a single distance value,\n", + " \"\"\"Plot the logical error probability per round of a single distance value,\n", " with data taken from the results CSV.\n", "\n", " Parameters\n", " ----------\n", " distance : int\n", - " Which distance value to plot the logical error rate of.\n", + " Which distance value to plot the logical error probability per round of.\n", " \"\"\"\n", " subframe = data[(data[\"Num Rounds\"] == distance)].sort_values(by=[\"Physical Error\"])\n", "\n", @@ -568,7 +568,7 @@ "plt.yticks([10 ** (-i / 3) for i in range(25)], fontsize=20)\n", "\n", "plt.xlabel(\"Physical error probability, $p$\", fontsize=20)\n", - "plt.ylabel(\"Logical error rate (per round)\", fontsize=20)\n", + "plt.ylabel(\"Logical error probability per round\", fontsize=20)\n", "plt.grid()\n", "plt.legend()" ] diff --git a/docs/examples/notebooks/simulation/ac_decoding.ipynb b/docs/examples/notebooks/simulation/ac_decoding.ipynb index 7a91c022..ac6890ac 100644 --- a/docs/examples/notebooks/simulation/ac_decoding.ipynb +++ b/docs/examples/notebooks/simulation/ac_decoding.ipynb @@ -322,7 +322,7 @@ "Here we demonstrate decoding of IBM's 72 qubit bivariate bicycle code with AC, and, for comparison, with BP-OSD.\n", "These BP parameters for the two algorithms are empirically found to work well. Since BP-OSD has a much heavier post-processing step than AC, it makes sense that the algorithm benefits from more rounds of BP.\n", "\n", - "In our example, we demonstrate AC's superior decoding time compared to BP_OSD, whilst maintaining a comparable logical error rate." + "In our example, we demonstrate AC's superior decoding time compared to BP_OSD, whilst maintaining a comparable logical error probability." ] }, { diff --git a/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb b/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb index 93b09266..595190ee 100644 --- a/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb +++ b/docs/examples/notebooks/simulation/leakage-aware_decoding.ipynb @@ -24,7 +24,7 @@ "* read-in circuits of surface codes and consider two leakage noise levels: low leakage (LL) and high leakage (HL);\n", "* generate measurement, detector, and observable data from the circuits;\n", "* decode the detector data thrice with LCD: once leakage-unaware (LU), once leakage-aware (LA), and once weighted leakage-aware (WLA);\n", - "* produce a plot of the logical error rate per round against code distance and calculate $\\Lambda$, the error suppression factor for the 6 decoding examples $\\{LL,\\;HL\\} \\times \\{LU,\\;LA,\\;WLA\\}$, demonstrating the improvement to logical error suppression through decoding with leakage awareness.\n", + "* produce a plot of the logical error probability per round against code distance and calculate $\\Lambda$, the error suppression factor for the 6 decoding examples $\\{LL,\\;HL\\} \\times \\{LU,\\;LA,\\;WLA\\}$, demonstrating the improvement to logical error suppression through decoding with leakage awareness.\n", "\n", "**Note:** The compilation of the adaptivity map, which happens ahead of runtime and sampling, is computationally intense, but sampling and decoding are fast. For higher code distances compilation times can be long. If you wish to run this notebook yourself, we strongly recommend selecting a subset of the distances and a lower number of shots (d = [5, 7, 9], shots ≥ 250,000). This is already included by default with the full experiment commented-out at the top of the ``Circuit Simulation and Decoding`` section code block." ] diff --git a/docs/guide/analysis.ipynb b/docs/guide/analysis.ipynb index 1e831b5d..ffa51689 100644 --- a/docs/guide/analysis.ipynb +++ b/docs/guide/analysis.ipynb @@ -473,7 +473,7 @@ "At a very high level, we appreciate comparing things on a single scale. In decoder analysis,\n", "there is a set of numbers that describe decoder quality and allow them to be compared.\n", "\n", - "### 1.1. Logical Error Rate\n", + "### 1.1. Logical error probability\n", "\n", "An obvious starting point is to measure how accurate a decoder is under fixed conditions.\n", "In QEC, this metric is called **logical error probability** (LEP) and is computed as the proportion\n", @@ -530,7 +530,7 @@ "source": [ "In Quantum Memory experiments, you try to preserve a qubit in the state it was initialised.\n", "The longer you do this, the more physical errors accumulate, thus the greater the chance for the decoder to make a mistake.\n", - "The theoretical model states that every error correction round increases the probability of a logical error by a factor known as the **logical error rate** $\\epsilon$.\n", + "The theoretical model states that every error correction round increases the probability of a logical error by a factor known as the **logical error probability per round** $\\epsilon$.\n", "You may also find it under the name \"logical error probability per round\".\n", "Note: doing no rounds means your LEP is equal to a logical **state preparation and measurement** (SPAM) error. Thus:\n", "\n", @@ -612,7 +612,7 @@ "A decoder may perform very well on small **code sizes**, or in particular **noise regimes**.\n", "It is useful to see how performance changes with changing conditions.\n", "\n", - "The [Threshold theorem](https://en.wikipedia.org/wiki/Threshold_theorem) suggests that logical error rate should exponentially decrease with the size of the code,\n", + "The [Threshold theorem](https://en.wikipedia.org/wiki/Threshold_theorem) suggests that logical error probability per round should exponentially decrease with the size of the code,\n", "but beyond *some* physical noise level this law flips, and adding more qubits leads to even worse logical errors.\n", "The point (amount of physical noise) where this happens is called a **threshold**. This number characterises\n", "how ready particular codes and decoders are for the noise level of real hardware. The bigger the threshold, the better.\n", @@ -709,7 +709,7 @@ "plt.axvline(x=threshold, color=\"gray\", linestyle=\"--\", label=f\"threshold={threshold:.3f}\")\n", "\n", "plt.xlabel(\"$P_{physical}$\")\n", - "plt.ylabel(\"Logical error rate\")\n", + "plt.ylabel(\"Logical error probability per round\")\n", "plt.yscale(\"log\")\n", "plt.legend()\n", "plt.show()" @@ -723,7 +723,7 @@ "### 1.3. Error scaling parameter $\\Lambda$\n", "\n", "Another way to see how the decoder responds to changes in code size is to take a vertical slice of the threshold plot.\n", - "If you set the noise level as a constant $p_0$, you may see how well the decoder's logical error rate improves\n", + "If you set the noise level as a constant $p_0$, you may see how well the decoder's logical error probability per round improves\n", "with code size change. The threshold theorem assumes this satisfies the exponential law:\n", "\n", "$$\n", @@ -762,11 +762,11 @@ "\n", "fitted_curve = get_lambda_fit(distances, probs, errors)\n", "plt.plot(distances, fitted_curve, color=colors[0], label=f\"Λ = {Lambda:.3f} ± {Lambda_error:.3f}\")\n", - "plt.errorbar(distances, probs, yerr=errors, color=colors[1], label=\"Logical Error Rate\", ls=\"\", marker=\"*\")\n", + "plt.errorbar(distances, probs, yerr=errors, color=colors[1], label=\"Logical error probability per round\", ls=\"\", marker=\"*\")\n", "plt.xticks(distances)\n", "plt.title(\"Λ fit\")\n", "plt.xlabel(\"Code distance\")\n", - "plt.ylabel(\"Logical Error Rate\")\n", + "plt.ylabel(\"Logical error probability per round\")\n", "plt.legend()\n", "plt.show()" ] @@ -1078,7 +1078,7 @@ "metadata": {}, "source": [ "In this notebook, you've explored a range of quantum error correction analysis tools available in Deltakit.\n", - "Starting from high-level experiment results, you've computed logical error rate, threshold, and error scaling parameter.\n", + "Starting from high-level experiment results, you've computed logical error probability per round, threshold, and error scaling parameter.\n", "You then drilled down to tools for detailed error distribution analysis and experiment automation tools.\n", "At the low level, you've analysed defect rates and detector correlations to gain deeper insight into physical error mechanisms.\n", "\n", diff --git a/docs/guide/decoding.md b/docs/guide/decoding.md index dd429c43..9ea22d13 100644 --- a/docs/guide/decoding.md +++ b/docs/guide/decoding.md @@ -284,7 +284,7 @@ Such circuits produce additional information about the leakage state of qubits t To generate such circuits, please refer to the chapter [Adding Noise](./adding_noise.md). -The `LCDecoder` supports leakage as an additional input, and uses it for logical error rate reduction. +The `LCDecoder` supports leakage as an additional input, and uses it for logical error probability reduction. ```{code-cell} ipython3 from deltakit.explorer.types import SI1000NoiseModel diff --git a/docs/guide/getting_started.md b/docs/guide/getting_started.md index b76024a5..ea4a47e1 100644 --- a/docs/guide/getting_started.md +++ b/docs/guide/getting_started.md @@ -40,7 +40,7 @@ To generate the underlying circuit for quantum memory experiments, there are sev - Bivariate bicycle codes ({class}`BivariateBicycleCode `), and - (More generally) any CSS code ({class}`CSSCode `). -To get the logical error rate with a quantum memory experiment, for example, you can use the rotated planar code to encode a logical qubit: +To get the logical error probability with a quantum memory experiment, for example, you can use the rotated planar code to encode a logical qubit: ```{code-cell} ipython3 from deltakit.explorer import codes diff --git a/tests/qmem/test_qmem.py b/tests/qmem/test_qmem.py index ba1e0984..c94c7f2d 100644 --- a/tests/qmem/test_qmem.py +++ b/tests/qmem/test_qmem.py @@ -23,7 +23,7 @@ # This value determines the maximum number of samples to take from the circuit. # A higher number of shots will provide a more accurate value of the -# logical error rate. +# logical error probability. max_shots = 1e7 # File name to save output data. @@ -175,7 +175,7 @@ def experiment_decoder_manager( x_distance: int, z_distance: int, num_rounds: int, physical_error_rate: float ) -> StimDecoderManager: """This function will combine all previous functions to return a single value: the - logical error rate. + logical error probability. Parameters ---------- @@ -191,7 +191,7 @@ def experiment_decoder_manager( Returns ------- float - The logical error rate for this code instance. + The logical error probability for this code instance. """ @@ -228,13 +228,13 @@ def experiment_decoder_manager( def plot_per_round_logical_error(data, colors, distance: int) -> None: - """Plot the logical error rate of a single distance value, + """Plot the logical error probability per round of a single distance value, with data taken from the results CSV. Parameters ---------- distance : int - Which distance value to plot the logical error rate of. + Which distance value to plot the logical error probability per round of. """ subframe = data[(data["Num Rounds"] == distance)].sort_values(by=["Physical Error"]) @@ -259,9 +259,12 @@ def test_qmem(): pt1pct1_shots, pt1pct1_fails = experiment_decoder_manager( x_distance=3, z_distance=3, num_rounds=3, physical_error_rate=0.001 ).run_batch_shots(1e5) - print(f"Logical error rate with physical error 1%: {pct1_fails / pct1_shots}") print( - f"Logical error rate with physical error .1%: {pt1pct1_fails / pt1pct1_shots}" + f"Logical error probability with physical error 1%: {pct1_fails / pct1_shots}" + ) + print( + "Logical error probability with physical error .1%:" + f"{pt1pct1_fails / pt1pct1_shots}" ) distances = [3, 5, 7, 9] @@ -309,7 +312,7 @@ def test_qmem(): plt.yticks([10 ** (-i / 3) for i in range(25)], fontsize=20) plt.xlabel("Physical error probability, $p$", fontsize=20) - plt.ylabel("Logical error rate (per round)", fontsize=20) + plt.ylabel("Logical error probability per round", fontsize=20) plt.grid() plt.legend() From b3d703c0cd02ecbbc51c442bedea816b881b42fd Mon Sep 17 00:00:00 2001 From: Adrien Suau <12374487+nelimee@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:35:57 +0100 Subject: [PATCH 25/26] fix:unignore deprecation warnings in tests (#114) * Un-ignore deprecation warnings in tests * Ignore specific warning --------- Co-authored-by: Marco Ghibaudi <62563515+mghibaudi@users.noreply.github.com> --- .../tests/test_qubit_identifiers.py | 26 +++++++++++++------ .../utils/_derivation_tools.py | 2 +- pyproject.toml | 5 +++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/deltakit-circuit/tests/test_qubit_identifiers.py b/deltakit-circuit/tests/test_qubit_identifiers.py index 1e077e55..960881bc 100644 --- a/deltakit-circuit/tests/test_qubit_identifiers.py +++ b/deltakit-circuit/tests/test_qubit_identifiers.py @@ -84,17 +84,27 @@ def test_warning_is_raised_if_calling_pairs_from_consecutive_method(): def test_error_is_raised_if_calling_pairs_from_consecutive_with_odd_sequence(): - with pytest.raises( - ValueError, match="Pairs cannot be constructed from an odd number of IDs." - ): - list(Qubit.pairs_from_consecutive([0, 1, 2])) + msg = ( + "This method should not be used. Instead please use the `from_consecutive` " + "method on the two qubit gate class" + ) + with pytest.warns(DeprecationWarning, match=msg): + with pytest.raises( + ValueError, match="Pairs cannot be constructed from an odd number of IDs." + ): + list(Qubit.pairs_from_consecutive([0, 1, 2])) def test_pairs_from_consecutive_returns_correct_qubit_pairs(): - assert list(Qubit.pairs_from_consecutive([0, 1, 2, 3])) == [ - (Qubit(0), Qubit(1)), - (Qubit(2), Qubit(3)), - ] + msg = ( + "This method should not be used. Instead please use the `from_consecutive` " + "method on the two qubit gate class" + ) + with pytest.warns(DeprecationWarning, match=msg): + assert list(Qubit.pairs_from_consecutive([0, 1, 2, 3])) == [ + (Qubit(0), Qubit(1)), + (Qubit(2), Qubit(3)), + ] @pytest.mark.parametrize( diff --git a/deltakit-decode/src/deltakit_decode/utils/_derivation_tools.py b/deltakit-decode/src/deltakit_decode/utils/_derivation_tools.py index 052855f6..ea5d0ab6 100644 --- a/deltakit-decode/src/deltakit_decode/utils/_derivation_tools.py +++ b/deltakit-decode/src/deltakit_decode/utils/_derivation_tools.py @@ -67,7 +67,7 @@ def _generate_expectation_data_multiprocess( # if num_processes > samples, floor to 1 step = max(len(samples)//num_processes, 1)+1 - split_samples = [islice(samples, n, n+step) + split_samples = [list(islice(samples, n, n+step)) for n in range(0, len(samples), step)] with pathos.helpers.mp.Pool(num_processes) as p: results = p.starmap(_compute_combinations_of_detectors, [ diff --git a/pyproject.toml b/pyproject.toml index 00a728c6..ce741b24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,7 +138,10 @@ addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] xfail_strict = true filterwarnings = [ "error", - "ignore::DeprecationWarning", # TODO: deal with these + # Note: the below line ignore a specific warning about changes in multiprocessing. + # This warning is only raised by a few tests in deltakit-decode. This is a temporary + # fix. + "ignore:This process .* is multi-threaded, use of fork\\(\\) may lead to deadlocks in the child.:DeprecationWarning" ] [tool.isort] From e0223b5d5d28fd9d2b6a7f7596d343d16b76df77 Mon Sep 17 00:00:00 2001 From: Adrien Suau <12374487+nelimee@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:51:19 +0100 Subject: [PATCH 26/26] feat: add deprecation policy (#120) * Un-ignore deprecation warnings in tests * Ignore specific warning * Add deprecation function * Adding pragma: no cover for overloads * Fix deprecated error reporting * Allow deprecated to be called with all-None values * Adding tests to deprecated --- .pre-commit-config.yaml | 1 + deltakit-core/pyproject.toml | 1 + .../src/deltakit_core/deprecation.py | 86 +++++++++++++++++++ deltakit-core/tests/unit/test_deprecation.py | 51 +++++++++++ 4 files changed, 139 insertions(+) create mode 100644 deltakit-core/src/deltakit_core/deprecation.py create mode 100644 deltakit-core/tests/unit/test_deprecation.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 83ab0351..fa51a425 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -52,6 +52,7 @@ repos: - types-requests - plotly-stubs - scipy-stubs + - semver - repo: https://github.com/crate-ci/typos rev: v1.37.1 diff --git a/deltakit-core/pyproject.toml b/deltakit-core/pyproject.toml index 23726aac..748bd954 100644 --- a/deltakit-core/pyproject.toml +++ b/deltakit-core/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "networkx>=3.1, <4.0", "stim>=1.12.1, <2.0", "typing_extensions>=4.0, <5.0", + "semver" ] [project.urls] diff --git a/deltakit-core/src/deltakit_core/deprecation.py b/deltakit-core/src/deltakit_core/deprecation.py new file mode 100644 index 00000000..fc5bbe09 --- /dev/null +++ b/deltakit-core/src/deltakit_core/deprecation.py @@ -0,0 +1,86 @@ +import functools +import warnings +from typing import Callable, ParamSpec, TypeVar, overload + +import semver + +P = ParamSpec("P") +RetType = TypeVar("RetType") + + +@overload +def deprecated(f: Callable[P, RetType] | None, /) -> Callable[P, RetType]: + pass # pragma: no cover + + +@overload +def deprecated( + *, + reason: str | None = None, + replaced_by: str | None = None, + removed_in_version: semver.Version | None = None, +) -> Callable[[Callable[P, RetType]], Callable[P, RetType]]: + pass # pragma: no cover + + +def deprecated( + f: Callable[P, RetType] | None = None, + /, + *, + reason: str | None = None, + replaced_by: str | None = None, + removed_in_version: semver.Version | None = None, +) -> Callable[[Callable[P, RetType]], Callable[P, RetType]] | Callable[P, RetType]: + """Deprecation decorator. + + Args: + f: function to deprecate, allow this function to be used as a decorator. + reason: rationale behind the deprecation. + replaced_by: a description of the new method that should be used instead of the + deprecated function. + removed_in_version: minimum version in which the deprecated function might be + removed. The deprecated function should not be removed earlier than this + version. + + Returns: + Depending on the provided arguments, either a wrapper around the provided ``f`` + that raises a deprecation warning or a decorator that can deprecate a function. + """ + if all(p is None for p in (f, reason, replaced_by, removed_in_version)): + raise ValueError( + "Expected at least one of f, reason, replaced_by or removed_in_version to " + "be provided. All of them are None." + ) + if f is not None: + msg = f"{f.__name__} is deprecated and will eventually be removed." + + @functools.wraps(f) + def deprecated_func(*args: P.args, **kwargs: P.kwargs) -> RetType: + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return f(*args, **kwargs) + + return deprecated_func + + # else + def deprecation_decorator( + func_to_deprecate: Callable[P, RetType], + ) -> Callable[P, RetType]: + msg = f"{func_to_deprecate.__name__} is deprecated and will " + msg += ( + f"be removed in version {removed_in_version}." + if removed_in_version is not None + else "eventually be removed." + ) + if reason is not None: + msg += f" Reason for deprecation: '{reason}'." + if replaced_by is not None: + msg += f" Consider using '{replaced_by}' instead." + + @functools.wraps(func_to_deprecate) + def deprecated_func(*args: P.args, **kwargs: P.kwargs) -> RetType: + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return func_to_deprecate(*args, **kwargs) + + return deprecated_func + + return deprecation_decorator diff --git a/deltakit-core/tests/unit/test_deprecation.py b/deltakit-core/tests/unit/test_deprecation.py new file mode 100644 index 00000000..b2a9b9d2 --- /dev/null +++ b/deltakit-core/tests/unit/test_deprecation.py @@ -0,0 +1,51 @@ +from deltakit_core.deprecation import deprecated +import pytest +import semver + + +def non_deprecated_add(a: float, b: float, ndigits: int | None = None) -> float: + return round(a + b, ndigits) + + +@deprecated +def default_deprecated_add(a: float, b: float, ndigits: int | None = None) -> float: + return round(a + b, ndigits) + + +@deprecated( + reason="Testing purposes", + replaced_by="nothing", + removed_in_version=semver.Version(0, 2, 0), +) +def custom_deprecated_add(a: float, b: float, ndigits: int | None = None) -> float: + return round(a + b, ndigits) + + +def test_default_deprecated() -> None: + msg = "^default_deprecated_add is deprecated and will eventually be removed.$" + with pytest.warns(DeprecationWarning, match=msg): + assert default_deprecated_add(2.00000001, 3.0000000003, ndigits=3) == 5 + + +def test_custom_deprecated() -> None: + msg = ( + "^custom_deprecated_add is deprecated and will be removed in version 0.2.0. " + "Reason for deprecation: 'Testing purposes'. Consider using 'nothing' instead." + ) + with pytest.warns(DeprecationWarning, match=msg): + assert custom_deprecated_add(2.00000001, 3.0000000003, ndigits=3) == 5 + + +def test_direct_calling_no_argument() -> None: + with pytest.raises(ValueError): + deprecated() + + +def test_direct_calling_with_none_callable() -> None: + with pytest.raises(ValueError): + deprecated(None) + + +def test_direct_calling_all_kwargs_none() -> None: + with pytest.raises(ValueError): + deprecated(reason=None, removed_in_version=None)