diff --git a/checks/all_.py b/checks/all_.py index 8f44f99f0..c8a13661e 100755 --- a/checks/all_.py +++ b/checks/all_.py @@ -5,4 +5,4 @@ if __name__ == "__main__": skip_files = ["--sysmon", "--skip", "configs", "requirements", "build_docs"] - exit(checks_superstaq.all_.run(*skip_files, *sys.argv[1:])) + sys.exit(checks_superstaq.all_.run(*skip_files, *sys.argv[1:])) diff --git a/checks/build_docs.py b/checks/build_docs.py index c3feaa2a3..92ef6801d 100755 --- a/checks/build_docs.py +++ b/checks/build_docs.py @@ -4,4 +4,4 @@ import checks_superstaq as checks if __name__ == "__main__": - exit(checks.build_docs.run(*sys.argv[1:])) + sys.exit(checks.build_docs.run(*sys.argv[1:])) diff --git a/checks/coverage_.py b/checks/coverage_.py index a3ad2f7e9..22af19bc5 100755 --- a/checks/coverage_.py +++ b/checks/coverage_.py @@ -5,7 +5,7 @@ import pytest_ if __name__ == "__main__": - exit( + sys.exit( checks_superstaq.coverage_.run( *sys.argv[1:], "--modular", "--sysmon", exclude=pytest_.EXCLUDE ) diff --git a/checks/format_.py b/checks/format_.py index 9360f84bc..9f7e1ab06 100755 --- a/checks/format_.py +++ b/checks/format_.py @@ -4,4 +4,4 @@ import checks_superstaq if __name__ == "__main__": - exit(checks_superstaq.format_.run(*sys.argv[1:])) + sys.exit(checks_superstaq.format_.run(*sys.argv[1:])) diff --git a/checks/lint_.py b/checks/lint_.py index ccadf11e7..933518ebe 100755 --- a/checks/lint_.py +++ b/checks/lint_.py @@ -4,4 +4,4 @@ import checks_superstaq if __name__ == "__main__": - exit(checks_superstaq.lint_.run(*sys.argv[1:])) + sys.exit(checks_superstaq.lint_.run(*sys.argv[1:])) diff --git a/checks/mypy_.py b/checks/mypy_.py index 877c54869..f1e80839a 100755 --- a/checks/mypy_.py +++ b/checks/mypy_.py @@ -9,4 +9,4 @@ ) if __name__ == "__main__": - exit(checks_superstaq.mypy_.run(*sys.argv[1:], exclude=EXCLUDE)) + sys.exit(checks_superstaq.mypy_.run(*sys.argv[1:], exclude=EXCLUDE)) diff --git a/checks/pytest_.py b/checks/pytest_.py index 704bf051f..70e3004c4 100755 --- a/checks/pytest_.py +++ b/checks/pytest_.py @@ -12,4 +12,4 @@ ) if __name__ == "__main__": - exit(checks_superstaq.pytest_.run(*sys.argv[1:], exclude=EXCLUDE)) + sys.exit(checks_superstaq.pytest_.run(*sys.argv[1:], exclude=EXCLUDE)) diff --git a/examples/scripts/find_bbcode_layouts.py b/examples/scripts/find_bbcode_layouts.py old mode 100644 new mode 100755 index 8a77fe0c7..7ada66325 --- a/examples/scripts/find_bbcode_layouts.py +++ b/examples/scripts/find_bbcode_layouts.py @@ -421,8 +421,7 @@ def get_max_comm_distance( for neighbor in code.graph.successors(node): loc_1 = get_qubit_pos(neighbor) squared_distance = (loc_0[0] - loc_1[0]) ** 2 + (loc_0[1] - loc_1[1]) ** 2 - if squared_distance > max_squared_distance: - max_squared_distance = squared_distance + max_squared_distance = max(max_squared_distance, squared_distance) return math.sqrt(max_squared_distance) @@ -486,7 +485,7 @@ def get_completed_qubit_pos_func( # assign check qubits to candidate locations biadjacency_matrix = placement_matrix <= (max_comm_distance + 1e-15) ** 2 - check_loc_indices, check_qubit_indices = scipy.optimize.linear_sum_assignment( + _check_loc_indices, check_qubit_indices = scipy.optimize.linear_sum_assignment( biadjacency_matrix, maximize=True ) diff --git a/experiments/bivariate_bicycle/run_search.py b/experiments/bivariate_bicycle/run_search.py index b9e9c47fd..8ab74242b 100755 --- a/experiments/bivariate_bicycle/run_search.py +++ b/experiments/bivariate_bicycle/run_search.py @@ -87,7 +87,7 @@ def run_and_save( nn, kk, dd = cache[key] # if we know the distance or the encoding rate is too low, there's nothing more to do if dd is not None or kk < nn * min_rate: - return None + return # compute and save code parameters params = get_quasi_cyclic_code_params(dims, exponents, min_rate, num_trials, silent=silent) diff --git a/experiments/quantum_tanner/collect_results.py b/experiments/quantum_tanner/collect_results.py index 0f9782bc6..d05b29de2 100755 --- a/experiments/quantum_tanner/collect_results.py +++ b/experiments/quantum_tanner/collect_results.py @@ -42,7 +42,7 @@ # collect all data for into a list data = [] for path in glob.glob(paths): - seed = path.split("_")[-1].strip(".txt")[1:] + seed = path.split("_")[-1].removesuffix(".txt")[1:] with open(path, "r") as file: num_trials = ast.literal_eval(file.readline().split(":")[-1]) param_text = file.readline().split(":")[-1] diff --git a/experiments/quantum_tanner/run_randomized_search.py b/experiments/quantum_tanner/run_randomized_search.py index 7660849d6..c12ebcdff 100755 --- a/experiments/quantum_tanner/run_randomized_search.py +++ b/experiments/quantum_tanner/run_randomized_search.py @@ -62,7 +62,7 @@ def get_mittal_code(length: int) -> codes.ClassicalCode: code = full_code.shortened([3]) else: raise ValueError(f"Unrecognized length for {name}: {length}") - setattr(code, "_name", name) + code._name = name return code @@ -99,7 +99,7 @@ def run_and_save( if group_order < base_code.num_bits: # No subset of this group can be large enough to have as many elements as there are bits in # the base code, so random quantum Tanner codes with the given input data do not exist. - return None + return group_id = f"SmallGroup-{group_order}-{group_index}" seed = get_deterministic_hash(group_order, group_index, base_code.matrix.tobytes(), sample) @@ -108,7 +108,7 @@ def run_and_save( if os.path.isfile(path) and not override_existing_data: # we already have the data for this code, so there is nothing to do - return None + return if not silent: job_id = f"{group_id} {base_code_id} {sample}/{num_samples}" diff --git a/experiments/quantum_tanner/verify_codes.py b/experiments/quantum_tanner/verify_codes.py index 6a64690e2..64c74f36b 100755 --- a/experiments/quantum_tanner/verify_codes.py +++ b/experiments/quantum_tanner/verify_codes.py @@ -49,7 +49,7 @@ def get_code(code_name: str, code_param: str) -> codes.ClassicalCode: parts = path.split("_") group_order, group_index = map(int, parts[-3].split("-")[-2:]) code_name, code_param = parts[-2].split("-") - seed = int(parts[-1].strip(".txt")[1:]) + seed = int(parts[-1].removesuffix(".txt")[1:]) group_id = f"SmallGroup-{group_order}-{group_index}" base_code_id = parts[-2] diff --git a/pyproject.toml b/pyproject.toml index e9e72ebda..6abe63673 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ build-backend = "uv_build" [tool.ruff] line-length = 100 -lint.extend-select = ["I", "W505"] +lint.extend-select = ["I", "W505", "ANN204"] lint.pycodestyle.max-doc-length = 100 [tool.mypy] diff --git a/src/qldpc/_util.py b/src/qldpc/_util.py new file mode 100644 index 000000000..0397f49f6 --- /dev/null +++ b/src/qldpc/_util.py @@ -0,0 +1,40 @@ +"""Miscellaneous internal utilities + +Copyright 2023 The qLDPC Authors and Infleqtion Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar + +CallableType = TypeVar("CallableType", bound=Callable[..., object]) + + +def format_docstring(**substitutions: object) -> Callable[[CallableType], CallableType]: + """Substitute named values into a function's docstring via str.format. + + This lets a docstring reference values (such as module-level constants) by name, for example + "Default: {error_rate}.", without making the docstring an f-string. An f-string cannot be used + as a docstring: Python evaluates it as an ordinary expression and leaves __doc__ set to None, + silently discarding the documentation. + """ + + def decorator(func: CallableType) -> CallableType: + if func.__doc__ is not None: + func.__doc__ = func.__doc__.format(**substitutions) + return func + + return decorator diff --git a/src/qldpc/_util_test.py b/src/qldpc/_util_test.py new file mode 100644 index 000000000..8d084d230 --- /dev/null +++ b/src/qldpc/_util_test.py @@ -0,0 +1,39 @@ +"""Unit tests for _util.py + +Copyright 2023 The qLDPC Authors and Infleqtion Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from qldpc._util import format_docstring + + +def test_format_docstring() -> None: + """Named values are substituted into a docstring.""" + + @format_docstring(value=1e-3, name="tag") + def func() -> None: + """A docstring with a {value} and a {name}.""" + + assert func.__doc__ == "A docstring with a 0.001 and a tag." + + +def test_format_docstring_without_docstring() -> None: + """A function without a docstring is left untouched.""" + + @format_docstring(value=1) + def func() -> None: + return None + + assert func.__doc__ is None + assert func() is None diff --git a/src/qldpc/abstract/__init__.py b/src/qldpc/abstract/__init__.py index d2d30522f..bf09742d7 100644 --- a/src/qldpc/abstract/__init__.py +++ b/src/qldpc/abstract/__init__.py @@ -42,25 +42,25 @@ "AlternatingGroup", "CyclicGroup", "DihedralGroup", + "Element", "Group", "GroupMember", + "GroupRing", "ProjectiveSpecialLinearGroup", + "Protograph", "QuaternionGroup", + "RingArray", + "RingMember", "SmallGroup", "SpecialLinearGroup", "SymmetricGroup", "TrivialGroup", - "get_coefficient_and_exponents", - "resolve_field", + "WedderburnArtinComponentTransformer", + "WedderburnArtinTransformer", "block_diag", + "get_coefficient_and_exponents", "get_howell_dual", "kron", "matmul", - "Element", - "GroupRing", - "Protograph", - "RingArray", - "RingMember", - "WedderburnArtinComponentTransformer", - "WedderburnArtinTransformer", + "resolve_field", ] diff --git a/src/qldpc/abstract/groups.py b/src/qldpc/abstract/groups.py index 7d4932176..d9d03826f 100644 --- a/src/qldpc/abstract/groups.py +++ b/src/qldpc/abstract/groups.py @@ -36,7 +36,7 @@ import math import operator from collections.abc import Callable, Iterator, Sequence -from typing import Any, Union +from typing import Any, ClassVar import galois import numpy as np @@ -60,7 +60,7 @@ def resolve_field( return field -NestedSequence = Sequence[Union[object, Sequence["NestedSequence"]]] +NestedSequence = Sequence[object | Sequence["NestedSequence"]] ################################################################################ # groups and group members @@ -251,7 +251,7 @@ def index(self, member: GroupMember) -> int: """The index of a GroupMember in this group.""" index = self._members.get(member) if not isinstance(index, int): - raise ValueError(f"Member {member} not in group {self}") + raise TypeError(f"Member {member} not in group {self}") return index def __mul__(self, other: Group) -> Group: @@ -279,7 +279,6 @@ def random(self, *, seed: int | None = None) -> GroupMember: return GroupMember.from_sympy(self._group.random()) - @functools.cache def regular_lift(self, member: GroupMember, *, right: bool = False) -> npt.NDArray[np.int_]: """Lift a group member to its regular representation. @@ -298,13 +297,19 @@ def regular_lift(self, member: GroupMember, *, right: bool = False) -> npt.NDArr See: - https://en.wikipedia.org/wiki/Regular_representation """ - matrix = np.zeros([self.order] * 2, dtype=int) - for ii, hh in enumerate(self.generate()): - jj = self.index(hh * member) if right else self.index(member * hh) - matrix[jj, ii] = 1 - return matrix + if (member, right) not in self._regular_lift_cache: + matrix = np.zeros([self.order] * 2, dtype=int) + for ii, hh in enumerate(self.generate()): + jj = self.index(hh * member) if right else self.index(member * hh) + matrix[jj, ii] = 1 + self._regular_lift_cache[member, right] = matrix + return self._regular_lift_cache[member, right] + + @functools.cached_property + def _regular_lift_cache(self) -> dict[tuple[GroupMember, bool], npt.NDArray[np.int_]]: + """Per-instance cache for regular_lift (avoids the memory leak of caching on methods).""" + return {} - @functools.cache def adjoint_lift(self, member: GroupMember) -> npt.NDArray[np.int_]: r"""Lift a group member to its adjoint representation. @@ -315,7 +320,16 @@ def adjoint_lift(self, member: GroupMember) -> npt.NDArray[np.int_]: If the group is abelian, the adjoint lift of every group member is the identity matrix. """ - return self.regular_lift(member) @ self.regular_lift(member, right=True).T + if member not in self._adjoint_lift_cache: + self._adjoint_lift_cache[member] = ( + self.regular_lift(member) @ self.regular_lift(member, right=True).T + ) + return self._adjoint_lift_cache[member] + + @functools.cached_property + def _adjoint_lift_cache(self) -> dict[GroupMember, npt.NDArray[np.int_]]: + """Per-instance cache for adjoint_lift (avoids the memory leak of caching on methods).""" + return {} @functools.cached_property def inversion_matrix(self) -> npt.NDArray[np.int_]: @@ -639,7 +653,7 @@ class QuaternionGroup(Group): # multiplication table for this group - _table = [ + _table: ClassVar = [ [0, 1, 2, 3, 4, 5, 6, 7], [1, 4, 3, 6, 5, 0, 7, 2], [2, 7, 4, 1, 6, 3, 0, 5], @@ -873,7 +887,7 @@ def __init__( for index, vec_bytes in enumerate(target_space): vec = np.frombuffer(vec_bytes, dtype=np.uint8).view(self.field) next_orbit = [root * member @ vec for root in roots] - next_vec = next((vec for vec in next_orbit if vec.tobytes() in target_space)) + next_vec = next(vec for vec in next_orbit if vec.tobytes() in target_space) next_index = target_space.index(next_vec.tobytes()) perm[index] = next_index generators.append(GroupMember(perm)) diff --git a/src/qldpc/abstract/groups_test.py b/src/qldpc/abstract/groups_test.py index 63b589367..edace8a31 100644 --- a/src/qldpc/abstract/groups_test.py +++ b/src/qldpc/abstract/groups_test.py @@ -70,10 +70,11 @@ def test_permutation_group(pytestconfig: pytest.Config) -> None: assert abstract.Group.from_generating_mats([[1]]) == abstract.CyclicGroup(1) - with pytest.raises(ValueError, match="not in group"): + with pytest.raises(TypeError, match="not in group"): abstract.CyclicGroup(1).index(abstract.GroupMember(2, 1)) assert isinstance(hash(group.hashable_generators()), int) + assert isinstance(hash(group), int) def test_trivial_group() -> None: diff --git a/src/qldpc/abstract/linalg.py b/src/qldpc/abstract/linalg.py index ab9c117ee..e5f7ef8e9 100644 --- a/src/qldpc/abstract/linalg.py +++ b/src/qldpc/abstract/linalg.py @@ -153,7 +153,7 @@ def get_howell_dual( WARNING: This method assumes--and does not verify--that matrix_hnf is in Howell normal form. """ ring = matrix_hnf.ring - transformer = transformer = transformer or ring.get_transformer() + transformer = transformer or ring.get_transformer() dual_matrix = np.zeros(matrix_hnf.shape, dtype=object) for row, col in enumerate(math.first_nonzero_cols(matrix_hnf)): pivot = matrix_hnf[row, col].copy() diff --git a/src/qldpc/abstract/rings.py b/src/qldpc/abstract/rings.py index 8a56f4cd0..b0e9fb0e6 100644 --- a/src/qldpc/abstract/rings.py +++ b/src/qldpc/abstract/rings.py @@ -30,13 +30,14 @@ import operator import warnings from collections.abc import Iterable, Iterator, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Literal, Union +from typing import TYPE_CHECKING, Any, Literal import galois import numpy as np import numpy.typing as npt import sympy.abc import sympy.core +from typing_extensions import Self import qldpc from qldpc import external @@ -516,7 +517,7 @@ def __getattribute__(self, name: str) -> Any: ################################################################################ # RingArray: RingMember-valued array -NestedSequence = Sequence[Union[object, Sequence["NestedSequence"]]] +NestedSequence = Sequence[object | Sequence["NestedSequence"]] class RingArray(npt.NDArray[np.object_]): @@ -528,14 +529,14 @@ def __new__( cls, data: npt.NDArray[np.object_] | NestedSequence, ring: GroupRing | Group | None = None, - ) -> RingArray: + ) -> Self: array = np.asarray(data, dtype=object).view(cls) ring = GroupRing(ring) if isinstance(ring, Group) else ring # identify the base group for this RingArray for value in array.ravel(): if not isinstance(value, RingMember): - raise ValueError( + raise TypeError( "Requirement failed: all entries of a RingArray must be RingMember-valued." "\nTry building an array with RingArray.build(...)" ) @@ -552,7 +553,8 @@ def __new__( def __array_finalize__(self, obj: npt.NDArray[np.object_] | None) -> None: """Propagate metadata to newly constructed arrays.""" - setattr(self, "_ring", getattr(obj, "_ring", None)) + # obj may be None or lack _ring during numpy view construction; _ring is set before use + self._ring = getattr(obj, "_ring", None) # type:ignore[assignment] def __array_function__( self, @@ -569,7 +571,7 @@ def __array_function__( result = super().__array_function__(func, types, args, kwargs) if isinstance(result, np.ndarray): result = result.view(RingArray) - setattr(result, "_ring", next(iter(rings), None)) + result._ring = next(iter(rings), None) # type:ignore[attr-defined] return result def __array_ufunc__( @@ -587,7 +589,7 @@ def __array_ufunc__( result = super().__array_ufunc__(ufunc, method, *inputs, **kwargs) if isinstance(result, np.ndarray): result = result.view(RingArray) - setattr(result, "_ring", next(iter(rings), None)) + result._ring = next(iter(rings), None) # type:ignore[attr-defined] return result def __str__(self) -> str: @@ -927,7 +929,7 @@ def howell_normal_form_poly(self) -> RingArray: """ assert self.ndim == 2 if not isinstance(self.group, CyclicGroup): - raise ValueError( + raise TypeError( "The Howell normal form induced by polynomial division requires an underlying" f" CyclicGroup, not {self.group}" ) diff --git a/src/qldpc/abstract/rings_test.py b/src/qldpc/abstract/rings_test.py index d5d7c688f..b04d5d36f 100644 --- a/src/qldpc/abstract/rings_test.py +++ b/src/qldpc/abstract/rings_test.py @@ -155,7 +155,7 @@ def test_ring_array(pytestconfig: pytest.Config) -> None: # fail to construct a valid ring array rings = [abstract.GroupRing(abstract.TrivialGroup(), field) for field in [2, 3]] - with pytest.raises(ValueError, match="must be RingMember-valued"): + with pytest.raises(TypeError, match="must be RingMember-valued"): abstract.RingArray([[0]]) with pytest.raises(ValueError, match="Cannot determine the underlying ring"): abstract.RingArray([]) @@ -272,7 +272,7 @@ def test_ring_row_reduction( abstract.RingArray.build([[1, 0], [1, 1]], ring).howell_normal_form() # the "polynomial" Howell normal form requires an underlying cyclic group - with pytest.raises(ValueError, match="requires an underlying CyclicGroup"): + with pytest.raises(TypeError, match="requires an underlying CyclicGroup"): abstract.RingArray.build([[1, 0], [1, 1]], ring).howell_normal_form(poly=True) # computing a reduced Groebner basis is the final boss @@ -305,14 +305,14 @@ def test_deprecations() -> None: vector = ring.field.Random(ring.group.order) with pytest.warns(DeprecationWarning, match="DEPRECATED"): ring_member = abstract.RingMember.from_vector(ring, vector) # type:ignore[arg-type] - assert np.array_equal(ring_member.to_vector(), vector) + assert np.array_equal(ring_member.to_vector(), vector) vector = ring.field.Random(2 * ring.group.order) with pytest.warns(DeprecationWarning, match="DEPRECATED"): ring_array = abstract.RingArray.from_field_vector(ring, vector) # type:ignore[arg-type] - assert np.array_equal(ring_array.to_field_vector(), vector) + assert np.array_equal(ring_array.to_field_vector(), vector) matrix = ring.field.Random((1, 2, ring.group.order)) with pytest.warns(DeprecationWarning, match="DEPRECATED"): ring_array = abstract.RingArray.from_field_array(ring, matrix) # type:ignore[arg-type] - assert np.array_equal(ring_array.to_field_array(), matrix) + assert np.array_equal(ring_array.to_field_array(), matrix) diff --git a/src/qldpc/abstract/wedderburn_artin.py b/src/qldpc/abstract/wedderburn_artin.py index e10f150ad..479a732bc 100644 --- a/src/qldpc/abstract/wedderburn_artin.py +++ b/src/qldpc/abstract/wedderburn_artin.py @@ -113,7 +113,7 @@ def recompose_array( f"Provided {len(components)} WedderburnArtinTransformer components for a ring that" f" should have {len(self.transformers)}" ) - if not len(set([array.shape[:-2] for array in components])) == 1: + if not len({array.shape[:-2] for array in components}) == 1: raise ValueError("Asked to combine arrays of inconsistent shapes") terms = [ trans.embed_array(array, from_blocks=from_blocks) @@ -592,7 +592,7 @@ def _get_primitive_idempotents( quotient_coeff: galois.Poly gcd: galois.Poly for factor, quotient in zip(factors, quotients): - gcd, factor_coeff, quotient_coeff = galois.egcd(factor, quotient) # type:ignore[assignment,arg-type] + gcd, _factor_coeff, quotient_coeff = galois.egcd(factor, quotient) # type:ignore[assignment,arg-type] idempotent_poly = quotient_coeff * quotient // gcd # <- G_j(x) new_idempotent = idempotent_poly.coeffs[::-1] @ powers[: len(idempotent_poly)] # <- e_j new_idempotents.append(new_idempotent) diff --git a/src/qldpc/circuits/__init__.py b/src/qldpc/circuits/__init__.py index 7ab60d13c..b2739cfb3 100644 --- a/src/qldpc/circuits/__init__.py +++ b/src/qldpc/circuits/__init__.py @@ -56,46 +56,46 @@ ) __all__ = [ - "get_logical_error_and_discard_rate", - "get_state_prep_diagnostic_circuit", - "get_state_prep_diagnostic_tasks", + "AbstractPauliChannel", + "AlphaSyndrome", + "DepolarizingNoiseModel", "DetectorRecord", + "EdgeColoring", + "EdgeColoringXZ", "MeasurementRecord", + "MemoryExperimentParts", + "NoiseModel", + "NoiseRule", + "PauliChannel", + "PauliChannelSequence", "QubitIDs", "Record", - "get_pauli_product_measurements", - "get_unaddressed_measurements", - "remap_qubit_target", - "restrict_to_qubits", - "with_remapped_qubits", + "SI1000NoiseModel", + "SyndromeMeasurementStrategy", + "as_noiseless_circuit", "get_encoder_and_decoder", "get_encoding_circuit", "get_encoding_tableau", + "get_logical_bell_prep", + "get_logical_error_and_discard_rate", "get_logical_state_stabilizers", "get_logical_tableau", - "get_state_stabilizers", - "restrict_tableau", - "AlphaSyndrome", - "EdgeColoring", - "EdgeColoringXZ", - "MemoryExperimentParts", - "SyndromeMeasurementStrategy", - "get_logical_bell_prep", "get_memory_experiment", "get_memory_experiment_parts", "get_observables", + "get_pauli_product_measurements", "get_qubit_coordinates", - "AbstractPauliChannel", - "DepolarizingNoiseModel", - "NoiseModel", - "NoiseRule", - "PauliChannel", - "PauliChannelSequence", - "SI1000NoiseModel", - "as_noiseless_circuit", + "get_state_prep_diagnostic_circuit", + "get_state_prep_diagnostic_tasks", + "get_state_stabilizers", "get_transversal_automorphism_group", "get_transversal_circuit", "get_transversal_circuits", "get_transversal_ops", "get_transversal_s", + "get_unaddressed_measurements", + "remap_qubit_target", + "restrict_tableau", + "restrict_to_qubits", + "with_remapped_qubits", ] diff --git a/src/qldpc/circuits/common.py b/src/qldpc/circuits/common.py index eeb735bcc..204c22682 100644 --- a/src/qldpc/circuits/common.py +++ b/src/qldpc/circuits/common.py @@ -18,8 +18,8 @@ from __future__ import annotations import functools -from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING, Callable, ParamSpec, TypeVar +from collections.abc import Callable, Mapping, Sequence +from typing import TYPE_CHECKING, ParamSpec, TypeVar import galois import numpy as np diff --git a/src/qldpc/circuits/encoding.py b/src/qldpc/circuits/encoding.py index 762a1d803..246450970 100644 --- a/src/qldpc/circuits/encoding.py +++ b/src/qldpc/circuits/encoding.py @@ -226,7 +226,7 @@ def get_state_stabilizers( simulator = stim.TableauSimulator() simulator.do(state_prep_circuit) for ss, stabilizer in enumerate(stabilizers): - if simulator.peek_observable_expectation(stabilizers[ss]) == -1: # pragma: no cover + if simulator.peek_observable_expectation(stabilizer) == -1: # pragma: no cover stabilizers[ss] = -stabilizer return stabilizers diff --git a/src/qldpc/circuits/memory/__init__.py b/src/qldpc/circuits/memory/__init__.py index 4c129faab..8aa9ff4c7 100644 --- a/src/qldpc/circuits/memory/__init__.py +++ b/src/qldpc/circuits/memory/__init__.py @@ -15,13 +15,13 @@ __all__ = [ "AlphaSyndrome", + "EdgeColoring", + "EdgeColoringXZ", "MemoryExperimentParts", + "SyndromeMeasurementStrategy", "get_logical_bell_prep", "get_memory_experiment", "get_memory_experiment_parts", "get_observables", "get_qubit_coordinates", - "EdgeColoring", - "EdgeColoringXZ", - "SyndromeMeasurementStrategy", ] diff --git a/src/qldpc/circuits/memory/alpha_syndrome.py b/src/qldpc/circuits/memory/alpha_syndrome.py index b3602bd74..2264ec068 100644 --- a/src/qldpc/circuits/memory/alpha_syndrome.py +++ b/src/qldpc/circuits/memory/alpha_syndrome.py @@ -41,6 +41,9 @@ # For the purposes of this schedule, a "gate" is just an ordered pair of target qubits. GateSchedule = list[list[tuple[int, int]]] +# default decoder used to estimate logical error rates while searching for a measurement schedule +DEFAULT_SINTER_DECODER = decoders.SinterDecoder() + class AlphaSyndrome(SyndromeMeasurementStrategy): """AlphaSyndrome strategy for constructing a syndrome measurement circuit. @@ -55,7 +58,7 @@ class AlphaSyndrome(SyndromeMeasurementStrategy): def __init__( self, noise_model: NoiseModel, - decoder: sinter.Decoder | str = decoders.SinterDecoder(), + decoder: sinter.Decoder | str = DEFAULT_SINTER_DECODER, iters_per_step: int = 1000, shots_per_iter: int = 10000, exploration_weight: float = math.sqrt(2), @@ -90,11 +93,12 @@ def __init__( # keyword arguments passed to sinter.predict_observables self.sinter_decoding_kwargs: dict[str, str | dict[str, sinter.Decoder]] if isinstance(decoder, str): - self.sinter_decoding_kwargs = dict(decoder=decoder) + self.sinter_decoding_kwargs = {"decoder": decoder} else: - self.sinter_decoding_kwargs = dict( - decoder="custom", custom_decoders=dict(custom=decoder) - ) + self.sinter_decoding_kwargs = { + "decoder": "custom", + "custom_decoders": {"custom": decoder}, + } @restrict_to_qubits def get_circuit( @@ -112,7 +116,7 @@ def get_circuit( circuits.MeasurementRecord: The record of measurements in the circuit. """ if not isinstance(code, codes.CSSCode): - raise ValueError( + raise TypeError( "The AlphaSyndrome strategy for syndrome measurement only supports CSS codes" ) qubit_ids = qubit_ids or QubitIDs.from_code(code) @@ -252,7 +256,7 @@ class TreeNode: - https://en.wikipedia.org/wiki/Monte_Carlo_tree_search """ - def __init__(self, state: TreeState, parent: TreeNode | None = None): + def __init__(self, state: TreeState, parent: TreeNode | None = None) -> None: self.state = state self.parent = parent diff --git a/src/qldpc/circuits/memory/alpha_syndrome_test.py b/src/qldpc/circuits/memory/alpha_syndrome_test.py index ea7aaf9d4..2ade2c30e 100644 --- a/src/qldpc/circuits/memory/alpha_syndrome_test.py +++ b/src/qldpc/circuits/memory/alpha_syndrome_test.py @@ -26,6 +26,13 @@ from qldpc import circuits, codes, math from qldpc.objects import Pauli +# default (fast, low-fidelity) strategy used by the validity-checking helper below +DEFAULT_STRATEGY = circuits.AlphaSyndrome( + circuits.DepolarizingNoiseModel(0.001), + iters_per_step=3, + shots_per_iter=1, +) + def test_alpha_syndrome(pytestconfig: pytest.Config) -> None: """Verify that syndromes are read out correctly.""" @@ -41,18 +48,14 @@ def test_alpha_syndrome(pytestconfig: pytest.Config) -> None: assert alpha_syndrome_is_valid(codes.HGPCode(code_a, code_b)) # AlphaSyndrome does not support non-CSS codes - with pytest.raises(ValueError, match="only supports CSS codes"): + with pytest.raises(TypeError, match="only supports CSS codes"): strategy = circuits.AlphaSyndrome(circuits.DepolarizingNoiseModel(0.001), "decoder_name") strategy.get_circuit(codes.FiveQubitCode()) def alpha_syndrome_is_valid( code: codes.QuditCode, - strategy: circuits.AlphaSyndrome = circuits.AlphaSyndrome( - circuits.DepolarizingNoiseModel(0.001), - iters_per_step=3, - shots_per_iter=1, - ), + strategy: circuits.AlphaSyndrome = DEFAULT_STRATEGY, ) -> bool: """Check that an AlphaSyndrome circuit correctly reads out stabilizers.""" # prepare a logical |0> state diff --git a/src/qldpc/circuits/memory/memory.py b/src/qldpc/circuits/memory/memory.py index 527579a44..4b7024544 100644 --- a/src/qldpc/circuits/memory/memory.py +++ b/src/qldpc/circuits/memory/memory.py @@ -22,6 +22,7 @@ import stim from qldpc import codes +from qldpc._util import format_docstring from qldpc.objects import Node, Pauli, PauliXZ from ..bookkeeping import DetectorRecord, MeasurementRecord, QubitIDs @@ -30,6 +31,9 @@ from ..noise_model import DEFAULT_IMMUNE_OP_TAG, NoiseModel, as_noiseless_circuit from .syndrome_measurement import EdgeColoring, SyndromeMeasurementStrategy +# default strategy used to schedule the two-qubit gates of a syndrome measurement circuit +DEFAULT_STRATEGY = EdgeColoring() + class MemoryExperimentParts(NamedTuple): initialization: stim.Circuit @@ -40,6 +44,7 @@ class MemoryExperimentParts(NamedTuple): qubit_ids: QubitIDs +@format_docstring(DEFAULT_IMMUNE_OP_TAG=DEFAULT_IMMUNE_OP_TAG) def get_memory_experiment( code: codes.QuditCode | codes.ClassicalCode, basis: PauliXZ | None = Pauli.X, @@ -47,9 +52,9 @@ def get_memory_experiment( *, noise_model: NoiseModel | None = None, qubit_ids: QubitIDs | None = None, - syndrome_measurement_strategy: SyndromeMeasurementStrategy = EdgeColoring(), + syndrome_measurement_strategy: SyndromeMeasurementStrategy = DEFAULT_STRATEGY, ) -> stim.Circuit: - f"""Construct a circuit for testing the performance of a code as a quantum memory. + """Construct a circuit for testing the performance of a code as a quantum memory. In a nutshell, the circuit constructed by this method performs (generally multiple) rounds quantum error correction (QEC) for the given code. Each round of QEC measures all parity checks @@ -98,8 +103,8 @@ def get_memory_experiment( must evaluate to 0 in the absence of errors, the preparation of Bell pairs allows us to annotate XX and ZZ observables for each Bell pair. Here one of the "X"s in XX is a logical X for a logical qubit of the code, and the other "X" is a physical X on an associated ancilla qubit; - likewise with ZZ. Since the ancilla qubit is noiseless, we can attribute an error in XX or ZZ to - a logical qubit error. + likewise with ZZ. Since the ancilla qubit is noiseless, we can attribute an error in XX or ZZ + to a logical qubit error. Having said all of that, we do not actually annotate memory simulation circuits with the XX and ZZ observables described above. Instead, we recognize that Bell-pair XX and ZZ operators are @@ -189,7 +194,7 @@ def get_memory_experiment_parts( num_rounds: int = 1, *, qubit_ids: QubitIDs | None = None, - syndrome_measurement_strategy: SyndromeMeasurementStrategy = EdgeColoring(), + syndrome_measurement_strategy: SyndromeMeasurementStrategy = DEFAULT_STRATEGY, ) -> MemoryExperimentParts: """Noiseless components of a memory experiment. @@ -236,7 +241,7 @@ def _get_basis_memory_experiment_parts( num_rounds: int = 1, *, qubit_ids: QubitIDs | None = None, - syndrome_measurement_strategy: SyndromeMeasurementStrategy = EdgeColoring(), + syndrome_measurement_strategy: SyndromeMeasurementStrategy = DEFAULT_STRATEGY, ) -> MemoryExperimentParts: """Components of a memory experiment that tracks logical operators of a fixed type (basis). @@ -248,7 +253,7 @@ def _get_basis_memory_experiment_parts( f" not {basis}" ) if not isinstance(code, codes.CSSCode): - raise ValueError("Memory experiments in a fixed basis only support CSS codes") + raise TypeError("Memory experiments in a fixed basis only support CSS codes") # identify all qubits by index qubit_ids = QubitIDs.validated(qubit_ids, code) if qubit_ids else QubitIDs.from_code(code) @@ -304,7 +309,7 @@ def _get_combined_memory_simulation_parts( num_rounds: int = 1, *, qubit_ids: QubitIDs | None = None, - syndrome_measurement_strategy: SyndromeMeasurementStrategy = EdgeColoring(), + syndrome_measurement_strategy: SyndromeMeasurementStrategy = DEFAULT_STRATEGY, ) -> MemoryExperimentParts: """Components of a memory experiment that tracks all logical operators. diff --git a/src/qldpc/circuits/memory/memory_test.py b/src/qldpc/circuits/memory/memory_test.py index 62cc1013d..6585fd093 100644 --- a/src/qldpc/circuits/memory/memory_test.py +++ b/src/qldpc/circuits/memory/memory_test.py @@ -63,7 +63,7 @@ def test_memory_experiment() -> None: # non-CSS and subsystem codes are not always supported with pytest.raises(ValueError, match=r"only support stabilizer \(non-subsystem\) codes"): circuits.get_memory_experiment(codes.BaconShorCode(2)) - with pytest.raises(ValueError, match=r"only support CSS codes"): + with pytest.raises(TypeError, match=r"only support CSS codes"): circuits.get_memory_experiment(codes.FiveQubitCode()) diff --git a/src/qldpc/circuits/memory/syndrome_measurement.py b/src/qldpc/circuits/memory/syndrome_measurement.py index 5220691ff..8ccaa2a24 100644 --- a/src/qldpc/circuits/memory/syndrome_measurement.py +++ b/src/qldpc/circuits/memory/syndrome_measurement.py @@ -174,7 +174,7 @@ def get_circuit( circuits.MeasurementRecord: The record of measurements in the circuit. """ if not isinstance(code, codes.CSSCode): - raise ValueError( + raise TypeError( "The EdgeColoringXZ strategy for syndrome measurement only supports CSS codes" ) diff --git a/src/qldpc/circuits/memory/syndrome_measurement_test.py b/src/qldpc/circuits/memory/syndrome_measurement_test.py index 83343d534..c8dabe25b 100644 --- a/src/qldpc/circuits/memory/syndrome_measurement_test.py +++ b/src/qldpc/circuits/memory/syndrome_measurement_test.py @@ -25,6 +25,9 @@ from qldpc import circuits, codes, math from qldpc.objects import Pauli +# default strategy used by the validity-checking helper below +DEFAULT_STRATEGY = circuits.EdgeColoring() + def test_syndrome_measurement(pytestconfig: pytest.Config) -> None: """Verify that syndromes are read out correctly.""" @@ -63,12 +66,12 @@ def test_syndrome_measurement(pytestconfig: pytest.Config) -> None: # EdgeColoringXZ strategy assert syndrome_measurement_is_valid(codes.SteaneCode(), circuits.EdgeColoringXZ()) - with pytest.raises(ValueError, match="only supports CSS codes"): + with pytest.raises(TypeError, match="only supports CSS codes"): circuits.EdgeColoringXZ().get_circuit(codes.FiveQubitCode()) def syndrome_measurement_is_valid( - code: codes.QuditCode, strategy: circuits.SyndromeMeasurementStrategy = circuits.EdgeColoring() + code: codes.QuditCode, strategy: circuits.SyndromeMeasurementStrategy = DEFAULT_STRATEGY ) -> bool: """Check the validity of syndrome measurement in a given code.""" # prepare a logical |0> state diff --git a/src/qldpc/circuits/noise_model.py b/src/qldpc/circuits/noise_model.py index bb3272954..bd844873e 100644 --- a/src/qldpc/circuits/noise_model.py +++ b/src/qldpc/circuits/noise_model.py @@ -116,6 +116,8 @@ def bad_qubit_noise(op: stim.CircuitInstruction) -> NoiseRule | None: import stim +from qldpc._util import format_docstring + from .common import stim_or_tsim_Circuit, tsim, with_remapped_qubits #################################################################################################### @@ -249,7 +251,9 @@ class AbstractPauliChannel(abc.ABC): Subclasses of ``AbstractPauliChannel`` are required to define a ``.to_circuit`` method. """ - def __init__(self, probabilities: Mapping[str, float], *, num_qubits: int | None = None): + def __init__( + self, probabilities: Mapping[str, float], *, num_qubits: int | None = None + ) -> None: """Validate and store a sparse Pauli-string -> probability mapping. Performs only *per-entry* validation (string shape / alphabet, not-identity, each @@ -372,7 +376,9 @@ class PauliChannel(AbstractPauliChannel): listed probabilities must sum to at most 1. """ - def __init__(self, probabilities: Mapping[str, float], *, num_qubits: int | None = None): + def __init__( + self, probabilities: Mapping[str, float], *, num_qubits: int | None = None + ) -> None: """Instantiate a Pauli channel. Args: @@ -593,7 +599,7 @@ def __init__( after: AbstractPauliChannel | Mapping[str, float] | stim.Circuit | None = None, readout_error: float | None = None, reset_error: float | None = None, - ): + ) -> None: """Initializes a noise rule with specified error channels. Args: @@ -776,7 +782,7 @@ def __init__( additional_error_waiting_for_m_or_r: ErrorSpec | None = None, rules: Mapping[str, NoiseRule] | None = None, rule_func: Callable[[stim.CircuitInstruction], NoiseRule | None] | None = None, - ): + ) -> None: """Initializes a noise model with specified parameters. Args: @@ -992,6 +998,7 @@ def get_noise_rule(self, op: stim.CircuitInstruction) -> NoiseRule | None: return None + @format_docstring(DEFAULT_IMMUNE_OP_TAG=DEFAULT_IMMUNE_OP_TAG) def noisy_circuit( self, circuit: stim_or_tsim_Circuit, @@ -1002,7 +1009,7 @@ def noisy_circuit( immunize_gates: bool = True, insert_ticks: bool = True, ) -> stim_or_tsim_Circuit: - f"""Construct a noisy version of the given circuit. + """Construct a noisy version of the given circuit. This method first uses TICKs to split the input circuit into moments of operations that can be applied in parallel, thereby preventing qubit reuse conflicts. Noise is then applied to @@ -1345,7 +1352,7 @@ def _validate_after_circuit(after_circuit: stim.Circuit) -> None: """ for op in after_circuit: if not isinstance(op, stim.CircuitInstruction): - raise ValueError( + raise TypeError( f"after (stim.Circuit form) may contain only noise instructions, not " f"{type(op).__name__}" ) diff --git a/src/qldpc/circuits/noise_model_test.py b/src/qldpc/circuits/noise_model_test.py index 58a626036..347a16120 100644 --- a/src/qldpc/circuits/noise_model_test.py +++ b/src/qldpc/circuits/noise_model_test.py @@ -958,7 +958,7 @@ def test_noise_rule_errors() -> None: circuits.NoiseRule(after=stim.Circuit("H 0")) fragment_with_repeat = stim.Circuit() fragment_with_repeat.append(stim.CircuitRepeatBlock(2, stim.Circuit("X_ERROR(0.1) 0"))) - with pytest.raises(ValueError, match="may contain only noise instructions"): + with pytest.raises(TypeError, match="may contain only noise instructions"): circuits.NoiseRule(after=fragment_with_repeat) # An empty PauliChannel with an explicitly-declared arity is honored — a mismatch against the diff --git a/src/qldpc/circuits/transversal_test.py b/src/qldpc/circuits/transversal_test.py index 379c0449a..7a38b1569 100644 --- a/src/qldpc/circuits/transversal_test.py +++ b/src/qldpc/circuits/transversal_test.py @@ -117,7 +117,7 @@ def test_finding_circuit( assert physical_circuit is not None # there are no logical two-qubit gates in this code - circuits.get_transversal_circuit(code, stim.Circuit("CX 0 1")) is None + assert circuits.get_transversal_circuit(code, stim.Circuit("CX 0 1")) is None # check that the physical circuit has the correct logical tableau reconstructed_logical_tableau = circuits.get_logical_tableau(code, physical_circuit) diff --git a/src/qldpc/codes/__init__.py b/src/qldpc/codes/__init__.py index adc3eb235..17433ec9c 100644 --- a/src/qldpc/codes/__init__.py +++ b/src/qldpc/codes/__init__.py @@ -51,47 +51,47 @@ ) __all__ = [ - "BCHCode", - "CyclicCode", - "ExtendedHammingCode", - "HammingCode", - "ReedMullerCode", - "ReedSolomonCode", - "RepetitionCode", - "RingCode", - "SimplexCode", - "TannerCode", "AbstractCode", - "ClassicalCode", - "CSSCode", - "ErrorRateFunc", - "QuditCode", - "get_distance_classical", - "get_distance_quantum", - "BaconShorCode", "BBCode", + "BCHCode", + "BaconShorCode", "C4Code", "C6Code", "CHGPCode", "CRCode", + "CSSCode", + "ClassicalCode", + "CyclicCode", + "ErrorRateFunc", + "ExtendedHammingCode", "FiveQubitCode", "FiveQuditCode", "GeneralizedSurfaceCode", "HGPCode", + "HammingCode", "IcebergCode", "LPCode", "ManyHypercubeCode", "QCCode", "QTCode", "QuantumHammingCode", + "QuditCode", + "ReedMullerCode", + "ReedSolomonCode", + "RepetitionCode", + "RingCode", "SHPCode", "SHYPSCode", "SLPCode", + "SimplexCode", "SteaneCode", "SurfaceCode", "T4Code", "TBCode", + "TannerCode", "TetrahedralCode", "ToricCode", "TrivialCode", + "get_distance_classical", + "get_distance_quantum", ] diff --git a/src/qldpc/codes/classical.py b/src/qldpc/codes/classical.py index cac3749ec..00ae6d5a1 100644 --- a/src/qldpc/codes/classical.py +++ b/src/qldpc/codes/classical.py @@ -270,7 +270,7 @@ def get_defining_polynomial( primitive_polys = galois.primitive_polys(order=field.order, degree=dim, terms=3) return next(primitive_polys) except StopIteration: - None + pass # find a suitable polynomial by brute force diff --git a/src/qldpc/codes/common.py b/src/qldpc/codes/common.py index 2620af76f..01a672c78 100644 --- a/src/qldpc/codes/common.py +++ b/src/qldpc/codes/common.py @@ -24,8 +24,8 @@ import itertools import random import warnings -from collections.abc import Callable, Collection, Iterable, Mapping, Sequence -from typing import Any, Iterator, TypeVar, cast +from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, Sequence +from typing import Any, TypeVar, cast import galois import networkx as nx @@ -78,6 +78,7 @@ def get_random_array( class AbstractCode(abc.ABC): """Base class for error-correcting codes.""" + _name: str _matrix: galois.FieldArray _field: type[galois.FieldArray] _dimension: int | None = None @@ -310,9 +311,7 @@ def matrix_to_graph(matrix: npt.NDArray[np.int_] | Sequence[Sequence[int]]) -> n node_c = Node(index=int(row), is_data=False) node_d = Node(index=int(col), is_data=True) graph.add_edge(node_c, node_d, val=matrix[row][col]) - setattr( - graph, "field", type(matrix) if isinstance(matrix, galois.FieldArray) else galois.GF2 - ) + graph.field = type(matrix) if isinstance(matrix, galois.FieldArray) else galois.GF2 return graph @staticmethod @@ -405,13 +404,13 @@ def tensor_product(code_a: ClassicalCode, code_b: ClassicalCode) -> ClassicalCod def get_code_params( self, *, bound: int | bool | None = None, **bound_kwargs: Any ) -> tuple[int, int, int | float]: - f"""Compute the parameters of this code: [n,k,d]. + """Compute the parameters of this code: [n,k,d]. Args: bound: If False, 0, or None (the default), compute the exact code distance. Otherwise, compute an upper bound on code distance by minimizing over int(bound) independent - randomized upper bounds; see help({type(self)}.get_distance_bound). - **bound_kwargs: Keyword arguments to pass to {type(self)}.get_distance_bound. + randomized upper bounds; see help(get_distance_bound). + **bound_kwargs: Keyword arguments to pass to get_distance_bound. Returns: A tuple of integers, (n, k, d), where: @@ -430,15 +429,15 @@ def get_distance( vector: Sequence[int] | npt.NDArray[np.int_] | None = None, **bound_kwargs: Any, ) -> int | float: - f"""Compute (or upper bound) the minimum Hamming weight of nontrivial code words. + """Compute (or upper bound) the minimum Hamming weight of nontrivial code words. Args: bound: If False, 0, or None (the default), compute the exact code distance. Otherwise, compute an upper bound on code distance by minimizing over int(bound) independent - randomized upper bounds; see help({type(self)}.get_distance_bound). + randomized upper bounds; see help(get_distance_bound). vector: If not None, rather than computing the code distance, compute the minimum Hamming distance between this vector and a code word. Default: None. - **bound_kwargs: Keyword arguments to pass to {type(self)}.get_distance_bound. + **bound_kwargs: Keyword arguments to pass to get_distance_bound. Returns: An integer distance (or bound) if it is defined, and np.nan otherwise. @@ -611,7 +610,7 @@ def from_name(name: str) -> ClassicalCode: standardized_name = name.strip().replace(" ", "") # strip whitespace matrix, field = external.codes.get_classical_code(standardized_name) code = ClassicalCode(matrix, field) - setattr(code, "_name", name) + code._name = name return code def get_automorphism_group(self, *, with_magma: bool = False) -> abstract.Group: @@ -912,9 +911,7 @@ def matrix_to_graph(matrix: npt.NDArray[np.int_] | Sequence[Sequence[int]]) -> n # initialize graph with nodes graph = nx.DiGraph() - setattr( - graph, "field", type(matrix) if isinstance(matrix, galois.FieldArray) else galois.GF2 - ) + graph.field = type(matrix) if isinstance(matrix, galois.FieldArray) else galois.GF2 for qudit in range(matrix.shape[-1]): graph.add_node(Node(index=qudit, is_data=True)) @@ -960,7 +957,7 @@ def to_css(self) -> CSSCode: """Try to convert this QuditCode into a CSSCode. Throw an error if we fail.""" code = self.maybe_to_css() if not isinstance(code, CSSCode): - raise ValueError( + raise TypeError( "Failed to convert a QuditCode into a CSSCode." "\nSome parity checks have both X and Z support:" f"\n{self}" @@ -1050,7 +1047,7 @@ def from_strings( for "X(a)*Z(a)", and strings such as "Z(1) _ X(1)*Z(3) X(2)" are also valid. """ field = abstract.resolve_field(field) - operator: type[Pauli] | type[QuditPauli] = Pauli if field is galois.GF2 else QuditPauli + operator: type[Pauli | QuditPauli] = Pauli if field is galois.GF2 else QuditPauli def parse_check(check: str) -> list[str]: check = check.replace("_", "I") @@ -1154,7 +1151,7 @@ def get_logical_ops( matrix, qudit_locs, (rows_sx, rows_gx, rows_sz, rows_gz), - (cols_sx, cols_gx, cols_lx, cols_sz, cols_gz, cols_lz), + (cols_sx, cols_gx, cols_lx, cols_sz, _cols_gz, cols_lz), ) = self.get_standard_form_data() matrix_x = matrix[:, 0, :] matrix_z = matrix[:, 1, :] @@ -1674,13 +1671,13 @@ def gauge_dimension(self) -> int: def get_code_params( self, *, bound: int | bool | None = None, **bound_kwargs: Any ) -> tuple[int, int, int | float]: - f"""Compute the parameters of this code: [n,k,d]. + """Compute the parameters of this code: [n,k,d]. Args: bound: If False, 0, or None (the default), compute the exact code distance. Otherwise, compute an upper bound on code distance by minimizing over int(bound) independent - randomized upper bounds; see help({type(self)}.get_distance_bound). - **bound_kwargs: Keyword arguments to pass to {type(self)}.get_distance_bound. + randomized upper bounds; see help(get_distance_bound). + **bound_kwargs: Keyword arguments to pass to get_distance_bound. Returns: A tuple of integers, (n, k, d), where: @@ -1693,13 +1690,13 @@ def get_code_params( return len(self), dimension, distance def get_distance(self, *, bound: int | bool | None = None, **bound_kwargs: Any) -> int | float: - f"""Compute (or upper bound) the minimum weight of nontrivial logical operators. + """Compute (or upper bound) the minimum weight of nontrivial logical operators. Args: bound: If False, 0, or None (the default), compute the exact code distance. Otherwise, compute an upper bound on code distance by minimizing over int(bound) independent - randomized upper bounds; see help({type(self)}.get_distance_bound). - **bound_kwargs: Keyword arguments to pass to {type(self)}.get_distance_bound. + randomized upper bounds; see help(get_distance_bound). + **bound_kwargs: Keyword arguments to pass to get_distance_bound. Returns: An integer distance (or bound) if it is defined, and np.nan otherwise. @@ -2446,7 +2443,7 @@ def get_logical_ops( matrix_z, qudit_locs, (rows_sx, rows_gx, rows_sz, rows_gz), - (cols_sx, cols_gx, cols_lx, cols_sz, cols_gz, cols_lz), + (cols_sx, cols_gx, cols_lx, cols_sz, _cols_gz, cols_lz), ) = self.get_standard_form_data_xz() # X/Z support of X/Z logical operators, as column vectors @@ -2751,7 +2748,7 @@ def dual(self) -> CSSCode: def get_distance( self, pauli: PauliXZ | None = None, *, bound: int | bool | None = None, **bound_kwargs: Any ) -> int | float: - f"""Compute (or upper bound) the minimum weight of nontrivial logical operators. + """Compute (or upper bound) the minimum weight of nontrivial logical operators. Args: pauli: If passed qldpc.objects.Pauli.X, compute the X-distance (minimum weight of an @@ -2759,8 +2756,8 @@ def get_distance( If None (the default), minimize over X and Z. bound: If False, 0, or None (the default), compute the exact code distance. Otherwise, compute an upper bound on code distance by minimizing over int(bound) independent - randomized upper bounds; see help({type(self)}.get_distance_bound). - **bound_kwargs: Keyword arguments to pass to {type(self)}.get_distance_bound. + randomized upper bounds; see help(get_distance_bound). + **bound_kwargs: Keyword arguments to pass to get_distance_bound. Returns: An integer distance (or bound) if it is defined, and np.nan otherwise. diff --git a/src/qldpc/codes/common_test.py b/src/qldpc/codes/common_test.py index 894108681..2c0cb7e15 100644 --- a/src/qldpc/codes/common_test.py +++ b/src/qldpc/codes/common_test.py @@ -20,7 +20,7 @@ import functools import itertools import unittest.mock -from typing import Iterator +from collections.abc import Iterator import galois import networkx as nx @@ -163,8 +163,8 @@ def test_distance_classical(bits: int = 3) -> None: trivial_code = codes.ClassicalCode([[1, 0], [1, 1]]) random_vector = np.random.randint(2, size=len(trivial_code)) assert trivial_code.dimension == 0 - assert trivial_code.get_distance_exact() is np.nan - assert trivial_code.get_distance_bound() is np.nan + assert np.isnan(trivial_code.get_distance_exact()) + assert np.isnan(trivial_code.get_distance_bound()) assert ( np.count_nonzero(random_vector) == trivial_code.get_distance_exact(vector=random_vector) @@ -197,7 +197,8 @@ def test_automorphism(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFix codes.RepetitionCode(2).get_automorphism_group() # otherwise, check that automorphisms do indeed preserve the code space - with ( + # this pytest.warns block intentionally wraps a loop of warning-emitting calls + with ( # noqa: PT031 unittest.mock.patch("qldpc.external.gap.is_installed", return_value=True), pytest.warns(UserWarning, match="with_magma=True"), ): @@ -366,12 +367,13 @@ def test_distance_qudit() -> None: assert code.get_distance(bound=True) == -1 # the distance of dimension-0 codes is undefined - assert codes.QuditCode([[0, 1]]).get_distance() is np.nan + assert np.isnan(codes.QuditCode([[0, 1]]).get_distance()) # fallback pythonic brute-force distance calculation code = codes.QuditCode(codes.SurfaceCode(2, field=3).matrix) with pytest.warns(UserWarning, match=r"may take a \(very\) long time"): assert code.get_distance_exact(cutoff=len(code)) <= len(code) + with pytest.warns(UserWarning, match=r"may take a \(very\) long time"): assert code.get_distance_exact() == 2 @@ -570,7 +572,7 @@ def test_qudit_to_css() -> None: code = codes.SteaneCode() assert code.is_equiv_to(codes.QuditCode(code.matrix).to_css()) - with pytest.raises(ValueError, match="both X and Z support"): + with pytest.raises(TypeError, match="both X and Z support"): codes.FiveQubitCode().to_css() @@ -589,7 +591,7 @@ def test_qudit_to_swel() -> None: # a non-CSS code cannot be converted to SWEL assert codes.FiveQubitCode().maybe_to_swel() == codes.FiveQubitCode() - with pytest.raises(ValueError, match="both X and Z support"): + with pytest.raises(TypeError, match="both X and Z support"): codes.FiveQubitCode().to_swel() # a CSS code that is not SWEL cannot be converted to SWEL @@ -761,14 +763,15 @@ def test_distance_css() -> None: assert code.get_distance(bound=True) == -1 with pytest.warns(UserWarning, match=r"may take a \(very\) long time"): assert code.get_distance_exact(cutoff=len(code)) <= len(code) + with pytest.warns(UserWarning, match=r"may take a \(very\) long time"): assert code.get_distance_exact() == 2 # the distance of a dimension-0 quantum code is undefined trivial_code = codes.ClassicalCode([[1, 0], [1, 1]]) code = codes.HGPCode(trivial_code) assert code.dimension == 0 - assert code.get_distance(bound=True) is np.nan - assert code.get_distance(bound=False) is np.nan + assert np.isnan(code.get_distance(bound=True)) + assert np.isnan(code.get_distance(bound=False)) def test_css_deformations() -> None: diff --git a/src/qldpc/codes/distance.py b/src/qldpc/codes/distance.py index 63196a0ff..0e9071846 100644 --- a/src/qldpc/codes/distance.py +++ b/src/qldpc/codes/distance.py @@ -266,7 +266,7 @@ def _get_hamming_weight_fn( return weight_fn, 0 if getattr(np, "bitwise_count", None) is not None: - weight_fn = getattr(np, "bitwise_count") + weight_fn = np.bitwise_count return weight_fn, 0 return _hamming_weight, 1 @@ -282,7 +282,7 @@ def _get_symplectic_weight_fn( return weight_fn, 0 if getattr(np, "bitwise_count", None) is not None: - np_bitwise_count = getattr(np, "bitwise_count") + np_bitwise_count = np.bitwise_count def weight_fn( arr: npt.NDArray[np.uint64], diff --git a/src/qldpc/codes/quantum.py b/src/qldpc/codes/quantum.py index b99ba673b..409ab1517 100644 --- a/src/qldpc/codes/quantum.py +++ b/src/qldpc/codes/quantum.py @@ -62,7 +62,7 @@ def __init__( gauge_dimension: int = 0, self_dual: bool = False, field: int | type[galois.FieldArray] | None = None, - ): + ) -> None: """Initialize a trivial code with the given code parameters. If num_stabs_z is not None, then num_stabs is the number of X-type stabilizers. @@ -882,7 +882,7 @@ def is_valid_basis(self, vec_a: tuple[int, int], vec_b: tuple[int, int]) -> bool """ order_a = self.get_order(vec_a) order_b = self.get_order(vec_b) - if not order_a * order_b == len(self) // 2: + if order_a * order_b != len(self) // 2: return False # brute-force determine whether every plaquette can be reached by the basis vectors @@ -1219,7 +1219,7 @@ def _get_distance_exact(self, pauli: PauliXZ | None) -> int | float: dist_a = self.code_a.get_distance() dist_b = ClassicalCode(self.code_b.matrix.T).get_distance() - return dist_a if dist_b is np.nan else dist_b if dist_a is np.nan else min(dist_a, dist_b) + return dist_a if np.isnan(dist_b) else dist_b if np.isnan(dist_a) else min(dist_a, dist_b) class CHGPCode(HGPCode): @@ -1757,7 +1757,7 @@ def get_subgraphs(cayplex: CayleyComplex) -> tuple[nx.DiGraph, nx.DiGraph]: identity = member * ~member # identify the set of nodes for which we still need to add faces - nodes_to_add = set([identity]) + nodes_to_add = {identity} # build the subgraphs one node at a time subgraph_x = nx.DiGraph() @@ -1817,8 +1817,7 @@ def save(self, path: str, *headers: str) -> None: with open(path, "w") as file: # write provided headers for header in headers: - for line in header.splitlines(): - file.write(f"# {line}\n") + file.writelines(f"# {line}\n" for line in header.splitlines()) # write subsets file.write("# subset_a:\n") @@ -1860,8 +1859,8 @@ def load(path: str) -> QTCode: last_index = index # construct subsets and generating codes - subset_a = set(abstract.GroupMember(gen) for gen in arrays[0]) - subset_b = set(abstract.GroupMember(gen) for gen in arrays[1]) + subset_a = {abstract.GroupMember(gen) for gen in arrays[0]} + subset_b = {abstract.GroupMember(gen) for gen in arrays[1]} code_a = ClassicalCode(arrays[2], field) code_b = ClassicalCode(arrays[3], field) return QTCode(subset_a, subset_b, code_a, code_b, bipartite=bipartite) @@ -1973,9 +1972,9 @@ def get_check(row: int, col: int) -> npt.NDArray[np.int_]: row_indices = [row - 1, row, row - 1, row] col_indices = [col - 1, col - 1, col, col] check = np.zeros((rows, cols), dtype=int) - for row, col in zip(row_indices, col_indices): - if 0 <= row < rows and 0 <= col < cols: - check[row, col] = 1 + for check_row, check_col in zip(row_indices, col_indices): + if 0 <= check_row < rows and 0 <= check_col < cols: + check[check_row, check_col] = 1 return check.ravel() checks_x = [] @@ -2159,8 +2158,8 @@ def get_check(row: int, col: int) -> npt.NDArray[np.int_]: row_indices = np.array([row - 1, row, row - 1, row]) % rows col_indices = np.array([col - 1, col - 1, col, col]) % cols check = np.zeros((rows, cols), dtype=int) - for row, col in zip(row_indices, col_indices): - check[row, col] = 1 + for check_row, check_col in zip(row_indices, col_indices): + check[check_row, check_col] = 1 return check.ravel() checks_x = [] diff --git a/src/qldpc/decoders/__init__.py b/src/qldpc/decoders/__init__.py index 696a1bdb9..dd3cb9ed8 100644 --- a/src/qldpc/decoders/__init__.py +++ b/src/qldpc/decoders/__init__.py @@ -41,16 +41,27 @@ __all__ = [ "BatchDecoder", + "CompiledSequentialWindowDecoder", + "CompiledSinterDecoder", + "CompiledSubgraphDecoder", + "CompiledTrivialDecoder", "CompositeDecoder", "Decoder", + "DetectorErrorModelArrays", "DirectDecoder", + "FlipPattern", "GUFDecoder", "ILPDecoder", "LookupDecoder", "RelayBPDecoder", + "SequentialSinterDecoder", + "SequentialWindowDecoder", + "SinterDecoder", + "SlidingWindowDecoder", + "SubgraphDecoder", + "SubgraphSinterDecoder", + "TrivialDecoder", "WeightedLookupDecoder", - "DetectorErrorModelArrays", - "FlipPattern", "decode", "get_decoder", "get_decoder_BF", @@ -58,18 +69,7 @@ "get_decoder_BP_OSD", "get_decoder_GUF", "get_decoder_ILP", - "get_decoder_lookup", "get_decoder_MWPM", "get_decoder_RBP", - "CompiledSequentialWindowDecoder", - "CompiledSinterDecoder", - "CompiledSubgraphDecoder", - "CompiledTrivialDecoder", - "SequentialSinterDecoder", - "SequentialWindowDecoder", - "SinterDecoder", - "SlidingWindowDecoder", - "SubgraphDecoder", - "SubgraphSinterDecoder", - "TrivialDecoder", + "get_decoder_lookup", ] diff --git a/src/qldpc/decoders/custom.py b/src/qldpc/decoders/custom.py index 99723f6a2..c87990c5e 100644 --- a/src/qldpc/decoders/custom.py +++ b/src/qldpc/decoders/custom.py @@ -129,7 +129,7 @@ def __init__( "See 'import relay_bp; help(relay_bp.bp)' for available Relay-BP decoders" ) if isinstance(pcm_or_dem, str): - raise ValueError( + raise TypeError( "I think you provided a Relay-BP decoder decoder name in place of a parity check" " matrix. There was breaking change to this API. See" " help(qldpc.decoders.RelayBPDecoder)" @@ -604,7 +604,7 @@ def __init__(self, matrix: IntegerArray, **decoder_args: object) -> None: matrix = matrix.todense() self.matrix = np.asarray(matrix, dtype=int) % self.modulus - num_checks, num_variables = self.matrix.shape + _num_checks, num_variables = self.matrix.shape # variables, their constraints, and the objective (minimizing number of nonzero variables) self.variable_constraints = [] @@ -726,14 +726,12 @@ def decode( syndrome_bits = np.flatnonzero(syndrome) # construct an "error set", within which we look for solutions to the decoding problem - error_set = set(Node(int(index), is_data=False) for index in syndrome_bits) + error_set = {Node(int(index), is_data=False) for index in syndrome_bits} solutions = np.zeros((0, len(self.code)), dtype=int) last_error_set_size = 0 while solutions.size == 0: # grow the error set by one step on the Tanner graph - error_set |= set( - neighbor for node in error_set for neighbor in self.graph.neighbors(node) - ) + error_set |= {neighbor for node in error_set for neighbor in self.graph.neighbors(node)} # if the error set has not grown, there is no valid solution, so exit now if len(error_set) == last_error_set_size: @@ -801,9 +799,9 @@ def get_sub_problem_indices( ] # identify interior data bit nodes, and their neighbors interior_data_nodes = [node for node in interior_nodes if node.is_data] - check_nodes = set(node for node in error_set if not node.is_data) | set( + check_nodes = {node for node in error_set if not node.is_data} | { neighbor for node in interior_data_nodes for neighbor in self.graph.neighbors(node) - ) + } checks = [node.index for node in check_nodes] bits = [node.index for node in interior_data_nodes] diff --git a/src/qldpc/decoders/custom_test.py b/src/qldpc/decoders/custom_test.py index ce2956475..75ffbbcdd 100644 --- a/src/qldpc/decoders/custom_test.py +++ b/src/qldpc/decoders/custom_test.py @@ -72,7 +72,7 @@ def test_relay_bp() -> None: decoders.get_decoder(np.array([[]]), with_RBP=True, name="invalid_name") # fail when a decoder name string is passed where the matrix should be - with pytest.raises(ValueError, match="breaking change"): + with pytest.raises(TypeError, match="breaking change"): decoders.RelayBPDecoder("MinSumBPDecoderF32") # passing explicit error_priors alongside a DEM emits a warning diff --git a/src/qldpc/decoders/dems.py b/src/qldpc/decoders/dems.py index d4e4a64c6..63b81ca55 100644 --- a/src/qldpc/decoders/dems.py +++ b/src/qldpc/decoders/dems.py @@ -20,7 +20,8 @@ import collections import dataclasses import itertools -from collections.abc import Collection, Hashable, Iterable, Set +from collections.abc import Collection, Hashable, Iterable +from collections.abc import Set as AbstractSet from typing import TypeVar import numpy as np @@ -43,7 +44,7 @@ def __init__(self, detectors: Iterable[int] = (), observables: Iterable[int] = ( object.__setattr__(self, "observables", _xor_reduce(observables)) @classmethod - def from_data(cls, detectors: Set[int], observables: Set[int]) -> FlipPattern: + def from_data(cls, detectors: AbstractSet[int], observables: AbstractSet[int]) -> FlipPattern: """Construct from sets, skipping the mod-2 pass at normal initialization.""" instance = object.__new__(cls) object.__setattr__(instance, "detectors", frozenset(detectors)) diff --git a/src/qldpc/decoders/retrieval.py b/src/qldpc/decoders/retrieval.py index d656dbdd2..700043a52 100644 --- a/src/qldpc/decoders/retrieval.py +++ b/src/qldpc/decoders/retrieval.py @@ -29,6 +29,7 @@ import scipy.sparse import stim +from qldpc._util import format_docstring from qldpc.math import IntegerArray from .custom import ( @@ -75,12 +76,12 @@ def get_decoder( # optionally inject a static decoder, ignoring all other arguments if (static_decoder := decoder_args.pop("static_decoder", None)) is not None: - assert hasattr(static_decoder, "decode") and callable(getattr(static_decoder, "decode")) + assert hasattr(static_decoder, "decode") and callable(static_decoder.decode) assert not decoder_args, "If passed a static decoder, we cannot process decoding arguments" return static_decoder # look for and construct a recognized decoder - for name in DECODER_CONSTRUCTORS.keys(): + for name in DECODER_CONSTRUCTORS: if decoder_args.pop(f"with_{name}", False): decoder_constructor = getattr(sys.modules[__name__], f"get_decoder_{name}") return decoder_constructor(pcm_or_dem, **decoder_args) @@ -94,6 +95,7 @@ def get_decoder( return get_decoder_BP_OSD(pcm_or_dem, **decoder_args) # type:ignore[arg-type] +@format_docstring(PLACEHOLDER_ERROR_RATE=PLACEHOLDER_ERROR_RATE) def get_decoder_BP_OSD( pcm_or_dem: IntegerArray | stim.DetectorErrorModel, *, @@ -101,7 +103,7 @@ def get_decoder_BP_OSD( error_channel: npt.NDArray[np.floating] | Sequence[float] | None = None, **decoder_args: object, ) -> Decoder: - f"""Decoder based on belief propagation with ordered statistics (BP+OSD). + """Decoder based on belief propagation with ordered statistics (BP+OSD). Args: pcm_or_dem: A parity check matrix or detector error model (DEM) to decode. @@ -125,6 +127,7 @@ def get_decoder_BP_OSD( return ldpc.BpOsdDecoder(pcm, error_channel=error_channel, **decoder_args) +@format_docstring(PLACEHOLDER_ERROR_RATE=PLACEHOLDER_ERROR_RATE) def get_decoder_BP_LSD( pcm_or_dem: IntegerArray | stim.DetectorErrorModel, *, @@ -132,7 +135,7 @@ def get_decoder_BP_LSD( error_channel: npt.NDArray[np.floating] | Sequence[float] | None = None, **decoder_args: object, ) -> Decoder: - f"""Decoder based on belief propagation with localized statistics (BP+LSD). + """Decoder based on belief propagation with localized statistics (BP+LSD). Args: pcm_or_dem: A parity check matrix or detector error model (DEM) to decode. @@ -156,6 +159,7 @@ def get_decoder_BP_LSD( return ldpc.bplsd_decoder.BpLsdDecoder(pcm, error_channel=error_channel, **decoder_args) +@format_docstring(PLACEHOLDER_ERROR_RATE=PLACEHOLDER_ERROR_RATE) def get_decoder_BF( pcm_or_dem: IntegerArray | stim.DetectorErrorModel, *, @@ -163,7 +167,7 @@ def get_decoder_BF( error_channel: npt.NDArray[np.floating] | Sequence[float] | None = None, **decoder_args: object, ) -> Decoder: - f"""Decoder based on belief finding (BF). + """Decoder based on belief finding (BF). Args: pcm_or_dem: A parity check matrix or detector error model (DEM) to decode. diff --git a/src/qldpc/decoders/sinter.py b/src/qldpc/decoders/sinter.py index d06674171..9a8063ad1 100644 --- a/src/qldpc/decoders/sinter.py +++ b/src/qldpc/decoders/sinter.py @@ -760,7 +760,7 @@ def compile_decoder_for_dem( for detector in detectors: time = self.detector_to_time(detector) if not isinstance(time, int): # pragma: no cover - raise ValueError( + raise TypeError( f"detector {detector} has an invalid (non-integer) time index: {time}" ) time_to_dets[time].append(detector) diff --git a/src/qldpc/decoders/sinter_test.py b/src/qldpc/decoders/sinter_test.py index 15e43f6ca..79a3a91fc 100644 --- a/src/qldpc/decoders/sinter_test.py +++ b/src/qldpc/decoders/sinter_test.py @@ -78,7 +78,7 @@ def test_subgraph_decoding() -> None: error(0.1) D2 L2 """) sampler = dem.compile_sampler() - det_data, obs_data, err_data = sampler.sample(100) + det_data, obs_data, _err_data = sampler.sample(100) # build a monolithic lookup-table decoder, compile, and predict observable flips decoder_1 = decoders.SinterDecoder(with_lookup=True, max_weight=3) @@ -113,7 +113,7 @@ def test_sequential_decoding() -> None: error(0.1) D2 L2 """) sampler = dem.compile_sampler() - det_data, obs_data, err_data = sampler.sample(100) + det_data, obs_data, _err_data = sampler.sample(100) # build a monolithic lookup-table decoder, compile, and predict observable flips decoder_1 = decoders.SinterDecoder(with_lookup=True, max_weight=3) diff --git a/src/qldpc/external/codes_test.py b/src/qldpc/external/codes_test.py index e1a1c7ae5..51ecde0a3 100644 --- a/src/qldpc/external/codes_test.py +++ b/src/qldpc/external/codes_test.py @@ -72,7 +72,7 @@ def test_get_quantum_code() -> None: dist_line = "