diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 62fba68c..a9a47a5c 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] - python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python: ["3.10", "3.11", "3.12", "3.13", "3.14"] name: "Python ${{ matrix.python }} / ${{ matrix.os }}" runs-on: ${{ matrix.os }} diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index ae72354a..d01697f5 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: 3.9 + python-version: "3.10" - run: | python3 -m pip install -U pip diff --git a/CHANGELOG.md b/CHANGELOG.md index e252aa67..864b986d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Type Hints**: Added `py.typed` marker for PEP 561 compliance, enabling type checkers (mypy, pyright) to recognize the package as typed when installed from PyPI. +### Changed + +- **Python Support**: Dropped Python 3.9 support, added Python 3.14 support. Now requires Python >=3.10, <3.15. + ## [0.2.0] - 2025-12-26 ### Added diff --git a/graphqomb/command.py b/graphqomb/command.py index 56ae7c90..f3603d14 100644 --- a/graphqomb/command.py +++ b/graphqomb/command.py @@ -14,8 +14,7 @@ from __future__ import annotations import dataclasses -import sys -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING if TYPE_CHECKING: from graphqomb.common import MeasBasis @@ -117,7 +116,4 @@ def __str__(self) -> str: return "TICK" -if sys.version_info >= (3, 10): - Command = N | E | M | X | Z | TICK -else: - Command = Union[N, E, M, X, Z, TICK] +Command = N | E | M | X | Z | TICK diff --git a/graphqomb/feedforward.py b/graphqomb/feedforward.py index ce4ccd65..e1f558ef 100644 --- a/graphqomb/feedforward.py +++ b/graphqomb/feedforward.py @@ -11,22 +11,16 @@ from __future__ import annotations -import sys from collections.abc import Iterable, Mapping from collections.abc import Set as AbstractSet from graphlib import TopologicalSorter -from typing import Any +from typing import Any, TypeGuard import typing_extensions from graphqomb.common import Axis, Plane, determine_pauli_axis from graphqomb.graphstate import BaseGraphState, odd_neighbors -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - from typing_extensions import TypeGuard - def _is_flow(flowlike: Mapping[int, Any]) -> TypeGuard[Mapping[int, int]]: r"""Check if the flowlike object is a flow. diff --git a/graphqomb/gates.py b/graphqomb/gates.py index db72c85c..9dc6b12a 100644 --- a/graphqomb/gates.py +++ b/graphqomb/gates.py @@ -31,10 +31,9 @@ from __future__ import annotations import math -import sys from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeAlias import numpy as np @@ -224,7 +223,7 @@ def matrix(self) -> NDArray[np.complex128]: def count_ones_in_binary(array: NDArray[np.int64]) -> NDArray[np.int64]: def count_ones_single(x: np.int64) -> int: - return bin(int(x)).count("1") + return int(x).bit_count() count_ones = np.vectorize(count_ones_single) return np.asarray(count_ones(array), dtype=np.int64) @@ -234,18 +233,8 @@ def count_ones_single(x: np.int64) -> int: return np.diag(np.exp(-1j * self.angle / 2 * z_sign)) -if sys.version_info >= (3, 10): - from typing import TypeAlias - - UnitGate: TypeAlias = J | CZ | PhaseGadget - """Unit gate type""" -else: - from typing import Union - - from typing_extensions import TypeAlias - - UnitGate: TypeAlias = Union[J, CZ, PhaseGadget] - """Unit gate type""" +UnitGate: TypeAlias = J | CZ | PhaseGadget +"""Unit gate type""" @dataclass(frozen=True) diff --git a/graphqomb/pattern.py b/graphqomb/pattern.py index cf98f766..f0a8d531 100644 --- a/graphqomb/pattern.py +++ b/graphqomb/pattern.py @@ -18,8 +18,7 @@ from graphqomb.command import TICK, Command, E, M, N, X, Z if TYPE_CHECKING: - from collections.abc import Iterator - from typing import Callable + from collections.abc import Callable, Iterator from graphqomb.pauli_frame import PauliFrame diff --git a/graphqomb/schedule_solver.py b/graphqomb/schedule_solver.py index 90cbeb05..ce478c93 100644 --- a/graphqomb/schedule_solver.py +++ b/graphqomb/schedule_solver.py @@ -59,14 +59,14 @@ def _add_constraints( for node, children in dag.items(): for child in children: if node in node2meas and child in node2meas: - model.Add(node2meas[node] < node2meas[child]) + model.add(node2meas[node] < node2meas[child]) # Edge constraints for node in graph.physical_nodes - set(graph.output_node_indices): for neighbor in graph.neighbors(node): if neighbor in graph.input_node_indices: continue - model.Add(node2prep[neighbor] < node2meas[node]) + model.add(node2prep[neighbor] < node2meas[node]) def _set_objective( @@ -107,26 +107,26 @@ def _compute_alive_nodes_at_time( """ alive_at_t: list[cp_model.IntVar] = [] for node in ctx.graph.physical_nodes: - a_pre = ctx.model.NewBoolVar(f"alive_pre_{node}_{t}") + a_pre = ctx.model.new_bool_var(f"alive_pre_{node}_{t}") if node in ctx.graph.input_node_indices: - ctx.model.Add(a_pre == 1) + ctx.model.add(a_pre == 1) else: p = node2prep[node] - ctx.model.Add(p <= t).OnlyEnforceIf(a_pre) - ctx.model.Add(p > t).OnlyEnforceIf(a_pre.Not()) + ctx.model.add(p <= t).only_enforce_if(a_pre) + ctx.model.add(p > t).only_enforce_if(a_pre.negated()) - a_meas = ctx.model.NewBoolVar(f"alive_meas_{node}_{t}") + a_meas = ctx.model.new_bool_var(f"alive_meas_{node}_{t}") if node in ctx.graph.output_node_indices: - ctx.model.Add(a_meas == 0) + ctx.model.add(a_meas == 0) else: q = node2meas[node] - ctx.model.Add(q <= t).OnlyEnforceIf(a_meas) - ctx.model.Add(q > t).OnlyEnforceIf(a_meas.Not()) + ctx.model.add(q <= t).only_enforce_if(a_meas) + ctx.model.add(q > t).only_enforce_if(a_meas.negated()) - alive = ctx.model.NewBoolVar(f"alive_{node}_{t}") - ctx.model.AddImplication(alive, a_pre) - ctx.model.AddImplication(alive, a_meas.Not()) - ctx.model.Add(a_pre - a_meas <= alive) + alive = ctx.model.new_bool_var(f"alive_{node}_{t}") + ctx.model.add_implication(alive, a_pre) + ctx.model.add_implication(alive, a_meas.negated()) + ctx.model.add(a_pre - a_meas <= alive) alive_at_t.append(alive) return alive_at_t @@ -139,11 +139,11 @@ def _set_minimize_space_objective( max_time: int, ) -> None: """Set objective to minimize the maximum number of qubits used at any time.""" - max_space = ctx.model.NewIntVar(0, len(ctx.graph.physical_nodes), "max_space") + max_space = ctx.model.new_int_var(0, len(ctx.graph.physical_nodes), "max_space") for t in range(max_time): alive_at_t = _compute_alive_nodes_at_time(ctx, node2prep, node2meas, t) - ctx.model.Add(max_space >= sum(alive_at_t)) - ctx.model.Minimize(max_space) + ctx.model.add(max_space >= sum(alive_at_t)) + ctx.model.minimize(max_space) def _set_minimize_time_objective( @@ -158,13 +158,13 @@ def _set_minimize_time_objective( if max_qubit_count is not None: for t in range(max_time): alive_at_t = _compute_alive_nodes_at_time(ctx, node2prep, node2meas, t) - ctx.model.Add(sum(alive_at_t) <= max_qubit_count) + ctx.model.add(sum(alive_at_t) <= max_qubit_count) # Time objective: minimize makespan meas_vars = list(node2meas.values()) - makespan = ctx.model.NewIntVar(0, max_time, "makespan") - ctx.model.AddMaxEquality(makespan, meas_vars) - ctx.model.Minimize(makespan) + makespan = ctx.model.new_int_var(0, max_time, "makespan") + ctx.model.add_max_equality(makespan, meas_vars) + ctx.model.minimize(makespan) def solve_schedule( @@ -203,9 +203,9 @@ def solve_schedule( node2meas: dict[int, cp_model.IntVar] = {} for node in graph.physical_nodes: if node not in graph.input_node_indices: - node2prep[node] = model.NewIntVar(0, max_time, f"prep_{node}") + node2prep[node] = model.new_int_var(0, max_time, f"prep_{node}") if node not in graph.output_node_indices: - node2meas[node] = model.NewIntVar(0, max_time, f"meas_{node}") + node2meas[node] = model.new_int_var(0, max_time, f"meas_{node}") # Add constraints _add_constraints(model, graph, dag, node2prep, node2meas) @@ -217,14 +217,11 @@ def solve_schedule( # Solve solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = timeout - status = solver.Solve(model) - - # Note: type: ignore is needed due to a bug in or-tools type annotations - # The actual runtime type of status is cp_model_pb2.ValueType, but it's incorrectly - # annotated as CpSolverStatus, causing mypy to report a false positive comparison-overlap error - if status in {cp_model.OPTIMAL, cp_model.FEASIBLE}: # type: ignore[comparison-overlap] - prepare_time = {node: solver.Value(var) for node, var in node2prep.items()} - measure_time = {node: solver.Value(var) for node, var in node2meas.items()} + status: cp_model.CpSolverStatus = solver.Solve(model) + + if status in {cp_model.OPTIMAL, cp_model.FEASIBLE}: + prepare_time: dict[int, int] = {node: int(solver.Value(var)) for node, var in node2prep.items()} + measure_time: dict[int, int] = {node: int(solver.Value(var)) for node, var in node2meas.items()} return prepare_time, measure_time return None diff --git a/graphqomb/statevec.py b/graphqomb/statevec.py index 60102f10..59c795ba 100644 --- a/graphqomb/statevec.py +++ b/graphqomb/statevec.py @@ -55,7 +55,7 @@ def __init__(self, state: ArrayLike | None = None, *, copy: bool | None = None) # Internal qubit ordering: maps external qubit index to internal index self.__qindex_mng = QubitIndexManager(num_qubits) - def __array__(self, dtype: DTypeLike = None, copy: bool | None = None) -> NDArray[np.complex128]: + def __array__(self, dtype: DTypeLike | None = None, copy: bool | None = None) -> NDArray[np.complex128]: return np.asarray(self.state(), dtype=dtype, copy=copy) @property diff --git a/pyproject.toml b/pyproject.toml index 85cfbb40..1a39933d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "graphqomb" version = "0.2.0" description = "A modular Graph State qompiler for measurement-based quantum computing." readme = "README.md" -requires-python = ">=3.9, <3.14" +requires-python = ">=3.10, <3.15" license = {text = "MIT"} authors = [ {name = "Masato Fukushima", email = "masa1063fuk@gmail.com"}, @@ -32,11 +32,11 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Physics", "Topic :: Scientific/Engineering :: Quantum Computing", "Topic :: Software Development :: Libraries :: Python Modules", @@ -61,7 +61,7 @@ doc = { file = ["docs/requirements.txt"] } graphqomb = ["py.typed"] [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 120 [tool.ruff.lint] diff --git a/tests/test_circuit.py b/tests/test_circuit.py index 87399628..b5252153 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -191,7 +191,7 @@ def test_circuit_instructions_matches_manual_expansion() -> None: assert len(instructions) == len(expected_instructions) # Compare each instruction - for inst, expected in zip(instructions, expected_instructions): + for inst, expected in zip(instructions, expected_instructions, strict=False): assert type(inst) is type(expected) if isinstance(inst, J) and isinstance(expected, J): assert inst.qubit == expected.qubit @@ -225,7 +225,7 @@ def test_circuit_instructions_returns_copy() -> None: assert macro1 is not macro2 # But with same contents assert len(macro1) == len(macro2) - for g1, g2 in zip(macro1, macro2): + for g1, g2 in zip(macro1, macro2, strict=False): assert type(g1) is type(g2) diff --git a/tests/test_euler.py b/tests/test_euler.py index fb361499..d5ced1a9 100644 --- a/tests/test_euler.py +++ b/tests/test_euler.py @@ -18,7 +18,7 @@ from graphqomb.matrix import is_unitary if TYPE_CHECKING: - from typing import Callable + from collections.abc import Callable @pytest.fixture