rigorous-levels 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Roberto Locatelli
3
+ """rigorous_levels: mathematically guaranteed eigenvalue intervals for the charge-basis rotor (transmon, cos mφ:
4
+ guaranteed_intervals), the spherical rotor in a field (spherical_rotor), the 1Σ molecule with hyperfine, Zeeman and
5
+ Stark terms (hyperfine_molecule), the Mathieu characteristic values a_r(q), b_r(q) (mathieu), the fluxonium in the
6
+ oscillator basis (fluxonium), parameters given as intervals (parameter_balls) and matrix elements ⟨v_i|n|v_j⟩,
7
+ ⟨v_i|cos mφ|v_j⟩, ⟨v_i|sin mφ|v_j⟩ between enclosed eigenvectors (matrix_elements); a float spectrum with a declared
8
+ error (rotor_spectrum); the scqubits convergence warning with the declared threshold rule "scq-threshold/1.0" and the
9
+ entry from scqubits objects (scqubits_adapter); a sandboxed multiport that runs external solvers inside one of the
10
+ isolation engines and compares them with the intervals (multiport, port_runner, isolation); the guaranteed per-level
11
+ error of the Duffing model against the transmon (duffing_vs_transmon).
12
+
13
+ The public English API is exposed here (`from rigorous_levels import Rotor, guaranteed_levels, ...`). The Italian module
14
+ names of 0.2.0 are kept as deprecated aliases (DeprecationWarning). This package is not an external
15
+ validator."""
16
+
17
+ __version__ = "0.3.0"
18
+
19
+ from rigorous_levels.guaranteed_intervals import (GUARANTEED, WIDE, ArbMissing, DeadlineExceeded, Intervals, OutOfDomain, # noqa: E402
20
+ guaranteed_levels)
21
+ from rigorous_levels.rotor_spectrum import NOT_CONVERGED, OK, InputRejected, Rotor, Spectrum, spectrum # noqa: E402
22
+ from rigorous_levels.spherical_rotor import SphericalRotor, guaranteed_sphere_levels # noqa: E402
23
+ from rigorous_levels.hyperfine_molecule import SigmaMolecule, guaranteed_molecule_levels, sector_dimension, zeeman_hz # noqa: E402
24
+ from rigorous_levels.mathieu import MathieuValue, mathieu_a, mathieu_b, mathieu_value # noqa: E402
25
+ from rigorous_levels.fluxonium import Fluxonium, guaranteed_fluxonium_levels # noqa: E402
26
+ from rigorous_levels.parameter_balls import RotorBall, guaranteed_levels_over_box # noqa: E402
27
+ from rigorous_levels.matrix_elements import MatrixElement, guaranteed_matrix_element # noqa: E402
28
+ from rigorous_levels.duffing_vs_transmon import DuffingError, duffing_error # noqa: E402
29
+ from rigorous_levels.scqubits_adapter import (OK_FLOAT_FLOOR, UNCHECKED, ScqubitsCheck, ScqubitsMissing, ScqubitsSpectrum, # noqa: E402
30
+ assert_inside, check, guaranteed_from_scqubits, scqubits_spectrum)
31
+ from rigorous_levels.multiport import (ENGINES, PORTS, Output, Request, SandboxMissing, Verdict, compare, run_port, # noqa: E402
32
+ run_ports, site_packages_of)
33
+
34
+ __all__ = ["__version__", "GUARANTEED", "WIDE", "OK", "NOT_CONVERGED", "UNCHECKED", "OK_FLOAT_FLOOR",
35
+ "ArbMissing", "DeadlineExceeded", "OutOfDomain", "InputRejected", "ScqubitsMissing", "SandboxMissing",
36
+ "Intervals", "Rotor", "Spectrum", "SphericalRotor", "SigmaMolecule", "MathieuValue", "Fluxonium", "RotorBall", "MatrixElement",
37
+ "DuffingError", "ScqubitsSpectrum", "ScqubitsCheck", "Request", "Output", "Verdict", "ENGINES", "PORTS",
38
+ "guaranteed_levels", "spectrum", "guaranteed_sphere_levels", "guaranteed_molecule_levels", "sector_dimension",
39
+ "zeeman_hz", "mathieu_a", "mathieu_b", "mathieu_value", "guaranteed_fluxonium_levels",
40
+ "guaranteed_levels_over_box", "guaranteed_matrix_element", "duffing_error", "scqubits_spectrum", "guaranteed_from_scqubits",
41
+ "check", "assert_inside", "run_port", "run_ports", "compare", "site_packages_of"]
@@ -0,0 +1,59 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Roberto Locatelli
3
+ """rigorous_levels._compat — the Italian names of 0.2.0 as DEPRECATED aliases of the English API of 0.3.0.
4
+
5
+ Every use of an old name (module, class, function, keyword argument, field) emits a DeprecationWarning that names the
6
+ new one, so a migration can be measured (`python -W error::DeprecationWarning` fails on any old name). The aliases are
7
+ removed in a later release; nothing new should use them. Modules: `rigorous_levels.intervallo_garantito`,
8
+ `rotore_spettro`, `scqubits_adattatore`, `rotore_sferico_garantito`, `molecola_iperfine_garantita`, `multiporta`.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import functools
13
+ import warnings
14
+ from typing import Dict, Optional
15
+
16
+
17
+ def _warn(old: str, new: str) -> None:
18
+ warnings.warn(f"{old} is deprecated since rigorous-levels 0.3.0: use {new}", DeprecationWarning, stacklevel=3)
19
+
20
+
21
+ def deprecated_module(old: str, new: str) -> None:
22
+ """Called at import time by an old-named module."""
23
+ warnings.warn(f"rigorous_levels.{old} is deprecated since rigorous-levels 0.3.0: use rigorous_levels.{new}", DeprecationWarning,
24
+ stacklevel=3)
25
+
26
+
27
+ def deprecated_function(new, old_name: str, kwmap: Optional[Dict[str, str]] = None):
28
+ """A function that warns, translates the Italian keyword arguments with `kwmap` and calls `new`."""
29
+ kwmap = dict(kwmap or {})
30
+
31
+ @functools.wraps(new)
32
+ def wrapper(*args, **kwargs):
33
+ _warn(old_name, new.__name__)
34
+ return new(*args, **{kwmap.get(k, k): v for k, v in kwargs.items()})
35
+ wrapper.__name__ = old_name
36
+ wrapper.__qualname__ = old_name
37
+ return wrapper
38
+
39
+
40
+ def deprecated_class(new: type, old_name: str, kwmap: Optional[Dict[str, str]] = None) -> type:
41
+ """A subclass of `new` (so isinstance checks on the new class still hold) whose constructor warns and translates
42
+ the Italian keyword arguments."""
43
+ kwmap = dict(kwmap or {})
44
+
45
+ def __init__(self, *args, **kwargs):
46
+ _warn(old_name, new.__name__)
47
+ new.__init__(self, *args, **{kwmap.get(k, k): v for k, v in kwargs.items()})
48
+ return type(old_name, (new,), {"__init__": __init__, "__doc__": f"Deprecated alias of {new.__name__}.",
49
+ "__module__": new.__module__})
50
+
51
+
52
+ def add_field_aliases(cls: type, aliases: Dict[str, str]) -> None:
53
+ """Italian field names as read-only properties that warn and return the English field."""
54
+ for old, new in aliases.items():
55
+ def getter(self, _old=old, _new=new):
56
+ warnings.warn(f"{cls.__name__}.{_old} is deprecated since rigorous-levels 0.3.0: use .{_new}", DeprecationWarning,
57
+ stacklevel=2)
58
+ return getattr(self, _new)
59
+ setattr(cls, old, property(getter))
@@ -0,0 +1,382 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Roberto Locatelli
3
+ """rigorous_levels._deterministic — float estimates of eigenvalues and eigenvectors that are the same on every machine.
4
+
5
+ The guaranteed modules use float estimates only to choose where the exact (Arb) counts are taken, but the ends of a
6
+ published interval are built from those points: an estimate that changes with the CPU or the BLAS/LAPACK kernel
7
+ changes the published interval (measured 2026-11-12: numpy.linalg.eigvalsh gave different last bits under
8
+ OPENBLAS_CORETYPE=Haswell, Prescott and Nehalem, and so did the fluxonium intervals). The functions here use only
9
+ Python float operations whose result IEEE 754 fixes bit for bit (+, −, ×, ÷, comparisons, math.sqrt, math.fsum,
10
+ float() of an int or a Fraction), each in a fixed order: no BLAS/LAPACK, no libm transcendental, no builtin sum() of
11
+ floats (its algorithm changed in Python 3.12). On binary64 hardware with round-to-nearest (every 64-bit platform
12
+ CPython supports) the results depend only on the inputs and this code.
13
+
14
+ - tridiagonal: bisection on the Sturm count (the LDLᵀ pivot recurrence, LAPACK dlaebz convention for tiny pivots);
15
+ - dense symmetric: Householder reduction to tridiagonal, then the same bisection;
16
+ - banded symmetric: bisection on the LDLᵀ inertia of the band (no pivoting: an estimate, the exact counts decide);
17
+ - eigenvectors of a band matrix: inverse iteration with banded LU and partial pivoting, from a fixed start vector.
18
+
19
+ Accuracy is that of a backward-stable method for the tridiagonal and dense paths (a few eps·‖A‖); for the band path
20
+ the LDLᵀ without pivoting is not backward stable in general. None of these numbers enters a proof: they only choose
21
+ points, and every point is then checked by an exact count."""
22
+ from __future__ import annotations
23
+
24
+ import math
25
+ from operator import mul
26
+ from typing import Callable, Dict, List, Optional, Sequence
27
+
28
+ EPS = 2.220446049250313e-16
29
+ SAFMIN = 2.2250738585072014e-308
30
+ MAX_BISECTIONS = 2200 # enough to reach adjacent floats from any finite bracket
31
+
32
+
33
+ def _scale_exponent(values) -> int:
34
+ """e with max|v| / 2^e in [0.5, 1) (0 if all zero); scaling by 2^−e is exact for normal floats and keeps every
35
+ square below the overflow threshold."""
36
+ m = 0.0
37
+ for v in values:
38
+ if not math.isfinite(v): # NaN too: max() would skip it in silence
39
+ raise OverflowError("an entry of the matrix is not finite")
40
+ if abs(v) > m:
41
+ m = abs(v)
42
+ if m == 0.0:
43
+ return 0
44
+ return math.frexp(m)[1]
45
+
46
+
47
+ def _fsum_dot(a: Sequence[float], b: Sequence[float]) -> float:
48
+ return math.fsum(map(mul, a, b))
49
+
50
+
51
+ def _checked(count: Callable[[float], int], check: Optional[Callable[[], None]]) -> Callable[[float], int]:
52
+ """count with the caller's deadline check before each evaluation (the check raises; it changes no number)."""
53
+ if check is None:
54
+ return count
55
+
56
+ def c(x: float) -> int:
57
+ check()
58
+ return count(x)
59
+ return c
60
+
61
+
62
+ def _bisect_lowest(count: Callable[[float], int], k: int, lo: float, hi: float, atol: float,
63
+ first: int = 0) -> List[float]:
64
+ """λ_first … λ_{k−1} from count(x) = #{eigenvalues < x}, given lo ≤ λ_0 and λ_{k−1} < hi. Every count narrows the
65
+ brackets of all the eigenvalues still to compute; each bracket stops at width ≤ atol + 2·eps·max(|a|, |b|) or
66
+ when the midpoint is no longer strictly inside (adjacent floats)."""
67
+ L = [lo] * k
68
+ U = [hi] * k
69
+ out: List[float] = []
70
+ for j in range(first, k):
71
+ a = L[j]
72
+ b = U[j]
73
+ if a > b:
74
+ a = b
75
+ for _ in range(MAX_BISECTIONS):
76
+ if b - a <= atol + 2.0 * EPS * max(abs(a), abs(b)):
77
+ break
78
+ m = a * 0.5 + b * 0.5
79
+ if not a < m < b:
80
+ break
81
+ c = count(m)
82
+ for t in range(j, k):
83
+ if t < c:
84
+ if m < U[t]:
85
+ U[t] = m
86
+ elif m > L[t]:
87
+ L[t] = m
88
+ if c > j:
89
+ b = m
90
+ else:
91
+ a = m
92
+ out.append(a * 0.5 + b * 0.5)
93
+ return out
94
+
95
+
96
+ def sturm_count(d: Sequence[float], e2: Sequence[float], x: float, pivmin: float) -> int:
97
+ """#{eigenvalues < x} of the symmetric tridiagonal with diagonal d and squared off-diagonal e2 (float)."""
98
+ q = d[0] - x
99
+ if abs(q) <= pivmin:
100
+ q = -pivmin
101
+ c = 1 if q < 0 else 0
102
+ for i in range(1, len(d)):
103
+ q = d[i] - x - e2[i - 1] / q
104
+ if abs(q) <= pivmin:
105
+ q = -pivmin
106
+ if q < 0:
107
+ c += 1
108
+ return c
109
+
110
+
111
+ def _pivmin(e2: Sequence[float]) -> float:
112
+ m = 1.0
113
+ for v in e2:
114
+ if v > m:
115
+ m = v
116
+ return SAFMIN * m
117
+
118
+
119
+ def tridiagonal_lowest(d: Sequence[float], e2: Sequence[float], k: int, first: int = 0,
120
+ check: Optional[Callable[[], None]] = None) -> List[float]:
121
+ """λ_first … λ_{k−1} (the lowest first) of the symmetric tridiagonal (d, squared off-diagonal e2), bisection on
122
+ the Sturm count."""
123
+ d = [float(x) for x in d]
124
+ e2 = [float(x) for x in e2]
125
+ n = len(d)
126
+ if not 1 <= k <= n:
127
+ raise ValueError("k out of range")
128
+ ex = max(_scale_exponent(d), (_scale_exponent(e2) + 1) // 2)
129
+ if ex != 0: # exact power-of-two scaling, undone exactly at the end
130
+ d = [math.ldexp(x, -ex) for x in d]
131
+ e2 = [math.ldexp(x, -2 * ex) for x in e2]
132
+ e = [math.sqrt(x) for x in e2]
133
+ lo, hi = math.inf, -math.inf
134
+ for i in range(n):
135
+ r = (e[i - 1] if i > 0 else 0.0) + (e[i] if i < n - 1 else 0.0)
136
+ if d[i] - r < lo:
137
+ lo = d[i] - r
138
+ if d[i] + r > hi:
139
+ hi = d[i] + r
140
+ scale = max(abs(lo), abs(hi))
141
+ atol = SAFMIN * max(scale, 1.0) # bisect to adjacent floats (relative accuracy where the matrix allows it)
142
+ pad = atol + 2.0 * EPS * scale * n
143
+ pivmin = _pivmin(e2)
144
+ out = _bisect_lowest(_checked(lambda x: sturm_count(d, e2, x, pivmin), check), k, lo - pad, hi + pad, atol, first)
145
+ return [math.ldexp(x, ex) for x in out] # ldexp raises OverflowError beyond float64
146
+
147
+
148
+ def householder_tridiagonal(A: Sequence[Sequence[float]]):
149
+ """(d, e2): diagonal and squared off-diagonal of Qᵀ A Q tridiagonal (A symmetric, dense, float). Householder
150
+ reflections H = I − v vᵀ·2/‖v‖² column by column; each column is scaled by its largest |entry| first. e2 holds
151
+ squares: OverflowError if an off-diagonal entry is beyond √(float max) ≈ 1.3e154 (dense_lowest never unscales them)."""
152
+ d, e2, ex = _householder_scaled(A)
153
+ if ex != 0:
154
+ d = [math.ldexp(x, ex) for x in d]
155
+ e2 = [math.ldexp(x, 2 * ex) for x in e2]
156
+ return d, e2
157
+
158
+
159
+ def _householder_scaled(A: Sequence[Sequence[float]], check: Optional[Callable[[], None]] = None):
160
+ """(d, e2, ex): the tridiagonal of A·2^−ex (exact power-of-two scaling, entries of A·2^−ex below 1)."""
161
+ n = len(A)
162
+ S = [[float(x) for x in row] for row in A]
163
+ ex = _scale_exponent(x for row in S for x in row)
164
+ if ex != 0: # exact power-of-two scaling, undone exactly at the end
165
+ S = [[math.ldexp(x, -ex) for x in row] for row in S]
166
+ d = [0.0] * n
167
+ e2 = [0.0] * max(n - 1, 0)
168
+ for k in range(n - 2):
169
+ if check is not None:
170
+ check()
171
+ x = [S[i][k] for i in range(k + 1, n)]
172
+ s = 0.0
173
+ for t in x:
174
+ if abs(t) > s:
175
+ s = abs(t)
176
+ d[k] = S[k][k]
177
+ if s == 0.0:
178
+ e2[k] = 0.0
179
+ continue
180
+ xs = [t / s for t in x]
181
+ alpha = math.sqrt(math.fsum(t * t for t in xs))
182
+ if xs[0] > 0:
183
+ alpha = -alpha
184
+ v = list(xs)
185
+ v[0] = v[0] - alpha # v = x/s − α e₁, |v₀| ≥ |x₀/s|: no cancellation
186
+ vv = math.fsum(t * t for t in v)
187
+ beta = alpha * s
188
+ e2[k] = beta * beta
189
+ if vv == 0.0:
190
+ continue
191
+ tau = 2.0 / vv
192
+ m = n - k - 1
193
+ rows = [S[k + 1 + i][k + 1:] for i in range(m)]
194
+ p = [tau * _fsum_dot(rows[i], v) for i in range(m)] # p = τ B v
195
+ K = tau * 0.5 * _fsum_dot(v, p) # K = τ vᵀp / 2
196
+ q = [p[i] - K * v[i] for i in range(m)] # B' = B − v qᵀ − q vᵀ
197
+ for i in range(m):
198
+ vi, qi = v[i], q[i]
199
+ row = [b - vi * qj - qi * vj for b, vj, qj in zip(rows[i], v, q)]
200
+ S[k + 1 + i][k + 1:] = row
201
+ if n >= 2:
202
+ d[n - 2] = S[n - 2][n - 2]
203
+ e2[n - 2] = S[n - 1][n - 2] * S[n - 1][n - 2]
204
+ d[n - 1] = S[n - 1][n - 1]
205
+ return d, e2, ex
206
+
207
+
208
+ def dense_lowest(A: Sequence[Sequence[float]], k: int, check: Optional[Callable[[], None]] = None) -> List[float]:
209
+ """The k lowest eigenvalues of the dense symmetric float matrix A (Householder, then Sturm bisection), computed
210
+ on A·2^−ex and scaled back exactly: the squared off-diagonal never leaves the scaled range (entries up to the
211
+ float max; before 2026-11-12 (NEMESIS) e2 was unscaled first and overflowed beyond ≈ 1.3e154)."""
212
+ d, e2, ex = _householder_scaled(A, check)
213
+ return [math.ldexp(x, ex) for x in tridiagonal_lowest(d, e2, k, check=check)] # ldexp raises OverflowError beyond float64
214
+
215
+
216
+ def band_count(diag: Sequence[float], bands: Dict[int, Sequence[float]], w: int, x: float, pivmin: float) -> int:
217
+ """#{negative pivots} of LDLᵀ of A − xI, A symmetric with diag and bands[m][i] = A[i, i+m] (1 ≤ m ≤ w)."""
218
+ n = len(diag)
219
+ D: List[float] = []
220
+ L: List[List[float]] = [] # L[i][t] = l_{i, i−w+t} for t < w
221
+ c = 0
222
+ for i in range(n):
223
+ Li = [0.0] * w
224
+ for j in range(max(0, i - w), i):
225
+ band = bands.get(i - j)
226
+ s = band[j] if band is not None else 0.0
227
+ Lj = L[j]
228
+ for t in range(max(0, i - w), j):
229
+ lit = Li[t - i + w]
230
+ if lit != 0.0:
231
+ ljt = Lj[t - j + w]
232
+ if ljt != 0.0:
233
+ s = s - lit * ljt * D[t]
234
+ Li[j - i + w] = s / D[j]
235
+ dd = diag[i] - x
236
+ for t in range(max(0, i - w), i):
237
+ lit = Li[t - i + w]
238
+ if lit != 0.0:
239
+ dd = dd - lit * lit * D[t]
240
+ if abs(dd) <= pivmin:
241
+ dd = -pivmin
242
+ if dd < 0:
243
+ c += 1
244
+ D.append(dd)
245
+ L.append(Li)
246
+ return c
247
+
248
+
249
+ def band_lowest(diag: Sequence[float], bands: Dict[int, Sequence[float]], k: int,
250
+ check: Optional[Callable[[], None]] = None) -> List[float]:
251
+ """The k lowest eigenvalues of the symmetric band matrix (diag, bands[m][i] = A[i, i+m]); tridiagonal bands go to
252
+ the Sturm path."""
253
+ diag = [float(x) for x in diag]
254
+ bands = {m: [float(x) for x in b] for m, b in bands.items() if any(x != 0 for x in b)}
255
+ w = max(bands, default=0)
256
+ n = len(diag)
257
+ ex = max([_scale_exponent(diag)] + [_scale_exponent(b) for b in bands.values()])
258
+ if ex != 0: # exact power-of-two scaling, undone exactly at the end
259
+ diag = [math.ldexp(x, -ex) for x in diag]
260
+ bands = {m: [math.ldexp(x, -ex) for x in b] for m, b in bands.items()}
261
+ return [math.ldexp(x, ex) for x in band_lowest(diag, bands, k, check)] # ldexp raises OverflowError beyond float64
262
+ if w <= 1:
263
+ b1 = bands.get(1, [0.0] * (n - 1))
264
+ return tridiagonal_lowest(diag, [x * x for x in b1], k, check=check)
265
+ lo, hi = math.inf, -math.inf
266
+ for i in range(n):
267
+ r = 0.0
268
+ for m, b in bands.items():
269
+ if i + m < n:
270
+ r += abs(b[i])
271
+ if i - m >= 0:
272
+ r += abs(b[i - m])
273
+ if diag[i] - r < lo:
274
+ lo = diag[i] - r
275
+ if diag[i] + r > hi:
276
+ hi = diag[i] + r
277
+ scale = max(abs(lo), abs(hi))
278
+ atol = SAFMIN * max(scale, 1.0) # bisect to adjacent floats (relative accuracy where the matrix allows it)
279
+ pad = atol + 2.0 * EPS * scale * n
280
+ big = 1.0
281
+ for b in bands.values():
282
+ for v in b:
283
+ if v * v > big:
284
+ big = v * v
285
+ pivmin = SAFMIN * big
286
+ return _bisect_lowest(_checked(lambda x: band_count(diag, bands, w, x, pivmin), check), k, lo - pad, hi + pad, atol)
287
+
288
+
289
+ def _start_vector(n: int) -> List[float]:
290
+ """Fixed start vector with components of both signs and no symmetry (integer LCG, exact conversions)."""
291
+ s = 12345
292
+ out = []
293
+ for _ in range(n):
294
+ s = (1103515245 * s + 12345) % 2147483648
295
+ out.append((s - 1073741824) / 1073741824.0 + (0.5 if s & 1 else -0.5))
296
+ return out
297
+
298
+
299
+ def band_eigenvector(diag: Sequence[float], bands: Dict[int, Sequence[float]], lam: float, iterations: int = 3,
300
+ check: Optional[Callable[[], None]] = None) -> List[float]:
301
+ """Unit eigenvector estimate for the eigenvalue estimate `lam`: inverse iteration with (A − lam·I) factored by
302
+ banded Gaussian elimination with partial pivoting (the right-hand side follows the same row operations), from a
303
+ fixed start vector; sign fixed so that the entry of largest |value| is > 0 (on a tie, the first such index:
304
+ the antisymmetric levels of a symmetric rotor have exact ties, so this rule fixes published signs)."""
305
+ diag = [float(x) for x in diag]
306
+ n = len(diag)
307
+ bands = {m: [float(x) for x in b] for m, b in bands.items() if m >= 1 and any(x != 0 for x in b)}
308
+ w = max(bands, default=0)
309
+ rows: List[Dict[int, float]] = []
310
+ norm = 0.0
311
+ for i in range(n):
312
+ r = {i: diag[i] - lam}
313
+ for m, b in bands.items():
314
+ if i + m < n and b[i] != 0.0:
315
+ r[i + m] = b[i]
316
+ if i - m >= 0 and b[i - m] != 0.0:
317
+ r[i - m] = b[i - m]
318
+ a = math.fsum(abs(v) for v in r.values())
319
+ if a > norm:
320
+ norm = a
321
+ rows.append(r)
322
+ tiny = EPS * norm if norm > 0 else SAFMIN
323
+ steps = [] # (pivot row chosen, [(row, multiplier)])
324
+ for c in range(n):
325
+ if check is not None and c % 256 == 0:
326
+ check()
327
+ best, bi = -1.0, c
328
+ for i in range(c, min(n, c + w + 1)):
329
+ v = abs(rows[i].get(c, 0.0))
330
+ if v > best:
331
+ best, bi = v, i
332
+ if bi != c:
333
+ rows[c], rows[bi] = rows[bi], rows[c]
334
+ pr = rows[c]
335
+ piv = pr.get(c, 0.0)
336
+ if abs(piv) < tiny:
337
+ piv = tiny if piv >= 0 else -tiny
338
+ pr[c] = piv
339
+ cols = sorted(col for col in pr if col > c)
340
+ mults = []
341
+ for i in range(c + 1, min(n, c + w + 1)):
342
+ ri = rows[i]
343
+ a = ri.pop(c, 0.0)
344
+ if a == 0.0:
345
+ continue
346
+ l = a / piv
347
+ mults.append((i, l))
348
+ for col in cols:
349
+ ri[col] = ri.get(col, 0.0) - l * pr[col]
350
+ steps.append((bi, mults))
351
+ U = [sorted((col, v) for col, v in rows[c].items() if col > c) for c in range(n)]
352
+ x = _start_vector(n)
353
+ for _ in range(iterations):
354
+ y = list(x)
355
+ for c, (bi, mults) in enumerate(steps):
356
+ if bi != c:
357
+ y[c], y[bi] = y[bi], y[c]
358
+ yc = y[c]
359
+ for i, l in mults:
360
+ y[i] = y[i] - l * yc
361
+ z = [0.0] * n
362
+ for c in range(n - 1, -1, -1):
363
+ s = y[c]
364
+ for col, v in U[c]:
365
+ s = s - v * z[col]
366
+ z[c] = s / rows[c][c]
367
+ s = 0.0
368
+ for v in z:
369
+ if abs(v) > s:
370
+ s = abs(v)
371
+ if s == 0.0 or not math.isfinite(s):
372
+ break
373
+ x = [v / s for v in z]
374
+ nrm = math.sqrt(math.fsum(v * v for v in x))
375
+ x = [v / nrm for v in x]
376
+ big, bi = -1.0, 0
377
+ for i, v in enumerate(x):
378
+ if abs(v) > big:
379
+ big, bi = abs(v), i
380
+ if x[bi] < 0:
381
+ x = [-v for v in x]
382
+ return x
@@ -0,0 +1,134 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2026 Roberto Locatelli
3
+ """rigorous_levels/duffing_vs_transmon.py — GUARANTEED per-level error of the Duffing model against the full transmon.
4
+
5
+ The dynamics stacks (qutip-qip circuitqed.py:84-120, dynamiqs benchmarks/systems.py:58-82) use
6
+ H_D = ω a†a + (α/2) a†a†aa; its levels are exact and diagonal in Fock: E^D_j = ω j + (α/2) j (j − 1). The full
7
+ transmon H = 4E_C(n − n_g)² − E_J cos φ has levels E_j enclosed by the intervals of rigorous_levels/guaranteed_intervals.py.
8
+ Here:
9
+ variant "spectrum": ω = E_1 − E_0 and α = (E_2 − E_1) − (E_1 − E_0) taken from the GUARANTEED intervals (by
10
+ construction δ_0 = δ_1 = δ_2 = 0): δ_j = (E_j − E_0) − E^D_j for j ≥ 3 as an exact interval;
11
+ variant "koch": ω = √(8 E_J E_C) − E_C and α = −E_C (Koch et al. 2007, eq. 2.11-2.12, first order): δ_j for
12
+ j ≥ 1, with √(8E_JE_C) enclosed in an Arb ball converted to a rational interval.
13
+ Closed inputs (OutOfDomain with the reason). Measured on 200 transmons of the device regime (2026-11-11): |δ_3| ≥ 1 MHz
14
+ in 200 of 200 (median 34 MHz). This package is not an external validator."""
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from dataclasses import dataclass
19
+ from fractions import Fraction
20
+ from typing import Optional, Tuple
21
+
22
+ from rigorous_levels._compat import add_field_aliases, deprecated_function
23
+ from rigorous_levels.guaranteed_intervals import Intervals, OutOfDomain, _flint, guaranteed_levels
24
+ from rigorous_levels.rotor_spectrum import Rotor
25
+
26
+ __all__ = ["DuffingError", "duffing_error", "duffing_levels", "OutOfDomain"]
27
+
28
+ VARIANTS = ("spectrum", "koch") # 0.2.0: "spettro", "koch"
29
+ K_MAX = 20
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class DuffingError:
34
+ variant: str
35
+ omega: Tuple[Fraction, Fraction] # interval of ω (GHz)
36
+ alpha: Tuple[Fraction, Fraction] # interval of α (GHz)
37
+ delta: Tuple[Tuple[Fraction, Fraction], ...] # for j < k: interval of (E_j − E_0) − E^D_j
38
+ intervals: Intervals # the transmon levels used
39
+ E_C: float
40
+
41
+ def delta_float(self):
42
+ return tuple((float(a), float(b)) for a, b in self.delta)
43
+
44
+ def delta_over_EC(self):
45
+ return tuple((float(a) / self.E_C, float(b) / self.E_C) for a, b in self.delta)
46
+
47
+
48
+ def _real(x, name, positive=False):
49
+ if isinstance(x, bool) or not isinstance(x, (int, float)):
50
+ raise OutOfDomain(f"{name}: expected a finite real number")
51
+ try:
52
+ x = float(x)
53
+ except OverflowError:
54
+ raise OutOfDomain(f"{name}: expected a finite real number") from None
55
+ if not math.isfinite(x) or (positive and x <= 0):
56
+ raise OutOfDomain(f"{name}: expected a finite real{' > 0' if positive else ''}")
57
+ return x
58
+
59
+
60
+ def duffing_levels(omega: Fraction, alpha: Fraction, j: int) -> Fraction:
61
+ """E^D_j = ω j + (α/2) j (j − 1), exact."""
62
+ return omega * j + alpha * j * (j - 1) / 2
63
+
64
+
65
+ def _sqrt_interval(fl, x: Fraction, prec: int = 128) -> Tuple[Fraction, Fraction]:
66
+ """Rational [low, high] enclosing √x (Arb ball → ends as Fractions)."""
67
+ old_prec = fl.ctx.prec
68
+ fl.ctx.prec = prec
69
+ try:
70
+ s = fl.arb(fl.fmpq(x.numerator, x.denominator)).sqrt()
71
+ lo = Fraction(str(s.lower().str(40, radius=False)))
72
+ hi = Fraction(str(s.upper().str(40, radius=False)))
73
+ finally:
74
+ fl.ctx.prec = old_prec
75
+ # the 40-digit strings are ROUNDED, not directed outwards: they are widened until the inclusion is verified in
76
+ # EXACT arithmetic (Fraction); the final inclusion is proven by the check, not by the string
77
+ return _widen(lo, hi, x)
78
+
79
+
80
+ def _widen(lo: Fraction, hi: Fraction, x: Fraction) -> Tuple[Fraction, Fraction]:
81
+ """Rational [lo', hi'] with lo' ≤ √x ≤ hi' (x ≥ 0), starting from a guess [lo, hi] (the rounded strings of Arb).
82
+ Validity in EXACT arithmetic: hi' ≥ 0 and hi'² ≥ x; lo' ≤ 0 or lo'² ≤ x.
83
+ hi: if negative it restarts from max(x, 1) (max(x,1)² ≥ x), then grows with a doubling step (always finite: hi > 0
84
+ increasing). lo: if the guess does not hold it falls back to x / hi' ((x/hi')² = x²/hi'² ≤ x): no loop that could
85
+ jump over √x (reviewer, 2026-11-11: with lo ≥ ~10√x or lo negative the decrement loop did not terminate)."""
86
+ if x < 0:
87
+ raise ArithmeticError("square root of a negative number")
88
+ if hi < 0:
89
+ hi = max(x, Fraction(1))
90
+ step = max(abs(hi), Fraction(1)) * Fraction(1, 10 ** 35)
91
+ while hi * hi < x:
92
+ hi += step
93
+ step *= 2
94
+ if lo > 0 and lo * lo > x:
95
+ lo = x / hi if hi > 0 else Fraction(0)
96
+ if not (lo <= hi) or hi < 0 or hi * hi < x or (lo > 0 and lo * lo > x):
97
+ raise ArithmeticError("interval of the square root not verified")
98
+ return lo, hi
99
+
100
+
101
+ def duffing_error(E_C: float, E_J: float, n_g: float = 0.0, k: int = 6, variant: str = "spectrum", *,
102
+ width: float = 1e-12, deadline: Optional[float] = None) -> DuffingError:
103
+ """Guaranteed per-level error of the Duffing model against the transmon (units of E_C, E_J; GHz by convention)."""
104
+ E_C = _real(E_C, "E_C", positive=True)
105
+ E_J = _real(E_J, "E_J", positive=True)
106
+ n_g = _real(n_g, "n_g")
107
+ if isinstance(k, bool) or not isinstance(k, int) or k < 3 or k > K_MAX:
108
+ raise OutOfDomain(f"k: expected an integer in [3, {K_MAX}] (δ is meaningful from j = 3; ω and α use the first three levels)")
109
+ if variant not in VARIANTS:
110
+ raise OutOfDomain(f"variant {variant!r} not in {VARIANTS}")
111
+ iv = guaranteed_levels(Rotor(E_C=E_C, cosines=((1, -E_J),), n_g=n_g), k, width, deadline=deadline)
112
+ b, a = iv.low, iv.high
113
+ if variant == "spectrum":
114
+ omega = (b[1] - a[0], a[1] - b[0])
115
+ f12 = (b[2] - a[1], a[2] - b[1])
116
+ alpha = (f12[0] - omega[1], f12[1] - omega[0])
117
+ else:
118
+ fl = _flint()
119
+ r = _sqrt_interval(fl, 8 * Fraction(E_J) * Fraction(E_C))
120
+ omega = (r[0] - Fraction(E_C), r[1] - Fraction(E_C))
121
+ alpha = (Fraction(-E_C), Fraction(-E_C))
122
+ delta = []
123
+ for j in range(k):
124
+ tr = (b[j] - a[0], a[j] - b[0]) # E_j − E_0
125
+ d_lo = duffing_levels(omega[0] if j >= 0 else omega[1], alpha[0] if j >= 2 else alpha[1], j)
126
+ d_hi = duffing_levels(omega[1], alpha[1] if j >= 2 else alpha[0], j)
127
+ # E^D_j is increasing in ω (j ≥ 0) and in α (j ≥ 2: j(j−1) ≥ 0): the extremes are at the ends
128
+ delta.append((tr[0] - d_hi, tr[1] - d_lo))
129
+ return DuffingError(variant, omega, alpha, tuple(delta), iv, E_C)
130
+
131
+
132
+ # deprecated Italian names of 0.2.0 (DeprecationWarning on use; removed in a later release)
133
+ add_field_aliases(DuffingError, {"variante": "variant", "intervalli": "intervals"})
134
+ DuffingError.delta_su_EC = deprecated_function(DuffingError.delta_over_EC, "delta_su_EC")