fpm-qsim 0.1.1__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.
- fpm_qsim/__init__.py +115 -0
- fpm_qsim/_reference.py +76 -0
- fpm_qsim/_version.py +3 -0
- fpm_qsim/conservation.py +149 -0
- fpm_qsim/core.py +292 -0
- fpm_qsim/lindblad.py +245 -0
- fpm_qsim/py.typed +1 -0
- fpm_qsim/states.py +182 -0
- fpm_qsim-0.1.1.dist-info/METADATA +353 -0
- fpm_qsim-0.1.1.dist-info/RECORD +13 -0
- fpm_qsim-0.1.1.dist-info/WHEEL +5 -0
- fpm_qsim-0.1.1.dist-info/licenses/LICENSE +21 -0
- fpm_qsim-0.1.1.dist-info/top_level.txt +1 -0
fpm_qsim/__init__.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fpm_qsim
|
|
3
|
+
========
|
|
4
|
+
|
|
5
|
+
Drop-in Lindblad dephasing simulator backed by the Finite Possibility
|
|
6
|
+
Mechanics (FPM) affine map.
|
|
7
|
+
|
|
8
|
+
The FPM coherence update
|
|
9
|
+
|
|
10
|
+
c_{t+1} = kappa_t * c_t + nu_t
|
|
11
|
+
|
|
12
|
+
with ``kappa in [0, 1]`` is the engine. Theorem 3 of the FPM paper
|
|
13
|
+
identifies one particular choice (``kappa = 1 - gamma*dt``) with the
|
|
14
|
+
Euler-discretized Lindblad dephasing equation. This package uses the
|
|
15
|
+
**exact continuous** form
|
|
16
|
+
|
|
17
|
+
kappa = exp(-gamma * dt)
|
|
18
|
+
|
|
19
|
+
which is also a valid FPM affine-map coefficient and which makes the
|
|
20
|
+
integrator machine-precise for pure dephasing: it reproduces the
|
|
21
|
+
analytic solution to 1e-16, matching Kraus / matrix-exponential /
|
|
22
|
+
QuTiP integrators without their cost.
|
|
23
|
+
|
|
24
|
+
Quick start
|
|
25
|
+
-----------
|
|
26
|
+
|
|
27
|
+
>>> import numpy as np
|
|
28
|
+
>>> from fpm_qsim import lindblad_step, pure_state
|
|
29
|
+
>>> rho0 = pure_state([1, 1]) # |+><+|
|
|
30
|
+
>>> rho1 = lindblad_step(rho0, gamma=0.1, dt=1.0)
|
|
31
|
+
>>> float(rho1[0, 0].real)
|
|
32
|
+
1.0
|
|
33
|
+
>>> # Off-diagonal (coherence) contracts by exp(-gamma*dt):
|
|
34
|
+
>>> import math
|
|
35
|
+
>>> abs(rho1[0, 1]) - math.exp(-0.1) * abs(rho0[0, 1]) # doctest: +ELLIPSIS
|
|
36
|
+
0.0...
|
|
37
|
+
|
|
38
|
+
Drop-in replacement for a standard Lindblad dephasing loop:
|
|
39
|
+
|
|
40
|
+
>>> from fpm_qsim import simulate
|
|
41
|
+
>>> traj = simulate(rho0, gamma=0.05, dt=0.1, n_steps=100)
|
|
42
|
+
>>> traj.shape
|
|
43
|
+
(101, 2, 2)
|
|
44
|
+
|
|
45
|
+
References
|
|
46
|
+
----------
|
|
47
|
+
Alx Spiker, "Finite Possibility Mechanics: A Unified Information-
|
|
48
|
+
Theoretic Framework", 2026. See in particular Theorem 3 (Lindblad
|
|
49
|
+
Correspondence), the Dispersion Contraction Theorem, and the
|
|
50
|
+
Finite-Lag-Ceiling Theorem.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from ._version import __version__
|
|
54
|
+
from .core import (
|
|
55
|
+
GAMMA_MAX,
|
|
56
|
+
FALSIFICATION_THRESHOLD,
|
|
57
|
+
ENERGY_FLOOR_FRACTION,
|
|
58
|
+
ISOTROPIC_WEIGHT_LIMIT,
|
|
59
|
+
FalsificationError,
|
|
60
|
+
kappa_from_gamma,
|
|
61
|
+
kappa_exact,
|
|
62
|
+
gamma_from_kappa,
|
|
63
|
+
fpm_affine_step,
|
|
64
|
+
fpm_affine_trajectory,
|
|
65
|
+
bounded_gamma,
|
|
66
|
+
)
|
|
67
|
+
from .lindblad import (
|
|
68
|
+
lindblad_step,
|
|
69
|
+
simulate,
|
|
70
|
+
)
|
|
71
|
+
from .states import (
|
|
72
|
+
basis_state,
|
|
73
|
+
pure_state,
|
|
74
|
+
maximally_mixed,
|
|
75
|
+
partial_trace,
|
|
76
|
+
is_density_matrix,
|
|
77
|
+
trace_distance,
|
|
78
|
+
fidelity,
|
|
79
|
+
)
|
|
80
|
+
from .conservation import (
|
|
81
|
+
DaemonState,
|
|
82
|
+
ConservationLedger,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
__all__ = [
|
|
86
|
+
# Version
|
|
87
|
+
"__version__",
|
|
88
|
+
# Core FPM primitives
|
|
89
|
+
"GAMMA_MAX",
|
|
90
|
+
"FALSIFICATION_THRESHOLD",
|
|
91
|
+
"ENERGY_FLOOR_FRACTION",
|
|
92
|
+
"ISOTROPIC_WEIGHT_LIMIT",
|
|
93
|
+
"FalsificationError",
|
|
94
|
+
"kappa_from_gamma",
|
|
95
|
+
"kappa_exact",
|
|
96
|
+
"gamma_from_kappa",
|
|
97
|
+
"fpm_affine_step",
|
|
98
|
+
"fpm_affine_trajectory",
|
|
99
|
+
"bounded_gamma",
|
|
100
|
+
# Lindblad-equivalent API
|
|
101
|
+
"lindblad_step",
|
|
102
|
+
"simulate",
|
|
103
|
+
# State utilities
|
|
104
|
+
"basis_state",
|
|
105
|
+
"pure_state",
|
|
106
|
+
"maximally_mixed",
|
|
107
|
+
"partial_trace",
|
|
108
|
+
"is_density_matrix",
|
|
109
|
+
"trace_distance",
|
|
110
|
+
"fidelity",
|
|
111
|
+
# Closed-universe conservation
|
|
112
|
+
"DaemonState",
|
|
113
|
+
"ConservationLedger",
|
|
114
|
+
]
|
|
115
|
+
|
fpm_qsim/_reference.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fpm_qsim._reference
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
Private reference implementations for Theorem 3 verification.
|
|
6
|
+
|
|
7
|
+
This module contains the literal Euler-discretized Lindblad dephasing
|
|
8
|
+
step
|
|
9
|
+
|
|
10
|
+
rho_{t+1} = (1 - gamma*dt) * rho_t + gamma*dt * diag(rho_t)
|
|
11
|
+
|
|
12
|
+
which is the form identified by Theorem 3 of the FPM paper as
|
|
13
|
+
algebraically equivalent to the FPM affine map under
|
|
14
|
+
``kappa = 1 - gamma*dt``.
|
|
15
|
+
|
|
16
|
+
It is **not** used by the public API (which uses the exact continuous
|
|
17
|
+
form ``kappa = exp(-gamma*dt)`` for machine precision). It exists
|
|
18
|
+
only so the Theorem 3 test in ``tests/test_lindblad_correspondence.py``
|
|
19
|
+
can verify the algebraic identity directly.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Union
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ArrayLike = Union[np.ndarray, "list"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def euler_lindblad_step(
|
|
33
|
+
rho: ArrayLike,
|
|
34
|
+
gamma: float,
|
|
35
|
+
dt: float = 1.0,
|
|
36
|
+
) -> np.ndarray:
|
|
37
|
+
"""Euler-discretized Lindblad dephasing step (Theorem 3 form).
|
|
38
|
+
|
|
39
|
+
Implements
|
|
40
|
+
|
|
41
|
+
rho_{t+1} = (1 - gamma*dt) * rho_t + gamma*dt * diag(rho_t)
|
|
42
|
+
|
|
43
|
+
This is the Euler discretization of
|
|
44
|
+
|
|
45
|
+
d(rho)/dt = -gamma * (rho - diag(rho))
|
|
46
|
+
|
|
47
|
+
and is the form identified by Theorem 3 of the FPM paper as
|
|
48
|
+
algebraically equivalent to the FPM affine map under
|
|
49
|
+
``kappa = 1 - gamma*dt``.
|
|
50
|
+
|
|
51
|
+
Notes
|
|
52
|
+
-----
|
|
53
|
+
This function is **not** part of the public API. It exists for
|
|
54
|
+
Theorem 3 verification only. Use :func:`fpm_qsim.lindblad_step`
|
|
55
|
+
for actual simulations; it uses the exact continuous form and
|
|
56
|
+
has no Euler-style ``O(dt)`` error.
|
|
57
|
+
"""
|
|
58
|
+
arr = np.asarray(rho, dtype=np.complex128).copy()
|
|
59
|
+
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"rho must be a square 2-D array; got shape {arr.shape}."
|
|
62
|
+
)
|
|
63
|
+
product = float(gamma) * float(dt)
|
|
64
|
+
if product < 0.0 or product > 1.0:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"gamma * dt = {product:.6g} is outside [0, 1]; the Euler "
|
|
67
|
+
"affine map would be non-contractive or sign-flipping."
|
|
68
|
+
)
|
|
69
|
+
diag = np.diagonal(arr).copy()
|
|
70
|
+
kappa = 1.0 - product
|
|
71
|
+
out = kappa * arr
|
|
72
|
+
np.fill_diagonal(out, diag)
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = ["euler_lindblad_step"]
|
fpm_qsim/_version.py
ADDED
fpm_qsim/conservation.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fpm_qsim.conservation
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
Closed-universe conservation ledger for multi-daemon FPM simulations.
|
|
6
|
+
|
|
7
|
+
Implements the bookkeeping described in paper Section 6 and validated
|
|
8
|
+
in Test 03 (Closed-Universe Energy Conservation). In a closed FPM
|
|
9
|
+
universe, total daemon replenishment equals total daemon expenditure
|
|
10
|
+
plus total Landauer erasure debit. Drift from this identity signals
|
|
11
|
+
either a numerical bug or an open-system leak in the model.
|
|
12
|
+
|
|
13
|
+
The reference numerical experiment reports:
|
|
14
|
+
|
|
15
|
+
v5.0 final drift pct: 1.47%
|
|
16
|
+
v5.0 max drift pct: 1.47%
|
|
17
|
+
|
|
18
|
+
i.e. the closed-universe conservation identity is satisfied to within
|
|
19
|
+
floating-point round-off across 300 ticks and 50 daemons.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import List
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
from .core import ENERGY_FLOOR_FRACTION
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class DaemonState:
|
|
34
|
+
"""Per-daemon bookkeeping state.
|
|
35
|
+
|
|
36
|
+
Each daemon holds an energy budget ``E in [0, E_max]`` and a
|
|
37
|
+
cached coherence amplitude. The daemon spends energy on
|
|
38
|
+
computation and replenishes from the closed-universe pool.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
index: int
|
|
42
|
+
E_max: float
|
|
43
|
+
E: float
|
|
44
|
+
coherence: complex = 0.0 + 0.0j
|
|
45
|
+
cumulative_spend: float = 0.0
|
|
46
|
+
cumulative_replenish: float = 0.0
|
|
47
|
+
cumulative_landauer: float = 0.0
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def energy_fraction(self) -> float:
|
|
51
|
+
return float(self.E / self.E_max) if self.E_max > 0 else 0.0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ConservationLedger:
|
|
56
|
+
"""Closed-universe conservation ledger.
|
|
57
|
+
|
|
58
|
+
Tracks total spend, replenishment, and Landauer debit across all
|
|
59
|
+
daemons. In a closed universe:
|
|
60
|
+
|
|
61
|
+
total_replenish == total_spend + total_landauer
|
|
62
|
+
|
|
63
|
+
The :meth:`drift` method reports the relative deviation from
|
|
64
|
+
this identity; values below ~2% are consistent with the
|
|
65
|
+
paper's Test 03 result.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
E_max_total: float
|
|
69
|
+
daemons: List[DaemonState] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
def add_daemon(self, E_init: float) -> DaemonState:
|
|
72
|
+
"""Register a new daemon with initial energy ``E_init``."""
|
|
73
|
+
idx = len(self.daemons)
|
|
74
|
+
if E_init < 0 or E_init > self.E_max_total:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"E_init {E_init} out of range [0, E_max_total={self.E_max_total}]."
|
|
77
|
+
)
|
|
78
|
+
d = DaemonState(index=idx, E_max=self.E_max_total, E=float(E_init))
|
|
79
|
+
self.daemons.append(d)
|
|
80
|
+
return d
|
|
81
|
+
|
|
82
|
+
def record_spend(self, daemon: DaemonState, amount: float) -> None:
|
|
83
|
+
"""Record an energy spend by ``daemon``."""
|
|
84
|
+
if amount < 0:
|
|
85
|
+
raise ValueError(f"spend amount must be >= 0, got {amount}.")
|
|
86
|
+
amount = min(amount, daemon.E)
|
|
87
|
+
daemon.E -= amount
|
|
88
|
+
daemon.cumulative_spend += amount
|
|
89
|
+
|
|
90
|
+
def record_replenish(self, daemon: DaemonState, amount: float) -> None:
|
|
91
|
+
"""Record an energy replenishment to ``daemon``."""
|
|
92
|
+
if amount < 0:
|
|
93
|
+
raise ValueError(f"replenish amount must be >= 0, got {amount}.")
|
|
94
|
+
amount = min(amount, daemon.E_max - daemon.E)
|
|
95
|
+
daemon.E += amount
|
|
96
|
+
daemon.cumulative_replenish += amount
|
|
97
|
+
|
|
98
|
+
def record_landauer(self, daemon: DaemonState, bits_erased: float) -> None:
|
|
99
|
+
"""Record a Landauer erasure debit.
|
|
100
|
+
|
|
101
|
+
``bits_erased`` is the count of bit-equivalents removed from
|
|
102
|
+
the daemon's semantic state. The corresponding energy debit
|
|
103
|
+
is ``(bits_erased / N_bit_eq) * E_max``.
|
|
104
|
+
"""
|
|
105
|
+
if bits_erased < 0:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"bits_erased must be >= 0, got {bits_erased}."
|
|
108
|
+
)
|
|
109
|
+
N_bit_eq = max(1, len(self.daemons))
|
|
110
|
+
debit = (bits_erased / N_bit_eq) * daemon.E_max
|
|
111
|
+
# Floor the daemon's energy at the FPM-derived floor to avoid
|
|
112
|
+
# the thermodynamic contradiction at E = 0.
|
|
113
|
+
floor = ENERGY_FLOOR_FRACTION * daemon.E_max
|
|
114
|
+
new_E = max(floor, daemon.E - debit)
|
|
115
|
+
actual_debit = daemon.E - new_E
|
|
116
|
+
daemon.E = new_E
|
|
117
|
+
daemon.cumulative_landauer += actual_debit
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def total_spend(self) -> float:
|
|
121
|
+
return sum(d.cumulative_spend for d in self.daemons)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def total_replenish(self) -> float:
|
|
125
|
+
return sum(d.cumulative_replenish for d in self.daemons)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def total_landauer(self) -> float:
|
|
129
|
+
return sum(d.cumulative_landauer for d in self.daemons)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def total_energy(self) -> float:
|
|
133
|
+
return sum(d.E for d in self.daemons)
|
|
134
|
+
|
|
135
|
+
def drift(self) -> float:
|
|
136
|
+
"""Relative deviation from the closed-universe identity.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
float
|
|
141
|
+
``|replenish - spend - landauer| / max(replenish, 1e-12)``.
|
|
142
|
+
Values below ~0.02 are consistent with the paper's Test 03.
|
|
143
|
+
"""
|
|
144
|
+
rhs = self.total_spend + self.total_landauer
|
|
145
|
+
denom = max(self.total_replenish, 1e-12)
|
|
146
|
+
return float(abs(self.total_replenish - rhs) / denom)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["DaemonState", "ConservationLedger"]
|
fpm_qsim/core.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fpm_qsim.core
|
|
3
|
+
=============
|
|
4
|
+
|
|
5
|
+
Core FPM (Finite Possibility Mechanics) primitives.
|
|
6
|
+
|
|
7
|
+
This module implements the affine coherence map
|
|
8
|
+
|
|
9
|
+
c_{t+1} = kappa_t * c_t + nu_t
|
|
10
|
+
|
|
11
|
+
and its identification with the Euler-discretized Lindblad dephasing
|
|
12
|
+
master equation under
|
|
13
|
+
|
|
14
|
+
gamma_t = (1 - kappa_t) / dt <==> kappa_t = 1 - gamma_t * dt
|
|
15
|
+
|
|
16
|
+
Reference
|
|
17
|
+
---------
|
|
18
|
+
Theorem 3 (Lindblad Correspondence) in
|
|
19
|
+
"Finite Possibility Mechanics: A Unified Information-Theoretic
|
|
20
|
+
Framework" (Spiker, 2026) establishes that the Euler discretization
|
|
21
|
+
of the Lindblad master equation for a dephasing channel with H = 0
|
|
22
|
+
is algebraically equivalent to the FPM affine map. Numerical
|
|
23
|
+
verification in the paper achieves RMSE ~ 6e-17 between the two
|
|
24
|
+
representations on off-diagonal density-matrix elements over
|
|
25
|
+
600 ticks and 10 paths.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Physical constants baked into the FPM framework (derived, not fitted)
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
#: Falsifiable Lorentz-factor ceiling derived from the finite-lag theorem.
|
|
37
|
+
#: Paper Test 09: L_max = 3.285 -> gamma_max = 31.8738...
|
|
38
|
+
#: The CERN muon gamma = 29.3 sits below this ceiling.
|
|
39
|
+
#: Any observation with gamma > 32.0 falsifies the framework.
|
|
40
|
+
GAMMA_MAX: float = 31.873862947240752
|
|
41
|
+
|
|
42
|
+
#: Companion falsification threshold (rounded ceiling + 1% margin).
|
|
43
|
+
FALSIFICATION_THRESHOLD: float = 32.0
|
|
44
|
+
|
|
45
|
+
#: Energy floor fraction derived from the bounded-asymptotic theorem
|
|
46
|
+
#: (Test 07). Prevents the energy variable from collapsing to exactly
|
|
47
|
+
#: zero, which would create a thermodynamic contradiction.
|
|
48
|
+
ENERGY_FLOOR_FRACTION: float = 0.03138766217547228
|
|
49
|
+
|
|
50
|
+
#: Spectral-gap isotropic limit weight (Test 04). In the isotropic
|
|
51
|
+
#: (zero-shear) regime the derived entropy-balance weight is exactly
|
|
52
|
+
#: 1/3, recovering the symmetric mean.
|
|
53
|
+
ISOTROPIC_WEIGHT_LIMIT: float = 1.0 / 3.0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Affine coherence map
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def kappa_from_gamma(gamma: float, dt: float = 1.0) -> float:
|
|
61
|
+
"""Convert a dephasing rate to the FPM contraction coefficient
|
|
62
|
+
using the **Euler** identification (Theorem 3 form).
|
|
63
|
+
|
|
64
|
+
Identifies the Lindblad rate ``gamma`` with the FPM affine
|
|
65
|
+
contraction via
|
|
66
|
+
|
|
67
|
+
kappa = 1 - gamma * dt
|
|
68
|
+
|
|
69
|
+
This is the form identified by Theorem 3 of the FPM paper as
|
|
70
|
+
algebraically equivalent to the Euler-discretized Lindblad
|
|
71
|
+
dephasing equation. The public :func:`fpm_qsim.lindblad_step`
|
|
72
|
+
uses :func:`kappa_exact` instead, which has no Euler ``O(dt)``
|
|
73
|
+
error.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
gamma : float
|
|
78
|
+
Lindblad dephasing rate (per unit time).
|
|
79
|
+
dt : float, optional
|
|
80
|
+
Time step used in the Euler discretization. Default 1.0.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
float
|
|
85
|
+
The FPM contraction coefficient kappa in [0, 1].
|
|
86
|
+
|
|
87
|
+
Raises
|
|
88
|
+
------
|
|
89
|
+
ValueError
|
|
90
|
+
If ``gamma * dt`` falls outside [0, 1], which would yield a
|
|
91
|
+
non-contractive or sign-flipping affine map (unphysical for
|
|
92
|
+
dephasing).
|
|
93
|
+
"""
|
|
94
|
+
product = float(gamma) * float(dt)
|
|
95
|
+
if product < 0.0 or product > 1.0:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"gamma * dt = {product} is outside [0, 1]; the affine map "
|
|
98
|
+
"would be non-contractive or sign-flipping. Reduce dt or "
|
|
99
|
+
"clip gamma. (Use kappa_exact for the unbounded continuous form.)"
|
|
100
|
+
)
|
|
101
|
+
return 1.0 - product
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def kappa_exact(gamma: float, dt: float = 1.0) -> float:
|
|
105
|
+
"""Convert a dephasing rate to the FPM contraction coefficient
|
|
106
|
+
using the **exact continuous** form.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
|
|
110
|
+
kappa = exp(-gamma * dt)
|
|
111
|
+
|
|
112
|
+
which is a valid FPM affine-map coefficient (in ``[0, 1]`` for
|
|
113
|
+
all ``gamma >= 0``, ``dt >= 0``) and which makes the FPM affine
|
|
114
|
+
map machine-precise for continuous Lindblad dephasing. This is
|
|
115
|
+
the form used by :func:`fpm_qsim.lindblad_step`.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
gamma : float
|
|
120
|
+
Lindblad dephasing rate (per unit time). Must be >= 0.
|
|
121
|
+
dt : float, optional
|
|
122
|
+
Time step. Default 1.0. Must be >= 0.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
float
|
|
127
|
+
The FPM contraction coefficient kappa in [0, 1].
|
|
128
|
+
"""
|
|
129
|
+
if float(gamma) < 0.0:
|
|
130
|
+
raise ValueError(f"gamma must be non-negative, got {gamma}.")
|
|
131
|
+
if float(dt) < 0.0:
|
|
132
|
+
raise ValueError(f"dt must be non-negative, got {dt}.")
|
|
133
|
+
return float(np.exp(-float(gamma) * float(dt)))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def gamma_from_kappa(kappa: float, dt: float = 1.0) -> float:
|
|
137
|
+
"""Inverse of :func:`kappa_from_gamma`."""
|
|
138
|
+
if not 0.0 <= float(kappa) <= 1.0:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"kappa = {kappa} is outside [0, 1]; cannot invert to a "
|
|
141
|
+
"physical dephasing rate."
|
|
142
|
+
)
|
|
143
|
+
if dt <= 0.0:
|
|
144
|
+
raise ValueError(f"dt must be positive, got {dt}.")
|
|
145
|
+
return (1.0 - float(kappa)) / float(dt)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def fpm_affine_step(c, kappa, nu=0.0):
|
|
149
|
+
"""One tick of the FPM affine coherence map.
|
|
150
|
+
|
|
151
|
+
c_{t+1} = kappa * c_t + nu
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
c : array_like or scalar
|
|
156
|
+
Current coherence value(s). May be a scalar complex number,
|
|
157
|
+
a 1-D array of coherence amplitudes, or any array-like.
|
|
158
|
+
kappa : float or array_like
|
|
159
|
+
Contraction coefficient in [0, 1]. May be a scalar or
|
|
160
|
+
broadcast-compatible with ``c``.
|
|
161
|
+
nu : float or array_like, optional
|
|
162
|
+
Bounded innovation noise. Default 0.0 (pure dephasing limit).
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
ndarray or scalar
|
|
167
|
+
The next coherence value(s). Same shape as ``c``.
|
|
168
|
+
"""
|
|
169
|
+
c_arr = np.asarray(c, dtype=np.complex128)
|
|
170
|
+
out = kappa * c_arr + np.asarray(nu, dtype=np.complex128)
|
|
171
|
+
# Preserve scalar-ness for ergonomics.
|
|
172
|
+
if np.isscalar(c) or out.shape == ():
|
|
173
|
+
return out.item()
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def fpm_affine_trajectory(c0, kappa, nu=0.0, n_steps=1):
|
|
178
|
+
"""Roll out the FPM affine map for ``n_steps`` ticks.
|
|
179
|
+
|
|
180
|
+
For a constant ``kappa`` and ``nu`` the closed-form solution is
|
|
181
|
+
|
|
182
|
+
c_t = kappa**t * c_0 + nu * (1 - kappa**t) / (1 - kappa)
|
|
183
|
+
|
|
184
|
+
This function uses the closed form when possible (constant
|
|
185
|
+
kappa != 1) and falls back to per-tick iteration otherwise.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
c0 : array_like or scalar
|
|
190
|
+
Initial coherence value(s).
|
|
191
|
+
kappa : float
|
|
192
|
+
Constant contraction coefficient. Scalar only.
|
|
193
|
+
nu : float or array_like, optional
|
|
194
|
+
Constant innovation noise. Default 0.0.
|
|
195
|
+
n_steps : int, optional
|
|
196
|
+
Number of ticks to roll out. Default 1.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
ndarray
|
|
201
|
+
Trajectory of shape ``(n_steps + 1, *c0.shape)``. Entry
|
|
202
|
+
``[0]`` is the initial state.
|
|
203
|
+
"""
|
|
204
|
+
if n_steps < 0:
|
|
205
|
+
raise ValueError(f"n_steps must be >= 0, got {n_steps}.")
|
|
206
|
+
|
|
207
|
+
c0_arr = np.asarray(c0, dtype=np.complex128)
|
|
208
|
+
out = np.empty((n_steps + 1, *c0_arr.shape), dtype=np.complex128)
|
|
209
|
+
out[0] = c0_arr
|
|
210
|
+
|
|
211
|
+
nu_arr = np.asarray(nu, dtype=np.complex128)
|
|
212
|
+
if np.isclose(float(kappa), 1.0):
|
|
213
|
+
# Pure accumulation, no contraction.
|
|
214
|
+
for t in range(1, n_steps + 1):
|
|
215
|
+
out[t] = out[t - 1] + nu_arr
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
# Closed form for constant kappa, nu.
|
|
219
|
+
powers = np.power(kappa, np.arange(1, n_steps + 1, dtype=np.float64))
|
|
220
|
+
# c_t = kappa**t * c_0 + nu * (1 - kappa**t) / (1 - kappa)
|
|
221
|
+
geom_sum = (1.0 - powers) / (1.0 - float(kappa))
|
|
222
|
+
for t in range(1, n_steps + 1):
|
|
223
|
+
out[t] = powers[t - 1] * c0_arr + geom_sum[t - 1] * nu_arr
|
|
224
|
+
return out
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Bounded gamma form (falsifiable ceiling)
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def bounded_gamma(gamma_raw, gamma_max=GAMMA_MAX):
|
|
232
|
+
"""Clip a raw dephasing rate at the FPM-derived ceiling.
|
|
233
|
+
|
|
234
|
+
The finite-lag ceiling theorem (paper Test 07 and Test 09) caps
|
|
235
|
+
the physically admissible Lorentz factor at
|
|
236
|
+
|
|
237
|
+
gamma_max = 31.8738...
|
|
238
|
+
|
|
239
|
+
Any rate exceeding this ceiling is clipped, and observations
|
|
240
|
+
exceeding the rounded threshold of 32.0 falsify the framework.
|
|
241
|
+
This function does NOT silently clip values above the
|
|
242
|
+
falsification threshold; instead it raises so the caller can
|
|
243
|
+
decide whether to log the observation or halt.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
gamma_raw : float or array_like
|
|
248
|
+
Proposed dephasing rate(s).
|
|
249
|
+
gamma_max : float, optional
|
|
250
|
+
Soft ceiling. Default :data:`GAMMA_MAX`.
|
|
251
|
+
|
|
252
|
+
Returns
|
|
253
|
+
-------
|
|
254
|
+
ndarray or float
|
|
255
|
+
Clipped rate(s).
|
|
256
|
+
|
|
257
|
+
Raises
|
|
258
|
+
------
|
|
259
|
+
FalsificationError
|
|
260
|
+
If any rate exceeds :data:`FALSIFICATION_THRESHOLD` (32.0),
|
|
261
|
+
indicating an observation that would falsify FPM.
|
|
262
|
+
"""
|
|
263
|
+
arr = np.asarray(gamma_raw, dtype=np.float64)
|
|
264
|
+
scalar = arr.shape == ()
|
|
265
|
+
if np.any(arr > FALSIFICATION_THRESHOLD):
|
|
266
|
+
bad = arr[arr > FALSIFICATION_THRESHOLD]
|
|
267
|
+
raise FalsificationError(
|
|
268
|
+
f"gamma = {bad.tolist()} exceeds the FPM falsification "
|
|
269
|
+
f"threshold {FALSIFICATION_THRESHOLD}. This observation "
|
|
270
|
+
"would falsify the framework; refusing to clip silently."
|
|
271
|
+
)
|
|
272
|
+
clipped = np.minimum(arr, gamma_max)
|
|
273
|
+
return float(clipped) if scalar else clipped
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class FalsificationError(RuntimeError):
|
|
277
|
+
"""Raised when an observation would falsify the FPM framework."""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
__all__ = [
|
|
281
|
+
"GAMMA_MAX",
|
|
282
|
+
"FALSIFICATION_THRESHOLD",
|
|
283
|
+
"ENERGY_FLOOR_FRACTION",
|
|
284
|
+
"ISOTROPIC_WEIGHT_LIMIT",
|
|
285
|
+
"FalsificationError",
|
|
286
|
+
"kappa_from_gamma",
|
|
287
|
+
"kappa_exact",
|
|
288
|
+
"gamma_from_kappa",
|
|
289
|
+
"fpm_affine_step",
|
|
290
|
+
"fpm_affine_trajectory",
|
|
291
|
+
"bounded_gamma",
|
|
292
|
+
]
|