diff --git a/CHANGELOG.md b/CHANGELOG.md index 66dd1094b..bc397efd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - #277: The method `Pattern.print_pattern` is now deprecated. -- Moved all device interface functionalities to an external library and removed their implementation from this library. +- #261: Moved all device interface functionalities to an external library and removed their implementation from this library. ## [0.3.1] - 2025-04-21 diff --git a/graphix/command.py b/graphix/command.py index 97c6d656a..a2fc52b1d 100644 --- a/graphix/command.py +++ b/graphix/command.py @@ -18,7 +18,7 @@ # Ruff suggests to move this import to a type-checking block, but dataclass requires it here from graphix.parameter import ExpressionOrFloat # noqa: TC001 from graphix.pauli import Pauli -from graphix.pretty_print import DataclassPrettyPrintMixin +from graphix.repr_mixins import DataclassReprMixin from graphix.states import BasicStates, State Node = int @@ -46,7 +46,7 @@ def __init_subclass__(cls) -> None: @dataclasses.dataclass(repr=False) -class N(_KindChecker, DataclassPrettyPrintMixin): +class N(_KindChecker, DataclassReprMixin): r"""Preparation command. Parameters @@ -63,7 +63,7 @@ class N(_KindChecker, DataclassPrettyPrintMixin): @dataclasses.dataclass(repr=False) -class M(_KindChecker, DataclassPrettyPrintMixin): +class M(_KindChecker, DataclassReprMixin): r"""Measurement command. Parameters @@ -112,7 +112,7 @@ def clifford(self, clifford_gate: Clifford) -> M: @dataclasses.dataclass(repr=False) -class E(_KindChecker, DataclassPrettyPrintMixin): +class E(_KindChecker, DataclassReprMixin): r"""Entanglement command between two qubits. Parameters @@ -126,7 +126,7 @@ class E(_KindChecker, DataclassPrettyPrintMixin): @dataclasses.dataclass(repr=False) -class C(_KindChecker, DataclassPrettyPrintMixin): +class C(_KindChecker, DataclassReprMixin): r"""Local Clifford gate command. Parameters @@ -143,7 +143,7 @@ class C(_KindChecker, DataclassPrettyPrintMixin): @dataclasses.dataclass(repr=False) -class X(_KindChecker, DataclassPrettyPrintMixin): +class X(_KindChecker, DataclassReprMixin): r"""X correction command. Parameters @@ -160,7 +160,7 @@ class X(_KindChecker, DataclassPrettyPrintMixin): @dataclasses.dataclass(repr=False) -class Z(_KindChecker, DataclassPrettyPrintMixin): +class Z(_KindChecker, DataclassReprMixin): r"""Z correction command. Parameters @@ -177,7 +177,7 @@ class Z(_KindChecker, DataclassPrettyPrintMixin): @dataclasses.dataclass(repr=False) -class S(_KindChecker, DataclassPrettyPrintMixin): +class S(_KindChecker, DataclassReprMixin): r"""S command. Parameters diff --git a/graphix/fundamentals.py b/graphix/fundamentals.py index 8967c7174..3f5de0103 100644 --- a/graphix/fundamentals.py +++ b/graphix/fundamentals.py @@ -12,7 +12,7 @@ from graphix.ops import Ops from graphix.parameter import cos_sin -from graphix.pretty_print import EnumPrettyPrintMixin +from graphix.repr_mixins import EnumReprMixin if TYPE_CHECKING: import numpy as np @@ -29,7 +29,7 @@ SupportsComplexCtor = Union[SupportsComplex, SupportsFloat, SupportsIndex, complex] -class Sign(EnumPrettyPrintMixin, Enum): +class Sign(EnumReprMixin, Enum): """Sign, plus or minus.""" PLUS = 1 @@ -112,7 +112,7 @@ def __complex__(self) -> complex: return complex(self.value) -class ComplexUnit(EnumPrettyPrintMixin, Enum): +class ComplexUnit(EnumReprMixin, Enum): """ Complex unit: 1, -1, j, -j. @@ -214,7 +214,7 @@ def matrix(self) -> npt.NDArray[np.complex128]: typing_extensions.assert_never(self) -class Axis(EnumPrettyPrintMixin, Enum): +class Axis(EnumReprMixin, Enum): """Axis: *X*, *Y* or *Z*.""" X = enum.auto() @@ -233,7 +233,7 @@ def matrix(self) -> npt.NDArray[np.complex128]: typing_extensions.assert_never(self) -class Plane(EnumPrettyPrintMixin, Enum): +class Plane(EnumReprMixin, Enum): # TODO: Refactor using match """Plane: *XY*, *YZ* or *XZ*.""" diff --git a/graphix/instruction.py b/graphix/instruction.py index b5dfc6256..382cfa257 100644 --- a/graphix/instruction.py +++ b/graphix/instruction.py @@ -14,7 +14,8 @@ # Ruff suggests to move this import to a type-checking block, but dataclass requires it here from graphix.parameter import ExpressionOrFloat # noqa: TC001 -from graphix.pretty_print import DataclassPrettyPrintMixin, OutputFormat, angle_to_str +from graphix.pretty_print import OutputFormat, angle_to_str +from graphix.repr_mixins import DataclassReprMixin def repr_angle(angle: ExpressionOrFloat) -> str: @@ -65,7 +66,7 @@ def __init_subclass__(cls) -> None: @dataclass(repr=False) -class CCX(_KindChecker, DataclassPrettyPrintMixin): +class CCX(_KindChecker, DataclassReprMixin): """Toffoli circuit instruction.""" target: int @@ -74,7 +75,7 @@ class CCX(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class RZZ(_KindChecker, DataclassPrettyPrintMixin): +class RZZ(_KindChecker, DataclassReprMixin): """RZZ circuit instruction.""" target: int @@ -87,7 +88,7 @@ class RZZ(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class CNOT(_KindChecker, DataclassPrettyPrintMixin): +class CNOT(_KindChecker, DataclassReprMixin): """CNOT circuit instruction.""" target: int @@ -96,7 +97,7 @@ class CNOT(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class SWAP(_KindChecker, DataclassPrettyPrintMixin): +class SWAP(_KindChecker, DataclassReprMixin): """SWAP circuit instruction.""" targets: tuple[int, int] @@ -104,7 +105,7 @@ class SWAP(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class H(_KindChecker, DataclassPrettyPrintMixin): +class H(_KindChecker, DataclassReprMixin): """H circuit instruction.""" target: int @@ -112,7 +113,7 @@ class H(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class S(_KindChecker, DataclassPrettyPrintMixin): +class S(_KindChecker, DataclassReprMixin): """S circuit instruction.""" target: int @@ -120,7 +121,7 @@ class S(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class X(_KindChecker, DataclassPrettyPrintMixin): +class X(_KindChecker, DataclassReprMixin): """X circuit instruction.""" target: int @@ -128,7 +129,7 @@ class X(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class Y(_KindChecker, DataclassPrettyPrintMixin): +class Y(_KindChecker, DataclassReprMixin): """Y circuit instruction.""" target: int @@ -136,7 +137,7 @@ class Y(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class Z(_KindChecker, DataclassPrettyPrintMixin): +class Z(_KindChecker, DataclassReprMixin): """Z circuit instruction.""" target: int @@ -144,7 +145,7 @@ class Z(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class I(_KindChecker, DataclassPrettyPrintMixin): +class I(_KindChecker, DataclassReprMixin): """I circuit instruction.""" target: int @@ -152,7 +153,7 @@ class I(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class M(_KindChecker, DataclassPrettyPrintMixin): +class M(_KindChecker, DataclassReprMixin): """M circuit instruction.""" target: int @@ -162,7 +163,7 @@ class M(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class RX(_KindChecker, DataclassPrettyPrintMixin): +class RX(_KindChecker, DataclassReprMixin): """X rotation circuit instruction.""" target: int @@ -172,7 +173,7 @@ class RX(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class RY(_KindChecker, DataclassPrettyPrintMixin): +class RY(_KindChecker, DataclassReprMixin): """Y rotation circuit instruction.""" target: int @@ -182,7 +183,7 @@ class RY(_KindChecker, DataclassPrettyPrintMixin): @dataclass(repr=False) -class RZ(_KindChecker, DataclassPrettyPrintMixin): +class RZ(_KindChecker, DataclassReprMixin): """Z rotation circuit instruction.""" target: int diff --git a/graphix/pretty_print.py b/graphix/pretty_print.py index b28fe6038..2892ce174 100644 --- a/graphix/pretty_print.py +++ b/graphix/pretty_print.py @@ -2,23 +2,19 @@ from __future__ import annotations -import dataclasses import enum import math import string -from dataclasses import MISSING from enum import Enum from fractions import Fraction from typing import TYPE_CHECKING, SupportsFloat from graphix import command +from graphix.fundamentals import Plane if TYPE_CHECKING: from collections.abc import Container - # these live only in the stub package, not at runtime - from _typeshed import DataclassInstance - from graphix.command import Node from graphix.pattern import Pattern @@ -106,9 +102,6 @@ def command_to_str(cmd: command.Command, output: OutputFormat) -> str: output: OutputFormat The expected format. """ - # Circumvent circular import - from graphix.fundamentals import Plane - out = [cmd.kind.name] if cmd.kind == command.CommandKind.E: @@ -223,64 +216,3 @@ def pattern_to_str( if truncated: return f"{result}...({len(command_list) - limit + 1} more commands)" return result - - -class DataclassPrettyPrintMixin: - """ - Mixin for a concise, eval-friendly `repr` of dataclasses. - - Compared to the default dataclass `repr`: - - Class variables are omitted (dataclasses.fields only returns actual fields). - - Fields whose values equal their defaults are omitted. - - Field names are only shown when preceding fields have been omitted, ensuring positional listings when possible. - - Use with `@dataclass(repr=False)` on the target class. - """ - - def __repr__(self: DataclassInstance) -> str: - """Return a representation string for a dataclass.""" - cls_name = type(self).__name__ - arguments = [] - saw_omitted = False - for field in dataclasses.fields(self): - value = getattr(self, field.name) - if field.default is not MISSING or field.default_factory is not MISSING: - default = field.default_factory() if field.default_factory is not MISSING else field.default - if value == default: - saw_omitted = True - continue - custom_repr = field.metadata.get("repr") - value_str = custom_repr(value) if custom_repr else repr(value) - if saw_omitted: - arguments.append(f"{field.name}={value_str}") - else: - arguments.append(value_str) - arguments_str = ", ".join(arguments) - return f"{cls_name}({arguments_str})" - - -class EnumPrettyPrintMixin: - """ - Mixin to provide a concise, eval-friendly repr for Enum members. - - Compared to the default ``, this mixin's `__repr__` - returns `ClassName.MEMBER_NAME`, which can be evaluated in Python (assuming the - enum class is in scope) to retrieve the same member. - """ - - def __repr__(self) -> str: - """ - Return a representation string of an Enum member. - - Returns - ------- - str - A string in the form `ClassName.MEMBER_NAME`. - """ - # Equivalently (as of Python 3.12), `str(value)` also produces - # "ClassName.MEMBER_NAME", but we build it explicitly here for - # clarity. - if not isinstance(self, Enum): - msg = "EnumMixin can only be used with Enum classes." - raise TypeError(msg) - return f"{self.__class__.__name__}.{self.name}" diff --git a/graphix/pyzx.py b/graphix/pyzx.py index 138282581..e3d355483 100644 --- a/graphix/pyzx.py +++ b/graphix/pyzx.py @@ -45,12 +45,6 @@ def to_pyzx_graph(og: OpenGraph) -> BaseGraph[int, tuple[int, int]]: >>> og = OpenGraph(g, measurements, inputs, outputs) >>> reconstructed_pyzx_graph = to_pyzx_graph(og) """ - # check pyzx availability and version - try: - import pyzx as zx - except ModuleNotFoundError as e: - msg = "Cannot find pyzx (optional dependency)." - raise RuntimeError(msg) from e if zx.__version__ != "0.9.0": warnings.warn( "`to_pyzx_graph` is guaranteed to work only with pyzx==0.9.0 due to possible breaking changes in `pyzx`.", diff --git a/graphix/random_objects.py b/graphix/random_objects.py index f4c8f50c6..d47e96f2e 100644 --- a/graphix/random_objects.py +++ b/graphix/random_objects.py @@ -13,6 +13,7 @@ from graphix.channels import KrausChannel, KrausData from graphix.ops import Ops from graphix.rng import ensure_rng +from graphix.sim.density_matrix import DensityMatrix from graphix.transpiler import Circuit if TYPE_CHECKING: @@ -21,7 +22,6 @@ from numpy.random import Generator from graphix.parameter import Parameter - from graphix.sim.density_matrix import DensityMatrix def rand_herm(sz: int, rng: Generator | None = None) -> npt.NDArray: @@ -80,8 +80,6 @@ def rand_dm( dm = rand_u @ dm @ rand_u.transpose().conj() if dm_dtype: - from graphix.sim.density_matrix import DensityMatrix # circumvent circular import - # will raise an error if incorrect dimension return DensityMatrix(data=dm) return dm diff --git a/graphix/repr_mixins.py b/graphix/repr_mixins.py new file mode 100644 index 000000000..70907e904 --- /dev/null +++ b/graphix/repr_mixins.py @@ -0,0 +1,73 @@ +"""Mixins for eval-friendly `repr` for dataclasses and Enum members.""" + +from __future__ import annotations + +import dataclasses +from dataclasses import MISSING +from enum import Enum +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # these live only in the stub package, not at runtime + from _typeshed import DataclassInstance + + +class DataclassReprMixin: + """ + Mixin for a concise, eval-friendly `repr` of dataclasses. + + Compared to the default dataclass `repr`: + - Class variables are omitted (dataclasses.fields only returns actual fields). + - Fields whose values equal their defaults are omitted. + - Field names are only shown when preceding fields have been omitted, ensuring positional listings when possible. + + Use with `@dataclass(repr=False)` on the target class. + """ + + def __repr__(self: DataclassInstance) -> str: + """Return a representation string for a dataclass.""" + cls_name = type(self).__name__ + arguments = [] + saw_omitted = False + for field in dataclasses.fields(self): + value = getattr(self, field.name) + if field.default is not MISSING or field.default_factory is not MISSING: + default = field.default_factory() if field.default_factory is not MISSING else field.default + if value == default: + saw_omitted = True + continue + custom_repr = field.metadata.get("repr") + value_str = custom_repr(value) if custom_repr else repr(value) + if saw_omitted: + arguments.append(f"{field.name}={value_str}") + else: + arguments.append(value_str) + arguments_str = ", ".join(arguments) + return f"{cls_name}({arguments_str})" + + +class EnumReprMixin: + """ + Mixin to provide a concise, eval-friendly repr for Enum members. + + Compared to the default ``, this mixin's `__repr__` + returns `ClassName.MEMBER_NAME`, which can be evaluated in Python (assuming the + enum class is in scope) to retrieve the same member. + """ + + def __repr__(self) -> str: + """ + Return a representation string of an Enum member. + + Returns + ------- + str + A string in the form `ClassName.MEMBER_NAME`. + """ + # Equivalently (as of Python 3.12), `str(value)` also produces + # "ClassName.MEMBER_NAME", but we build it explicitly here for + # clarity. + if not isinstance(self, Enum): + msg = "EnumMixin can only be used with Enum classes." + raise TypeError(msg) + return f"{self.__class__.__name__}.{self.name}" diff --git a/graphix/utils.py b/graphix/utils.py index e6817b17b..326ce421b 100644 --- a/graphix/utils.py +++ b/graphix/utils.py @@ -2,6 +2,7 @@ from __future__ import annotations +import inspect import sys import typing from typing import TYPE_CHECKING, Any, ClassVar, Literal, SupportsInt, TypeVar @@ -31,8 +32,6 @@ def check_kind(cls: type, scope: dict[str, Any]) -> None: # MEMO: `inspect.get_annotations` unavailable return - import inspect - ann = inspect.get_annotations(cls, eval_str=True, locals=scope).get("kind") if ann is None: msg = "kind must be annotated." diff --git a/pyproject.toml b/pyproject.toml index 1716822e9..a0832f015 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ extend-ignore = [ "DOC", # Docstring "E501", # Line too long "EM10", # Raise string - "PLC0415", # Import not at the top level "PLR1702", # Too many nests "PLW1641", # __hash__ missing "PT011", # pytest raises too broad diff --git a/tests/test_pyzx.py b/tests/test_pyzx.py index ea3ca6275..cd0d0486d 100644 --- a/tests/test_pyzx.py +++ b/tests/test_pyzx.py @@ -1,6 +1,5 @@ from __future__ import annotations -import importlib.util # Use fully-qualified import to avoid name conflict (util) import random from copy import deepcopy from typing import TYPE_CHECKING @@ -13,22 +12,29 @@ from graphix.random_objects import rand_circuit from graphix.transpiler import Circuit -if TYPE_CHECKING: - from pyzx.graph.base import BaseGraph +try: + import pyzx as zx + from pyzx.generate import cliffordT as clifford_t # noqa: N813 -SEED = 123 + from graphix.pyzx import from_pyzx_graph, to_pyzx_graph +except ImportError: + pytestmark = pytest.mark.skip(reason="pyzx not installed") + if TYPE_CHECKING: + import sys -def _pyzx_notfound() -> bool: - return importlib.util.find_spec("pyzx") is None + # We skip type-checking the case where there is no pyzx, since + # pyright cannot figure out that tests are skipped in this + # case. + sys.exit(1) -@pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") -def test_graph_equality() -> None: - from pyzx.generate import cliffordT as clifford_t # noqa: N813 +if TYPE_CHECKING: + from pyzx.graph.base import BaseGraph +SEED = 123 - from graphix.pyzx import from_pyzx_graph +def test_graph_equality() -> None: random.seed(SEED) g = clifford_t(4, 10, 0.1) @@ -42,10 +48,6 @@ def test_graph_equality() -> None: def assert_reconstructed_pyzx_graph_equal(g: BaseGraph[int, tuple[int, int]]) -> None: """Convert a graph to and from an Open graph and then checks the resulting pyzx graph is equal to the original.""" - import pyzx as zx - - from graphix.pyzx import from_pyzx_graph, to_pyzx_graph - zx.simplify.to_graph_like(g) g_copy = deepcopy(g) @@ -66,20 +68,14 @@ def assert_reconstructed_pyzx_graph_equal(g: BaseGraph[int, tuple[int, int]]) -> # Tests that compiling from a pyzx graph to an OpenGraph returns the same # graph. Only works with small circuits up to 4 qubits since PyZX's `tensorfy` # function seems to consume huge amount of memory for larger qubit -@pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") def test_random_clifford_t() -> None: - from pyzx.generate import cliffordT as clifford_t # noqa: N813 - for _ in range(15): g = clifford_t(4, 10, 0.1) assert_reconstructed_pyzx_graph_equal(g) -@pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") @pytest.mark.parametrize("jumps", range(1, 11)) def test_random_circuit(fx_bg: PCG64, jumps: int) -> None: - from graphix.pyzx import from_pyzx_graph, to_pyzx_graph - rng = Generator(fx_bg.jumped(jumps)) nqubits = 5 depth = 5 @@ -98,12 +94,7 @@ def test_random_circuit(fx_bg: PCG64, jumps: int) -> None: assert np.abs(np.dot(state.flatten().conjugate(), state2.flatten())) == pytest.approx(1) -@pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") def test_rz() -> None: - import pyzx as zx - - from graphix.pyzx import from_pyzx_graph - circuit = Circuit(2) circuit.rz(0, np.pi / 4) pattern = circuit.transpile().pattern @@ -117,12 +108,7 @@ def test_rz() -> None: # Issue #235 -@pytest.mark.skipif(_pyzx_notfound(), reason="pyzx not installed") def test_full_reduce_toffoli() -> None: - import pyzx as zx - - from graphix.pyzx import from_pyzx_graph, to_pyzx_graph - c = Circuit(3) c.ccx(0, 1, 2) p = c.transpile().pattern