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.
- rigorous_levels/__init__.py +41 -0
- rigorous_levels/_compat.py +59 -0
- rigorous_levels/_deterministic.py +382 -0
- rigorous_levels/duffing_vs_transmon.py +134 -0
- rigorous_levels/fluxonium.py +273 -0
- rigorous_levels/guaranteed_intervals.py +455 -0
- rigorous_levels/hyperfine_molecule.py +646 -0
- rigorous_levels/intervallo_garantito.py +23 -0
- rigorous_levels/isolation.py +1020 -0
- rigorous_levels/mathieu.py +216 -0
- rigorous_levels/matrix_elements.py +201 -0
- rigorous_levels/molecola_iperfine_garantita.py +24 -0
- rigorous_levels/multiport.py +496 -0
- rigorous_levels/multiporta.py +27 -0
- rigorous_levels/parameter_balls.py +170 -0
- rigorous_levels/port_runner.py +501 -0
- rigorous_levels/reproduce.py +307 -0
- rigorous_levels/rotor_spectrum.py +218 -0
- rigorous_levels/rotore_sferico_garantito.py +22 -0
- rigorous_levels/rotore_spettro.py +19 -0
- rigorous_levels/scqubits_adapter.py +305 -0
- rigorous_levels/scqubits_adattatore.py +35 -0
- rigorous_levels/spherical_rotor.py +247 -0
- rigorous_levels-0.3.0.dist-info/METADATA +414 -0
- rigorous_levels-0.3.0.dist-info/RECORD +29 -0
- rigorous_levels-0.3.0.dist-info/WHEEL +5 -0
- rigorous_levels-0.3.0.dist-info/licenses/LICENSE +202 -0
- rigorous_levels-0.3.0.dist-info/licenses/NOTICE +10 -0
- rigorous_levels-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -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")
|