Skip to content

Commit f52d691

Browse files
authored
ci: enforce ruff in CI, reformat to ruff 0.15.0, prune dead lint deps (#1688)
- code-style job now runs 'ruff check' and 'ruff format --check' (no --fix / no in-place format), so violations fail CI instead of being auto-fixed in the runner and discarded. Pin the ruff action to 0.15.0 to match pyproject and pre-commit. - Reformat the files that had drifted under the old discard-the-fix flow. - Drop unused lint deps (flake8, flake8-docstrings, isort, pep8, myst-parser). - Sync the pre-commit ruff-pre-commit pin to v0.15.0. - Enable uv caching in the build/test workflow. Closes #1671
1 parent 72e58e3 commit f52d691

36 files changed

Lines changed: 145 additions & 532 deletions

.github/workflows/build-test-actions.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ jobs:
1919
runs-on: ubuntu-latest
2020
steps:
2121
- uses: actions/checkout@v7
22+
# Pin ruff to the version in pyproject so CI formatting matches local/pre-commit.
2223
- uses: astral-sh/ruff-action@v3
23-
- run: ruff check --fix
24-
- run: ruff format
24+
with:
25+
version: "0.15.0"
26+
# Check only (no --fix / no in-place format): style and format violations must fail CI
27+
# rather than being silently auto-fixed in the runner and discarded.
28+
- run: ruff check
29+
- run: ruff format --check
2530
- name: Install uv
2631
uses: astral-sh/setup-uv@v7
32+
with:
33+
enable-cache: true
2734
# Run the ty type checker for visibility. Non-blocking for now: there is a large backlog of existing
2835
# diagnostics, so failing CI on them would require a separate type-annotation cleanup.
2936
- name: Type-check with ty (non-blocking)
@@ -57,6 +64,8 @@ jobs:
5764
brew install openblas lapack suite-sparse
5865
- name: Install uv
5966
uses: astral-sh/setup-uv@v7
67+
with:
68+
enable-cache: true
6069
- name: Install toqito + dependencies
6170
run: |
6271
uv sync --group dev --no-group docs --no-group lint

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
33
# Ruff version (keep in sync with the version in pyproject.toml)
4-
rev: v0.11.0
4+
rev: v0.15.0
55
hooks:
66
# Run the linter.
77
- id: ruff

pyproject.toml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@ dev = [
5757
"pre-commit>=3.0.0",
5858
]
5959
lint = [
60-
"flake8==7.3.0",
61-
"flake8-docstrings==1.7.0",
62-
"isort==8.0.1",
63-
"myst-parser==5.1.0",
64-
"pep8==1.7.1",
6560
"ruff==0.15.0",
6661
"ty==0.0.12",
6762
]

toqito/channel_metrics/channel_exclusion.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,7 @@ def _min_error_primal(
166166
problem = pc.Problem()
167167

168168
strategy_ops = [
169-
pc.HermitianVariable(f"W[{idx}]", (dim_in * dim_out, dim_in * dim_out))
170-
for idx in range(n_channels)
169+
pc.HermitianVariable(f"W[{idx}]", (dim_in * dim_out, dim_in * dim_out)) for idx in range(n_channels)
171170
]
172171
x_var = pc.HermitianVariable("X", (dim_in, dim_in))
173172

toqito/channel_metrics/channel_relative_entropy.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,9 @@ def channel_relative_entropy(
113113
if hamiltonian.shape != (in_dim, in_dim):
114114
raise ValueError("The Hamiltonian must have shape (in_dim, in_dim).")
115115
if not is_quantum_channel(channel_1):
116-
raise ValueError(
117-
"Channel relative entropy is only defined if channel_1 is a quantum channel."
118-
)
116+
raise ValueError("Channel relative entropy is only defined if channel_1 is a quantum channel.")
119117
if not is_completely_positive(channel_2):
120-
raise ValueError(
121-
"Channel relative entropy is only defined if channel_2 is completely positive."
122-
)
118+
raise ValueError("Channel relative entropy is only defined if channel_2 is completely positive.")
123119
if np.allclose(channel_1, channel_2):
124120
if mean:
125121
return 0.0
@@ -156,12 +152,7 @@ def channel_relative_entropy(
156152
cvx.Maximize(
157153
cvx.real(
158154
cvx.trace(cvx.kron(rho_a, eye_out) @ (choi_1 - choi_2))
159-
+ cvx.sum(
160-
[
161-
cvx.trace(qs[k] @ (alpha[k] * choi_1 + beta[k] * choi_2))
162-
for k in range(r - 1)
163-
]
164-
)
155+
+ cvx.sum([cvx.trace(qs[k] @ (alpha[k] * choi_1 + beta[k] * choi_2)) for k in range(r - 1)])
165156
)
166157
),
167158
cons,
@@ -180,10 +171,7 @@ def channel_relative_entropy(
180171
upper_cons = (
181172
[y_var >= 0]
182173
+ [ns[0] - choi_1 + choi_2 >> 0]
183-
+ [
184-
ns[k] - gamma[k - 1] * choi_1 - delta[k - 1] * choi_2 >> 0
185-
for k in range(1, r + 1)
186-
]
174+
+ [ns[k] - gamma[k - 1] * choi_1 - delta[k - 1] * choi_2 >> 0 for k in range(1, r + 1)]
187175
+ [ns[k] >> 0 for k in range(1, r + 1)]
188176
+ [
189177
x_var * np.eye(in_dim)

toqito/channel_metrics/tests/test_channel_relative_entropy.py

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
from toqito.cones.integral_relative_entropy import _sandwich_parameters
1313
from toqito.perms import swap_operator
1414

15-
_CHANNEL_RELATIVE_ENTROPY_MOD = importlib.import_module(
16-
"toqito.channel_metrics.channel_relative_entropy"
17-
)
15+
_CHANNEL_RELATIVE_ENTROPY_MOD = importlib.import_module("toqito.channel_metrics.channel_relative_entropy")
1816

1917

2018
def _dense(mat):
@@ -91,9 +89,7 @@ def test_identical_channels_zero():
9189
"""Identical channels should give zero in both bounds and mean modes."""
9290
choi = depolarizing(2, 1)
9391

94-
lower, upper = channel_relative_entropy(
95-
choi, choi, in_dim=2, epsilon_dec=0.2, mean=False
96-
)
92+
lower, upper = channel_relative_entropy(choi, choi, in_dim=2, epsilon_dec=0.2, mean=False)
9793
avg = channel_relative_entropy(choi, choi, in_dim=2, epsilon_dec=0.2, mean=True)
9894

9995
assert lower == 0
@@ -252,9 +248,7 @@ def solve(self, **kwargs):
252248
monkeypatch.setattr(_CHANNEL_RELATIVE_ENTROPY_MOD.cvx, "Problem", FakeProblem)
253249

254250
with pytest.raises(RuntimeError, match="Lower-bound SDP failed"):
255-
channel_relative_entropy(
256-
depolarizing(2, 0.2), depolarizing(2, 0.4), in_dim=2, epsilon_dec=0.2
257-
)
251+
channel_relative_entropy(depolarizing(2, 0.2), depolarizing(2, 0.4), in_dim=2, epsilon_dec=0.2)
258252

259253

260254
def test_raises_when_upper_sdp_fails(monkeypatch):
@@ -280,9 +274,7 @@ def solve(self, **kwargs):
280274
monkeypatch.setattr(_CHANNEL_RELATIVE_ENTROPY_MOD.cvx, "Problem", FakeProblem)
281275

282276
with pytest.raises(RuntimeError, match="Upper-bound SDP failed"):
283-
channel_relative_entropy(
284-
depolarizing(2, 0.2), depolarizing(2, 0.4), in_dim=2, epsilon_dec=0.2
285-
)
277+
channel_relative_entropy(depolarizing(2, 0.2), depolarizing(2, 0.4), in_dim=2, epsilon_dec=0.2)
286278

287279

288280
def test_warns_on_optimal_inaccurate_lower(monkeypatch):
@@ -299,9 +291,7 @@ class FakeProblem:
299291
def __init__(self, objective, constraints):
300292
self.value = 1.0
301293
FakeProblem.created += 1
302-
self.status = (
303-
cvx.OPTIMAL_INACCURATE if FakeProblem.created == 1 else cvx.OPTIMAL
304-
)
294+
self.status = cvx.OPTIMAL_INACCURATE if FakeProblem.created == 1 else cvx.OPTIMAL
305295

306296
def solve(self, **kwargs):
307297
pass
@@ -310,9 +300,7 @@ def solve(self, **kwargs):
310300
monkeypatch.setattr(_CHANNEL_RELATIVE_ENTROPY_MOD.cvx, "Problem", FakeProblem)
311301

312302
with pytest.warns(UserWarning, match="Lower-bound SDP returned OPTIMAL_INACCURATE"):
313-
channel_relative_entropy(
314-
depolarizing(2, 0.2), depolarizing(2, 0.4), in_dim=2, epsilon_dec=0.2
315-
)
303+
channel_relative_entropy(depolarizing(2, 0.2), depolarizing(2, 0.4), in_dim=2, epsilon_dec=0.2)
316304

317305

318306
def test_warns_on_optimal_inaccurate_upper(monkeypatch):
@@ -329,9 +317,7 @@ class FakeProblem:
329317
def __init__(self, objective, constraints):
330318
self.value = 1.0
331319
FakeProblem.created += 1
332-
self.status = (
333-
cvx.OPTIMAL if FakeProblem.created == 1 else cvx.OPTIMAL_INACCURATE
334-
)
320+
self.status = cvx.OPTIMAL if FakeProblem.created == 1 else cvx.OPTIMAL_INACCURATE
335321

336322
def solve(self, **kwargs):
337323
pass
@@ -340,9 +326,7 @@ def solve(self, **kwargs):
340326
monkeypatch.setattr(_CHANNEL_RELATIVE_ENTROPY_MOD.cvx, "Problem", FakeProblem)
341327

342328
with pytest.warns(UserWarning, match="Upper-bound SDP returned OPTIMAL_INACCURATE"):
343-
channel_relative_entropy(
344-
depolarizing(2, 0.2), depolarizing(2, 0.4), in_dim=2, epsilon_dec=0.2
345-
)
329+
channel_relative_entropy(depolarizing(2, 0.2), depolarizing(2, 0.4), in_dim=2, epsilon_dec=0.2)
346330

347331

348332
@pytest.mark.slow
@@ -363,11 +347,7 @@ def test_channel_relative_entropy_paper_example(param_p: float, expected_mean: f
363347
# N_deph(rho) = 0.4 rho + 0.6 sigma_z rho sigma_z
364348
# M_dep(rho) = (1 - 3p/4) rho + p/4 (X rho X + Y rho Y + Z rho Z)
365349
channel_1 = _dense(pauli_channel(np.array([0.4, 0.0, 0.0, 0.6])))
366-
channel_2 = _dense(
367-
pauli_channel(
368-
np.array([1 - 3 * param_p / 4, param_p / 4, param_p / 4, param_p / 4])
369-
)
370-
)
350+
channel_2 = _dense(pauli_channel(np.array([1 - 3 * param_p / 4, param_p / 4, param_p / 4, param_p / 4])))
371351

372352
lower, upper = channel_relative_entropy(channel_1, channel_2, in_dim=2, mean=False)
373353
avg = (lower + upper) / 2

toqito/channels/bitflip.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ def bitflip(
7777
raise ValueError("Input matrix must be 2x2 for the bitflip channel.")
7878

7979
warnings.warn(
80-
"Passing `input_mat` to `bitflip` is deprecated; "
81-
"use `apply_channel(input_mat, bitflip(...))` instead.",
80+
"Passing `input_mat` to `bitflip` is deprecated; use `apply_channel(input_mat, bitflip(...))` instead.",
8281
DeprecationWarning,
8382
stacklevel=2,
8483
)

toqito/cones/integral_relative_entropy.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,11 @@ def _sandwich_parameters(rho: np.ndarray, sigma: np.ndarray) -> tuple[float, flo
2323
w_xy = _generalized_eigenvalues(rho, sigma)
2424
w_yx = _generalized_eigenvalues(sigma, rho)
2525
except LinAlgError as exc:
26-
raise ValueError(
27-
"Failed to compute sandwich parameters from generalized eigenvalues."
28-
) from exc
26+
raise ValueError("Failed to compute sandwich parameters from generalized eigenvalues.") from exc
2927
finite_xy = w_xy[np.isfinite(w_xy)]
3028
finite_yx = w_yx[np.isfinite(w_yx)]
3129
if finite_xy.size == 0 or finite_yx.size == 0:
32-
raise ValueError(
33-
"Failed to compute sandwich parameters from generalized eigenvalues."
34-
)
30+
raise ValueError("Failed to compute sandwich parameters from generalized eigenvalues.")
3531
lam = float(np.max(finite_xy))
3632
mu = float(np.min(finite_yx))
3733
return mu, lam

toqito/cones/lieb_ando.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# Adapted from CVXQUAD (https://github.com/hfawzi/cvxquad), BSD-2-Clause.
44
# Original implementation by Fawzi, Saunderson, et al.
55

6-
76
import cvxpy
87
import numpy as np
98
from scipy.linalg import fractional_matrix_power
@@ -72,9 +71,7 @@ def lieb_ando(
7271
_require_square_2d(mat_b, "mat_b")
7372
_require_2d(mat_k, "mat_k")
7473
if mat_k.shape[0] != mat_a.shape[0] or mat_k.shape[1] != mat_b.shape[1]:
75-
raise ValueError(
76-
"mat_k must have the same number of rows as mat_a and the same number of columns as mat_b."
77-
)
74+
raise ValueError("mat_k must have the same number of rows as mat_a and the same number of columns as mat_b.")
7875

7976
if isinstance(mat_a, np.ndarray) and isinstance(mat_b, np.ndarray):
8077
if not is_positive_semidefinite(mat_a) or not is_positive_semidefinite(mat_b):
@@ -83,36 +80,26 @@ def lieb_ando(
8380
b_raised = fractional_matrix_power(mat_b, t)
8481
return float(np.real(np.trace(mat_k.conj().T @ a_raised @ mat_k @ b_raised)))
8582
elif isinstance(mat_a, np.ndarray):
86-
if not is_positive_semidefinite(mat_a) or not is_positive_semidefinite(
87-
mat_b.value
88-
):
83+
if not is_positive_semidefinite(mat_a) or not is_positive_semidefinite(mat_b.value):
8984
raise ValueError("mat_a and mat_b must be positive semidefinite.")
9085
mat_kak = mat_k.conj().T @ fractional_matrix_power(mat_a, 1 - t) @ mat_k
9186
mat_kak = (mat_kak + mat_kak.conj().T) / 2
9287
return trace_matrix_power(mat_b, t, mat_kak)
9388
elif isinstance(mat_b, np.ndarray):
94-
if not is_positive_semidefinite(mat_a.value) or not is_positive_semidefinite(
95-
mat_b
96-
):
89+
if not is_positive_semidefinite(mat_a.value) or not is_positive_semidefinite(mat_b):
9790
raise ValueError("mat_a and mat_b must be positive semidefinite.")
9891
mat_kkb = mat_k @ fractional_matrix_power(mat_b, t) @ mat_k.conj().T
9992
mat_kkb = (mat_kkb + mat_kkb.conj().T) / 2
10093
return trace_matrix_power(mat_a, 1 - t, mat_kkb)
10194
else:
10295
if not mat_a.is_affine() or not mat_b.is_affine():
10396
raise ValueError("mat_a and mat_b must be affine expressions.")
104-
if not is_positive_semidefinite(mat_a.value) or not is_positive_semidefinite(
105-
mat_b.value
106-
):
97+
if not is_positive_semidefinite(mat_a.value) or not is_positive_semidefinite(mat_b.value):
10798
raise ValueError("mat_a and mat_b must be positive semidefinite.")
10899

109100
n = mat_a.shape[0]
110101
m = mat_b.shape[0]
111-
is_cplx = (
112-
np.any(np.imag(mat_a.value) != 0)
113-
or np.any(np.imag(mat_b.value) != 0)
114-
or np.any(np.imag(mat_k) != 0)
115-
)
102+
is_cplx = np.any(np.imag(mat_a.value) != 0) or np.any(np.imag(mat_b.value) != 0) or np.any(np.imag(mat_k) != 0)
116103
Kvec = np.reshape(mat_k.T, (n * m, 1), order="F")
117104
KvKv = Kvec @ Kvec.conj().T
118105
KvKv = (KvKv + KvKv.conj().T) / 2
@@ -127,18 +114,14 @@ def lieb_ando(
127114
mat_a_kron = cvxpy.kron(mat_a, np.eye(m))
128115
mat_b_kron = cvxpy.kron(np.eye(n), cvxpy.conj(mat_b))
129116
if t >= 0 and t <= 1:
130-
cons = geometric_mean_hypo_cone(
131-
mat_a_kron, mat_b_kron, T, t, fullhyp=False, hermitian=is_cplx
132-
)
117+
cons = geometric_mean_hypo_cone(mat_a_kron, mat_b_kron, T, t, fullhyp=False, hermitian=is_cplx)
133118
problem = cvxpy.Problem(cvxpy.Maximize(obj), cons)
134119
result = problem.solve()
135120
if problem.status not in (cvxpy.OPTIMAL, cvxpy.OPTIMAL_INACCURATE):
136121
raise ValueError(f"The SDP did not solve successfully (status: {problem.status}).")
137122
return result
138123
elif (t >= -1 and t <= 0) or (t >= 1 and t <= 2):
139-
cons = geometric_mean_epi_cone(
140-
mat_a_kron, mat_b_kron, T, t, hermitian=is_cplx
141-
)
124+
cons = geometric_mean_epi_cone(mat_a_kron, mat_b_kron, T, t, hermitian=is_cplx)
142125
problem = cvxpy.Problem(cvxpy.Minimize(obj), cons)
143126
result = problem.solve()
144127
if problem.status not in (cvxpy.OPTIMAL, cvxpy.OPTIMAL_INACCURATE):

toqito/cones/ln_quantum_entropy.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
from toqito.matrix_props import is_positive_semidefinite
1515

1616

17-
def ln_quantum_entropy(
18-
mat_x: np.ndarray | cvxpy.Expression, m: int = 3, k: int = 3, apx: int = 0
19-
) -> float:
17+
def ln_quantum_entropy(mat_x: np.ndarray | cvxpy.Expression, m: int = 3, k: int = 3, apx: int = 0) -> float:
2018
r"""Compute the quantum entropy \(-\operatorname{tr}(X \log X)\) for PSD \(X\).
2119
2220
Note that this function uses the natural logarithm (base e) and not the base-2 logarithm.
@@ -68,9 +66,7 @@ def ln_quantum_entropy(
6866
if not mat_x.is_affine():
6967
raise ValueError("mat_x must be an affine CVXPY expression.")
7068
if mat_x.value is None:
71-
raise ValueError(
72-
"Affine mat_x has no numeric initial value; set `.value` for PSD checks."
73-
)
69+
raise ValueError("Affine mat_x has no numeric initial value; set `.value` for PSD checks.")
7470
if not is_positive_semidefinite(mat_x.value):
7571
raise ValueError("mat_x must be positive semidefinite at the initial value.")
7672

0 commit comments

Comments
 (0)