tnfr 6.0.0__py3-none-any.whl → 7.0.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.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +50 -5
- tnfr/__init__.pyi +0 -7
- tnfr/_compat.py +0 -1
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +44 -2
- tnfr/alias.py +14 -13
- tnfr/alias.pyi +5 -37
- tnfr/cache.py +9 -729
- tnfr/cache.pyi +8 -224
- tnfr/callback_utils.py +16 -31
- tnfr/callback_utils.pyi +3 -29
- tnfr/cli/__init__.py +17 -11
- tnfr/cli/__init__.pyi +0 -21
- tnfr/cli/arguments.py +175 -14
- tnfr/cli/arguments.pyi +5 -11
- tnfr/cli/execution.py +434 -48
- tnfr/cli/execution.pyi +14 -24
- tnfr/cli/utils.py +20 -3
- tnfr/cli/utils.pyi +5 -5
- tnfr/config/__init__.py +2 -1
- tnfr/config/__init__.pyi +2 -0
- tnfr/config/feature_flags.py +83 -0
- tnfr/config/init.py +1 -1
- tnfr/config/operator_names.py +1 -14
- tnfr/config/presets.py +6 -26
- tnfr/constants/__init__.py +10 -13
- tnfr/constants/__init__.pyi +10 -22
- tnfr/constants/aliases.py +31 -0
- tnfr/constants/core.py +4 -3
- tnfr/constants/init.py +1 -1
- tnfr/constants/metric.py +3 -3
- tnfr/dynamics/__init__.py +64 -10
- tnfr/dynamics/__init__.pyi +3 -4
- tnfr/dynamics/adaptation.py +79 -13
- tnfr/dynamics/aliases.py +10 -9
- tnfr/dynamics/coordination.py +77 -35
- tnfr/dynamics/dnfr.py +575 -274
- tnfr/dynamics/dnfr.pyi +1 -10
- tnfr/dynamics/integrators.py +47 -33
- tnfr/dynamics/integrators.pyi +0 -1
- tnfr/dynamics/runtime.py +489 -129
- tnfr/dynamics/sampling.py +2 -0
- tnfr/dynamics/selectors.py +101 -62
- tnfr/execution.py +15 -8
- tnfr/execution.pyi +5 -25
- tnfr/flatten.py +7 -3
- tnfr/flatten.pyi +1 -8
- tnfr/gamma.py +22 -26
- tnfr/gamma.pyi +0 -6
- tnfr/glyph_history.py +37 -26
- tnfr/glyph_history.pyi +1 -19
- tnfr/glyph_runtime.py +16 -0
- tnfr/glyph_runtime.pyi +9 -0
- tnfr/immutable.py +20 -15
- tnfr/immutable.pyi +4 -7
- tnfr/initialization.py +5 -7
- tnfr/initialization.pyi +1 -9
- tnfr/io.py +6 -305
- tnfr/io.pyi +13 -8
- tnfr/mathematics/__init__.py +81 -0
- tnfr/mathematics/backend.py +426 -0
- tnfr/mathematics/dynamics.py +398 -0
- tnfr/mathematics/epi.py +254 -0
- tnfr/mathematics/generators.py +222 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/operators.py +233 -0
- tnfr/mathematics/operators_factory.py +71 -0
- tnfr/mathematics/projection.py +78 -0
- tnfr/mathematics/runtime.py +173 -0
- tnfr/mathematics/spaces.py +247 -0
- tnfr/mathematics/transforms.py +292 -0
- tnfr/metrics/__init__.py +10 -10
- tnfr/metrics/coherence.py +123 -94
- tnfr/metrics/common.py +22 -13
- tnfr/metrics/common.pyi +42 -11
- tnfr/metrics/core.py +72 -14
- tnfr/metrics/diagnosis.py +48 -57
- tnfr/metrics/diagnosis.pyi +3 -7
- tnfr/metrics/export.py +3 -5
- tnfr/metrics/glyph_timing.py +41 -31
- tnfr/metrics/reporting.py +13 -6
- tnfr/metrics/sense_index.py +884 -114
- tnfr/metrics/trig.py +167 -11
- tnfr/metrics/trig.pyi +1 -0
- tnfr/metrics/trig_cache.py +112 -15
- tnfr/node.py +400 -17
- tnfr/node.pyi +55 -38
- tnfr/observers.py +111 -8
- tnfr/observers.pyi +0 -15
- tnfr/ontosim.py +9 -6
- tnfr/ontosim.pyi +0 -5
- tnfr/operators/__init__.py +529 -42
- tnfr/operators/__init__.pyi +14 -0
- tnfr/operators/definitions.py +350 -18
- tnfr/operators/definitions.pyi +0 -14
- tnfr/operators/grammar.py +760 -0
- tnfr/operators/jitter.py +28 -22
- tnfr/operators/registry.py +7 -12
- tnfr/operators/registry.pyi +0 -2
- tnfr/operators/remesh.py +38 -61
- tnfr/rng.py +17 -300
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/selector.py +3 -4
- tnfr/selector.pyi +1 -1
- tnfr/sense.py +22 -24
- tnfr/sense.pyi +0 -7
- tnfr/structural.py +504 -21
- tnfr/structural.pyi +41 -18
- tnfr/telemetry/__init__.py +23 -1
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/nu_f.py +423 -0
- tnfr/telemetry/nu_f.pyi +123 -0
- tnfr/tokens.py +1 -4
- tnfr/tokens.pyi +1 -6
- tnfr/trace.py +20 -53
- tnfr/trace.pyi +9 -37
- tnfr/types.py +244 -15
- tnfr/types.pyi +200 -14
- tnfr/units.py +69 -0
- tnfr/units.pyi +16 -0
- tnfr/utils/__init__.py +107 -48
- tnfr/utils/__init__.pyi +80 -11
- tnfr/utils/cache.py +1705 -65
- tnfr/utils/cache.pyi +370 -58
- tnfr/utils/chunks.py +104 -0
- tnfr/utils/chunks.pyi +21 -0
- tnfr/utils/data.py +95 -5
- tnfr/utils/data.pyi +8 -17
- tnfr/utils/graph.py +2 -4
- tnfr/utils/init.py +31 -7
- tnfr/utils/init.pyi +4 -11
- tnfr/utils/io.py +313 -14
- tnfr/{helpers → utils}/numeric.py +50 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +92 -4
- tnfr/validation/__init__.pyi +77 -17
- tnfr/validation/compatibility.py +79 -43
- tnfr/validation/compatibility.pyi +4 -6
- tnfr/validation/grammar.py +55 -133
- tnfr/validation/grammar.pyi +37 -8
- tnfr/validation/graph.py +138 -0
- tnfr/validation/graph.pyi +17 -0
- tnfr/validation/rules.py +161 -74
- tnfr/validation/rules.pyi +55 -18
- tnfr/validation/runtime.py +263 -0
- tnfr/validation/runtime.pyi +31 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +37 -0
- tnfr/validation/spectral.py +159 -0
- tnfr/validation/spectral.pyi +46 -0
- tnfr/validation/syntax.py +28 -139
- tnfr/validation/syntax.pyi +7 -4
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/viz/__init__.py +9 -0
- tnfr/viz/matplotlib.py +246 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
- tnfr-7.0.0.dist-info/RECORD +185 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/constants_glyphs.py +0 -16
- tnfr/constants_glyphs.pyi +0 -12
- tnfr/grammar.py +0 -25
- tnfr/grammar.pyi +0 -13
- tnfr/helpers/__init__.py +0 -151
- tnfr/helpers/__init__.pyi +0 -66
- tnfr/helpers/numeric.pyi +0 -12
- tnfr/presets.py +0 -15
- tnfr/presets.pyi +0 -7
- tnfr/utils/io.pyi +0 -10
- tnfr/utils/validators.py +0 -130
- tnfr/utils/validators.pyi +0 -19
- tnfr-6.0.0.dist-info/RECORD +0 -157
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""ΔNFR generator construction utilities."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Final, Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.random import Generator
|
|
8
|
+
|
|
9
|
+
from .backend import ensure_array, ensure_numpy, get_backend
|
|
10
|
+
|
|
11
|
+
__all__ = ["build_delta_nfr", "build_lindblad_delta_nfr"]
|
|
12
|
+
|
|
13
|
+
_TOPOLOGIES: Final[set[str]] = {"laplacian", "adjacency"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _ring_adjacency(dim: int) -> np.ndarray:
|
|
17
|
+
"""Return the adjacency matrix for a coherent ring topology."""
|
|
18
|
+
|
|
19
|
+
adjacency = np.zeros((dim, dim), dtype=float)
|
|
20
|
+
if dim == 1:
|
|
21
|
+
return adjacency
|
|
22
|
+
|
|
23
|
+
indices = np.arange(dim)
|
|
24
|
+
adjacency[indices, (indices + 1) % dim] = 1.0
|
|
25
|
+
adjacency[(indices + 1) % dim, indices] = 1.0
|
|
26
|
+
return adjacency
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _laplacian_from_adjacency(adjacency: np.ndarray) -> np.ndarray:
|
|
30
|
+
"""Construct a Laplacian operator from an adjacency matrix."""
|
|
31
|
+
|
|
32
|
+
degrees = adjacency.sum(axis=1)
|
|
33
|
+
laplacian = np.diag(degrees) - adjacency
|
|
34
|
+
return laplacian
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _hermitian_noise(dim: int, rng: Generator) -> np.ndarray:
|
|
38
|
+
"""Generate a Hermitian noise matrix with reproducible statistics."""
|
|
39
|
+
|
|
40
|
+
real = rng.standard_normal((dim, dim))
|
|
41
|
+
imag = rng.standard_normal((dim, dim))
|
|
42
|
+
noise = real + 1j * imag
|
|
43
|
+
return 0.5 * (noise + noise.conj().T)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _as_square_matrix(
|
|
47
|
+
matrix: Sequence[Sequence[complex]] | np.ndarray,
|
|
48
|
+
*,
|
|
49
|
+
expected_dim: int | None = None,
|
|
50
|
+
label: str = "matrix",
|
|
51
|
+
) -> np.ndarray:
|
|
52
|
+
"""Return ``matrix`` as a square :class:`numpy.ndarray` with validation."""
|
|
53
|
+
|
|
54
|
+
array = np.asarray(matrix, dtype=np.complex128)
|
|
55
|
+
if array.ndim != 2 or array.shape[0] != array.shape[1]:
|
|
56
|
+
raise ValueError(f"{label} must be a square matrix.")
|
|
57
|
+
if expected_dim is not None and array.shape[0] != expected_dim:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"{label} dimension mismatch: expected {expected_dim}, received {array.shape[0]}."
|
|
60
|
+
)
|
|
61
|
+
return array
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_delta_nfr(
|
|
65
|
+
dim: int,
|
|
66
|
+
*,
|
|
67
|
+
topology: str = "laplacian",
|
|
68
|
+
nu_f: float = 1.0,
|
|
69
|
+
scale: float = 1.0,
|
|
70
|
+
rng: Generator | None = None,
|
|
71
|
+
) -> np.ndarray:
|
|
72
|
+
"""Construct a Hermitian ΔNFR generator using canonical TNFR topologies.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
dim:
|
|
77
|
+
Dimensionality of the Hilbert space supporting the ΔNFR operator.
|
|
78
|
+
topology:
|
|
79
|
+
Requested canonical topology. Supported values are ``"laplacian"``
|
|
80
|
+
and ``"adjacency"``.
|
|
81
|
+
nu_f:
|
|
82
|
+
Structural frequency scaling applied to the resulting operator.
|
|
83
|
+
scale:
|
|
84
|
+
Additional scaling applied uniformly to the operator amplitude.
|
|
85
|
+
rng:
|
|
86
|
+
Optional NumPy :class:`~numpy.random.Generator` used to inject
|
|
87
|
+
reproducible Hermitian noise.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
if dim <= 0:
|
|
91
|
+
raise ValueError("ΔNFR generators require a positive dimensionality.")
|
|
92
|
+
|
|
93
|
+
if topology not in _TOPOLOGIES:
|
|
94
|
+
allowed = ", ".join(sorted(_TOPOLOGIES))
|
|
95
|
+
raise ValueError(f"Unknown ΔNFR topology: {topology}. Expected one of: {allowed}.")
|
|
96
|
+
|
|
97
|
+
adjacency = _ring_adjacency(dim)
|
|
98
|
+
if topology == "laplacian":
|
|
99
|
+
base = _laplacian_from_adjacency(adjacency)
|
|
100
|
+
else:
|
|
101
|
+
base = adjacency
|
|
102
|
+
|
|
103
|
+
matrix = base.astype(np.complex128, copy=False)
|
|
104
|
+
|
|
105
|
+
if rng is not None:
|
|
106
|
+
noise = _hermitian_noise(dim, rng)
|
|
107
|
+
matrix = matrix + (1.0 / np.sqrt(dim)) * noise
|
|
108
|
+
|
|
109
|
+
matrix *= (nu_f * scale)
|
|
110
|
+
hermitian = 0.5 * (matrix + matrix.conj().T)
|
|
111
|
+
backend = get_backend()
|
|
112
|
+
return np.asarray(ensure_numpy(ensure_array(hermitian, backend=backend), backend=backend), dtype=np.complex128)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_lindblad_delta_nfr(
|
|
116
|
+
*,
|
|
117
|
+
hamiltonian: Sequence[Sequence[complex]] | np.ndarray | None = None,
|
|
118
|
+
collapse_operators: Sequence[Sequence[Sequence[complex]] | np.ndarray] | None = None,
|
|
119
|
+
dim: int | None = None,
|
|
120
|
+
nu_f: float = 1.0,
|
|
121
|
+
scale: float = 1.0,
|
|
122
|
+
ensure_trace_preserving: bool = True,
|
|
123
|
+
ensure_contractive: bool = True,
|
|
124
|
+
atol: float = 1e-9,
|
|
125
|
+
) -> np.ndarray:
|
|
126
|
+
"""Construct a Lindblad ΔNFR generator in Liouville space.
|
|
127
|
+
|
|
128
|
+
The resulting matrix acts on vectorised density operators using the
|
|
129
|
+
canonical column-major flattening. The construction follows the standard
|
|
130
|
+
Gorini–Kossakowski–Sudarshan–Lindblad prescription while exposing TNFR
|
|
131
|
+
semantics through ``ν_f`` and ``scale``.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
hamiltonian:
|
|
136
|
+
Optional coherent component. When ``None`` a null Hamiltonian is
|
|
137
|
+
assumed.
|
|
138
|
+
collapse_operators:
|
|
139
|
+
Iterable with the dissipative operators driving the contractive
|
|
140
|
+
semigroup. Each entry must be square with the same dimension as the
|
|
141
|
+
Hamiltonian. When ``None`` the generator reduces to the coherent part.
|
|
142
|
+
dim:
|
|
143
|
+
Explicit Hilbert-space dimension. Only required if neither
|
|
144
|
+
``hamiltonian`` nor ``collapse_operators`` are provided. When supplied,
|
|
145
|
+
it must match the dimension inferred from the Hamiltonian and collapse
|
|
146
|
+
operators.
|
|
147
|
+
nu_f, scale:
|
|
148
|
+
Structural frequency scaling applied uniformly to the final generator.
|
|
149
|
+
ensure_trace_preserving:
|
|
150
|
+
When ``True`` (default) the resulting superoperator is validated to
|
|
151
|
+
leave the identity invariant.
|
|
152
|
+
ensure_contractive:
|
|
153
|
+
When ``True`` (default) the spectrum is required to have non-positive
|
|
154
|
+
real parts within ``atol``.
|
|
155
|
+
atol:
|
|
156
|
+
Absolute tolerance used for Hermiticity, trace and spectral checks.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
operators = list(collapse_operators or [])
|
|
160
|
+
|
|
161
|
+
inferred_dim: int | None = dim
|
|
162
|
+
if hamiltonian is not None:
|
|
163
|
+
hermitian = _as_square_matrix(hamiltonian, label="hamiltonian")
|
|
164
|
+
inferred_dim = hermitian.shape[0]
|
|
165
|
+
elif operators:
|
|
166
|
+
inferred_dim = _as_square_matrix(operators[0], label="collapse operator[0]").shape[0]
|
|
167
|
+
|
|
168
|
+
if inferred_dim is None:
|
|
169
|
+
raise ValueError("dim must be supplied when no operators are provided.")
|
|
170
|
+
|
|
171
|
+
if inferred_dim <= 0:
|
|
172
|
+
raise ValueError("ΔNFR generators require a positive dimension.")
|
|
173
|
+
|
|
174
|
+
dimension = inferred_dim
|
|
175
|
+
|
|
176
|
+
if dim is not None and dim != dimension:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"Provided dim is inconsistent with the supplied operators: "
|
|
179
|
+
f"expected {dimension}, received {dim}."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if hamiltonian is None:
|
|
183
|
+
hermitian = np.zeros((dimension, dimension), dtype=np.complex128)
|
|
184
|
+
else:
|
|
185
|
+
hermitian = _as_square_matrix(hamiltonian, expected_dim=dimension, label="hamiltonian")
|
|
186
|
+
if not np.allclose(hermitian, hermitian.conj().T, atol=atol):
|
|
187
|
+
raise ValueError("Hamiltonian component must be Hermitian within tolerance.")
|
|
188
|
+
|
|
189
|
+
dissipators = [
|
|
190
|
+
_as_square_matrix(operator, expected_dim=dimension, label=f"collapse operator[{index}]")
|
|
191
|
+
for index, operator in enumerate(operators)
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
identity = np.eye(dimension, dtype=np.complex128)
|
|
195
|
+
liouvillian = -1j * (np.kron(identity, hermitian) - np.kron(hermitian.T, identity))
|
|
196
|
+
|
|
197
|
+
for operator in dissipators:
|
|
198
|
+
adjoint_product = operator.conj().T @ operator
|
|
199
|
+
liouvillian += np.kron(operator.conj(), operator)
|
|
200
|
+
liouvillian -= 0.5 * np.kron(identity, adjoint_product)
|
|
201
|
+
liouvillian -= 0.5 * np.kron(adjoint_product.T, identity)
|
|
202
|
+
|
|
203
|
+
liouvillian *= (nu_f * scale)
|
|
204
|
+
|
|
205
|
+
if ensure_trace_preserving:
|
|
206
|
+
identity_vec = identity.reshape(dimension * dimension, order="F")
|
|
207
|
+
left_residual = identity_vec.conj().T @ liouvillian
|
|
208
|
+
if not np.allclose(left_residual, np.zeros_like(left_residual), atol=10 * atol):
|
|
209
|
+
raise ValueError("Lindblad generator must preserve the trace of density operators.")
|
|
210
|
+
|
|
211
|
+
backend = get_backend()
|
|
212
|
+
liouvillian_backend = ensure_array(liouvillian, backend=backend)
|
|
213
|
+
|
|
214
|
+
if ensure_contractive:
|
|
215
|
+
eigenvalues_backend, _ = backend.eig(liouvillian_backend)
|
|
216
|
+
eigenvalues = ensure_numpy(eigenvalues_backend, backend=backend)
|
|
217
|
+
if np.max(eigenvalues.real) > atol:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
"Lindblad generator is not contractive: spectrum has positive real components."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return np.asarray(ensure_numpy(liouvillian_backend, backend=backend), dtype=np.complex128)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Structural metrics preserving TNFR coherence invariants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Sequence
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .operators import CoherenceOperator
|
|
10
|
+
|
|
11
|
+
__all__ = ["dcoh"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _as_coherent_vector(
|
|
15
|
+
state: Sequence[complex] | np.ndarray,
|
|
16
|
+
*,
|
|
17
|
+
dimension: int,
|
|
18
|
+
) -> np.ndarray:
|
|
19
|
+
"""Return a complex vector compatible with ``CoherenceOperator`` matrices."""
|
|
20
|
+
|
|
21
|
+
vector = np.asarray(state, dtype=np.complex128)
|
|
22
|
+
if vector.ndim != 1 or vector.shape[0] != dimension:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"State vector dimension mismatch: "
|
|
25
|
+
f"expected ({dimension},), received {vector.shape!r}."
|
|
26
|
+
)
|
|
27
|
+
return vector
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _normalise_vector(
|
|
31
|
+
vector: np.ndarray,
|
|
32
|
+
*,
|
|
33
|
+
atol: float,
|
|
34
|
+
label: str,
|
|
35
|
+
) -> np.ndarray:
|
|
36
|
+
norm = np.linalg.norm(vector)
|
|
37
|
+
if np.isclose(norm, 0.0, atol=atol):
|
|
38
|
+
raise ValueError(f"Cannot normalise null coherence state {label}.")
|
|
39
|
+
return vector / norm
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def dcoh(
|
|
43
|
+
psi1: Sequence[complex] | np.ndarray,
|
|
44
|
+
psi2: Sequence[complex] | np.ndarray,
|
|
45
|
+
operator: CoherenceOperator,
|
|
46
|
+
*,
|
|
47
|
+
normalise: bool = True,
|
|
48
|
+
atol: float = 1e-9,
|
|
49
|
+
) -> float:
|
|
50
|
+
"""Return the TNFR dissimilarity of coherence between ``psi1`` and ``psi2``.
|
|
51
|
+
|
|
52
|
+
The metric follows the canonical TNFR expectation contracts:
|
|
53
|
+
|
|
54
|
+
* States are converted to Hilbert-compatible complex vectors respecting the
|
|
55
|
+
``CoherenceOperator`` dimension, preserving the spectral phase space.
|
|
56
|
+
* Optional normalisation keeps overlap and expectations coherent with
|
|
57
|
+
unit-phase contracts, preventing coherence inflation.
|
|
58
|
+
* Expectation values ``⟨ψ|Ĉ|ψ⟩`` must remain strictly positive; null or
|
|
59
|
+
negative projections signal a collapse and therefore raise ``ValueError``.
|
|
60
|
+
|
|
61
|
+
Parameters mirror the runtime helpers so callers can rely on the same
|
|
62
|
+
tolerances. Numerical overflow is contained by bounding intermediate ratios
|
|
63
|
+
within ``[0, 1]`` up to ``atol`` before applying the Bures-style angle
|
|
64
|
+
``arccos(√ratio)``, ensuring the returned dissimilarity remains within the
|
|
65
|
+
TNFR coherence interval.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
dimension = operator.matrix.shape[0]
|
|
69
|
+
vector1 = _as_coherent_vector(psi1, dimension=dimension)
|
|
70
|
+
vector2 = _as_coherent_vector(psi2, dimension=dimension)
|
|
71
|
+
|
|
72
|
+
if normalise:
|
|
73
|
+
vector1_norm = _normalise_vector(vector1, atol=atol, label="ψ₁")
|
|
74
|
+
vector2_norm = _normalise_vector(vector2, atol=atol, label="ψ₂")
|
|
75
|
+
else:
|
|
76
|
+
vector1_norm = vector1
|
|
77
|
+
vector2_norm = vector2
|
|
78
|
+
|
|
79
|
+
weighted_vector2 = operator.matrix @ vector2_norm
|
|
80
|
+
if weighted_vector2.shape != vector2_norm.shape:
|
|
81
|
+
raise ValueError("Operator application distorted coherence dimensionality.")
|
|
82
|
+
|
|
83
|
+
cross = np.vdot(vector1_norm, weighted_vector2)
|
|
84
|
+
if not np.isfinite(cross):
|
|
85
|
+
raise ValueError("State overlap produced a non-finite value.")
|
|
86
|
+
|
|
87
|
+
expect1 = float(operator.expectation(vector1, normalise=normalise, atol=atol))
|
|
88
|
+
expect2 = float(operator.expectation(vector2, normalise=normalise, atol=atol))
|
|
89
|
+
|
|
90
|
+
for idx, value in enumerate((expect1, expect2), start=1):
|
|
91
|
+
if not np.isfinite(value):
|
|
92
|
+
raise ValueError(f"Coherence expectation diverged for state ψ{idx}.")
|
|
93
|
+
if value <= 0.0 or np.isclose(value, 0.0, atol=atol):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"Coherence expectation must remain strictly positive to"
|
|
96
|
+
f" preserve TNFR invariants (state ψ{idx})."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
denominator = expect1 * expect2
|
|
100
|
+
if not np.isfinite(denominator):
|
|
101
|
+
raise ValueError("Coherence expectations produced a non-finite product.")
|
|
102
|
+
if denominator <= 0.0 or np.isclose(denominator, 0.0, atol=atol):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"Product of coherence expectations must be strictly positive to"
|
|
105
|
+
" evaluate dissimilarity."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
ratio = (np.abs(cross) ** 2) / denominator
|
|
109
|
+
eps = max(np.finfo(float).eps * 10.0, atol)
|
|
110
|
+
if ratio < -eps:
|
|
111
|
+
raise ValueError("Overlap produced a negative coherence ratio.")
|
|
112
|
+
if ratio < 0.0:
|
|
113
|
+
ratio = 0.0
|
|
114
|
+
if ratio > 1.0 + eps:
|
|
115
|
+
raise ValueError("Coherence ratio exceeded unity beyond tolerance.")
|
|
116
|
+
if ratio > 1.0:
|
|
117
|
+
ratio = 1.0
|
|
118
|
+
|
|
119
|
+
return float(np.arccos(np.sqrt(ratio)))
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Spectral operators modelling coherence and frequency dynamics."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Sequence
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .backend import MathematicsBackend, ensure_array, ensure_numpy, get_backend
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover - typing imports only
|
|
12
|
+
import numpy.typing as npt
|
|
13
|
+
|
|
14
|
+
ComplexVector = npt.NDArray[np.complexfloating[np.float64, np.float64]]
|
|
15
|
+
ComplexMatrix = npt.NDArray[np.complexfloating[np.float64, np.float64]]
|
|
16
|
+
else: # pragma: no cover - runtime alias
|
|
17
|
+
ComplexVector = np.ndarray
|
|
18
|
+
ComplexMatrix = np.ndarray
|
|
19
|
+
|
|
20
|
+
__all__ = ["CoherenceOperator", "FrequencyOperator"]
|
|
21
|
+
|
|
22
|
+
DEFAULT_C_MIN: float = 0.1
|
|
23
|
+
_C_MIN_UNSET = object()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _as_complex_vector(
|
|
27
|
+
vector: Sequence[complex] | np.ndarray | Any,
|
|
28
|
+
*,
|
|
29
|
+
backend: MathematicsBackend,
|
|
30
|
+
) -> Any:
|
|
31
|
+
arr = ensure_array(vector, dtype=np.complex128, backend=backend)
|
|
32
|
+
if getattr(arr, "ndim", len(getattr(arr, "shape", ()))) != 1:
|
|
33
|
+
raise ValueError("Vector input must be one-dimensional.")
|
|
34
|
+
return arr
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _as_complex_matrix(
|
|
38
|
+
matrix: Sequence[Sequence[complex]] | np.ndarray | Any,
|
|
39
|
+
*,
|
|
40
|
+
backend: MathematicsBackend,
|
|
41
|
+
) -> Any:
|
|
42
|
+
arr = ensure_array(matrix, dtype=np.complex128, backend=backend)
|
|
43
|
+
shape = getattr(arr, "shape", None)
|
|
44
|
+
if shape is None or len(shape) != 2 or shape[0] != shape[1]:
|
|
45
|
+
raise ValueError("Operator matrix must be square.")
|
|
46
|
+
return arr
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _make_diagonal(values: Any, *, backend: MathematicsBackend) -> Any:
|
|
50
|
+
dim = int(getattr(values, "shape")[0])
|
|
51
|
+
identity = ensure_array(np.eye(dim, dtype=np.complex128), backend=backend)
|
|
52
|
+
return backend.einsum("i,ij->ij", values, identity)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True)
|
|
56
|
+
class CoherenceOperator:
|
|
57
|
+
"""Hermitian operator capturing coherence redistribution.
|
|
58
|
+
|
|
59
|
+
The operator encapsulates how a TNFR EPI redistributes coherence across
|
|
60
|
+
its spectral components. It supports construction either from an explicit
|
|
61
|
+
matrix expressed on the canonical basis or from a pre-computed list of
|
|
62
|
+
eigenvalues (interpreted as already diagonalised). The minimal eigenvalue
|
|
63
|
+
``c_min`` is tracked explicitly so structural stability thresholds are easy
|
|
64
|
+
to evaluate during simulations. The precedence for determining the stored
|
|
65
|
+
threshold is: an explicit ``c_min`` wins, otherwise the spectral floor
|
|
66
|
+
(minimum real eigenvalue) is used, with ``0.1`` acting as the canonical
|
|
67
|
+
fallback for callers that still wish to supply a fixed number.
|
|
68
|
+
|
|
69
|
+
When instantiated under an automatic differentiation backend (JAX, PyTorch)
|
|
70
|
+
the spectral decomposition remains differentiable provided the supplied
|
|
71
|
+
operator is non-defective. NumPy callers receive ``numpy.ndarray`` outputs
|
|
72
|
+
and all tolerance checks match the historical semantics.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
matrix: ComplexMatrix
|
|
76
|
+
eigenvalues: ComplexVector
|
|
77
|
+
c_min: float
|
|
78
|
+
backend: MathematicsBackend = field(init=False, repr=False)
|
|
79
|
+
_matrix_backend: Any = field(init=False, repr=False)
|
|
80
|
+
_eigenvalues_backend: Any = field(init=False, repr=False)
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
operator: Sequence[Sequence[complex]] | Sequence[complex] | np.ndarray | Any,
|
|
85
|
+
*,
|
|
86
|
+
c_min: float | object = _C_MIN_UNSET,
|
|
87
|
+
ensure_hermitian: bool = True,
|
|
88
|
+
atol: float = 1e-9,
|
|
89
|
+
backend: MathematicsBackend | None = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
resolved_backend = backend or get_backend()
|
|
92
|
+
operand = ensure_array(operator, dtype=np.complex128, backend=resolved_backend)
|
|
93
|
+
if getattr(operand, "ndim", len(getattr(operand, "shape", ()))) == 1:
|
|
94
|
+
eigvals_backend = _as_complex_vector(operand, backend=resolved_backend)
|
|
95
|
+
if ensure_hermitian:
|
|
96
|
+
imag = ensure_numpy(eigvals_backend.imag, backend=resolved_backend)
|
|
97
|
+
if not np.allclose(imag, 0.0, atol=atol):
|
|
98
|
+
raise ValueError("Hermitian operators require real eigenvalues.")
|
|
99
|
+
matrix_backend = _make_diagonal(eigvals_backend, backend=resolved_backend)
|
|
100
|
+
eigenvalues_backend = eigvals_backend
|
|
101
|
+
else:
|
|
102
|
+
matrix_backend = _as_complex_matrix(operand, backend=resolved_backend)
|
|
103
|
+
if ensure_hermitian and not self._check_hermitian(matrix_backend, atol=atol, backend=resolved_backend):
|
|
104
|
+
raise ValueError("Coherence operator must be Hermitian.")
|
|
105
|
+
if ensure_hermitian:
|
|
106
|
+
eigenvalues_backend, _ = resolved_backend.eigh(matrix_backend)
|
|
107
|
+
else:
|
|
108
|
+
eigenvalues_backend, _ = resolved_backend.eig(matrix_backend)
|
|
109
|
+
|
|
110
|
+
self.backend = resolved_backend
|
|
111
|
+
self._matrix_backend = matrix_backend
|
|
112
|
+
self._eigenvalues_backend = eigenvalues_backend
|
|
113
|
+
self.matrix = ensure_numpy(matrix_backend, backend=resolved_backend)
|
|
114
|
+
self.eigenvalues = ensure_numpy(eigenvalues_backend, backend=resolved_backend)
|
|
115
|
+
derived_c_min = float(np.min(self.eigenvalues.real))
|
|
116
|
+
if c_min is _C_MIN_UNSET:
|
|
117
|
+
self.c_min = derived_c_min
|
|
118
|
+
else:
|
|
119
|
+
self.c_min = float(c_min)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _check_hermitian(
|
|
123
|
+
matrix: Any,
|
|
124
|
+
*,
|
|
125
|
+
atol: float = 1e-9,
|
|
126
|
+
backend: MathematicsBackend,
|
|
127
|
+
) -> bool:
|
|
128
|
+
matrix_np = ensure_numpy(matrix, backend=backend)
|
|
129
|
+
return bool(np.allclose(matrix_np, matrix_np.conj().T, atol=atol))
|
|
130
|
+
|
|
131
|
+
def is_hermitian(self, *, atol: float = 1e-9) -> bool:
|
|
132
|
+
"""Return ``True`` when the operator matches its adjoint."""
|
|
133
|
+
|
|
134
|
+
return self._check_hermitian(self._matrix_backend, atol=atol, backend=self.backend)
|
|
135
|
+
|
|
136
|
+
def is_positive_semidefinite(self, *, atol: float = 1e-9) -> bool:
|
|
137
|
+
"""Check that all eigenvalues are non-negative within ``atol``."""
|
|
138
|
+
|
|
139
|
+
return bool(np.all(self.eigenvalues.real >= -atol))
|
|
140
|
+
|
|
141
|
+
def spectrum(self) -> ComplexVector:
|
|
142
|
+
"""Return the complex eigenvalue spectrum."""
|
|
143
|
+
|
|
144
|
+
return np.asarray(self.eigenvalues, dtype=np.complex128)
|
|
145
|
+
|
|
146
|
+
def spectral_radius(self) -> float:
|
|
147
|
+
"""Return the largest magnitude eigenvalue (spectral radius)."""
|
|
148
|
+
|
|
149
|
+
return float(np.max(np.abs(self.eigenvalues)))
|
|
150
|
+
|
|
151
|
+
def spectral_bandwidth(self) -> float:
|
|
152
|
+
"""Return the real bandwidth ``max(λ) - min(λ)``."""
|
|
153
|
+
|
|
154
|
+
eigvals = self.eigenvalues.real
|
|
155
|
+
return float(np.max(eigvals) - np.min(eigvals))
|
|
156
|
+
|
|
157
|
+
def expectation(
|
|
158
|
+
self,
|
|
159
|
+
state: Sequence[complex] | np.ndarray,
|
|
160
|
+
*,
|
|
161
|
+
normalise: bool = True,
|
|
162
|
+
atol: float = 1e-9,
|
|
163
|
+
) -> float:
|
|
164
|
+
vector_backend = _as_complex_vector(state, backend=self.backend)
|
|
165
|
+
if vector_backend.shape != (self.matrix.shape[0],):
|
|
166
|
+
raise ValueError("State vector dimension mismatch with operator.")
|
|
167
|
+
working = vector_backend
|
|
168
|
+
if normalise:
|
|
169
|
+
norm_value = ensure_numpy(self.backend.norm(working), backend=self.backend)
|
|
170
|
+
norm = float(norm_value)
|
|
171
|
+
if np.isclose(norm, 0.0):
|
|
172
|
+
raise ValueError("Cannot normalise a null state vector.")
|
|
173
|
+
working = working / norm
|
|
174
|
+
column = working[..., None]
|
|
175
|
+
bra = self.backend.conjugate_transpose(column)
|
|
176
|
+
evolved = self.backend.matmul(self._matrix_backend, column)
|
|
177
|
+
expectation_backend = self.backend.matmul(bra, evolved)
|
|
178
|
+
expectation = ensure_numpy(expectation_backend, backend=self.backend)
|
|
179
|
+
expectation_scalar = complex(np.asarray(expectation).reshape(()))
|
|
180
|
+
if abs(expectation_scalar.imag) > atol:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
"Expectation value carries an imaginary component beyond tolerance."
|
|
183
|
+
)
|
|
184
|
+
eps = np.finfo(float).eps
|
|
185
|
+
tol = max(1000.0, float(atol / eps)) if atol > 0 else 1000.0
|
|
186
|
+
real_expectation = np.real_if_close(expectation_scalar, tol=tol)
|
|
187
|
+
if np.iscomplexobj(real_expectation):
|
|
188
|
+
raise ValueError("Expectation remained complex after coercion.")
|
|
189
|
+
return float(real_expectation)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class FrequencyOperator(CoherenceOperator):
|
|
193
|
+
"""Operator encoding the structural frequency distribution.
|
|
194
|
+
|
|
195
|
+
The frequency operator reuses the coherence machinery but enforces a real
|
|
196
|
+
spectrum representing the structural hertz (νf) each mode contributes. Its
|
|
197
|
+
helpers therefore constrain outputs to the real axis and expose projections
|
|
198
|
+
suited for telemetry collection.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self,
|
|
203
|
+
operator: Sequence[Sequence[complex]] | Sequence[complex] | np.ndarray | Any,
|
|
204
|
+
*,
|
|
205
|
+
ensure_hermitian: bool = True,
|
|
206
|
+
atol: float = 1e-9,
|
|
207
|
+
backend: MathematicsBackend | None = None,
|
|
208
|
+
) -> None:
|
|
209
|
+
super().__init__(
|
|
210
|
+
operator,
|
|
211
|
+
ensure_hermitian=ensure_hermitian,
|
|
212
|
+
atol=atol,
|
|
213
|
+
backend=backend,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def spectrum(self) -> np.ndarray:
|
|
217
|
+
"""Return the real-valued structural frequency spectrum."""
|
|
218
|
+
|
|
219
|
+
return np.asarray(self.eigenvalues.real, dtype=float)
|
|
220
|
+
|
|
221
|
+
def is_positive_semidefinite(self, *, atol: float = 1e-9) -> bool:
|
|
222
|
+
"""Frequency spectra must be non-negative to preserve νf semantics."""
|
|
223
|
+
|
|
224
|
+
return bool(np.all(self.spectrum() >= -atol))
|
|
225
|
+
|
|
226
|
+
def project_frequency(
|
|
227
|
+
self,
|
|
228
|
+
state: Sequence[complex] | np.ndarray,
|
|
229
|
+
*,
|
|
230
|
+
normalise: bool = True,
|
|
231
|
+
atol: float = 1e-9,
|
|
232
|
+
) -> float:
|
|
233
|
+
return self.expectation(state, normalise=normalise, atol=atol)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Factory helpers to assemble TNFR coherence and frequency operators."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from .backend import ensure_array, ensure_numpy, get_backend
|
|
7
|
+
from .operators import CoherenceOperator, FrequencyOperator
|
|
8
|
+
|
|
9
|
+
__all__ = ["make_coherence_operator", "make_frequency_operator"]
|
|
10
|
+
|
|
11
|
+
_ATOL = 1e-9
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _validate_dimension(dim: int) -> int:
|
|
15
|
+
if int(dim) != dim:
|
|
16
|
+
raise ValueError("Operator dimension must be an integer.")
|
|
17
|
+
if dim <= 0:
|
|
18
|
+
raise ValueError("Operator dimension must be strictly positive.")
|
|
19
|
+
return int(dim)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_coherence_operator(
|
|
23
|
+
dim: int,
|
|
24
|
+
*,
|
|
25
|
+
spectrum: np.ndarray | None = None,
|
|
26
|
+
c_min: float = 0.1,
|
|
27
|
+
) -> CoherenceOperator:
|
|
28
|
+
"""Return a Hermitian positive semidefinite :class:`CoherenceOperator`."""
|
|
29
|
+
|
|
30
|
+
dimension = _validate_dimension(dim)
|
|
31
|
+
if not np.isfinite(c_min):
|
|
32
|
+
raise ValueError("Coherence threshold ``c_min`` must be finite.")
|
|
33
|
+
|
|
34
|
+
backend = get_backend()
|
|
35
|
+
|
|
36
|
+
if spectrum is None:
|
|
37
|
+
eigenvalues_backend = ensure_array(np.full(dimension, float(c_min), dtype=float), backend=backend)
|
|
38
|
+
else:
|
|
39
|
+
eigenvalues_backend = ensure_array(spectrum, dtype=np.complex128, backend=backend)
|
|
40
|
+
eigenvalues_np = ensure_numpy(eigenvalues_backend, backend=backend)
|
|
41
|
+
if eigenvalues_np.ndim != 1:
|
|
42
|
+
raise ValueError("Coherence spectrum must be one-dimensional.")
|
|
43
|
+
if eigenvalues_np.shape[0] != dimension:
|
|
44
|
+
raise ValueError("Coherence spectrum size must match operator dimension.")
|
|
45
|
+
if np.any(np.abs(eigenvalues_np.imag) > _ATOL):
|
|
46
|
+
raise ValueError("Coherence spectrum must be real-valued within tolerance.")
|
|
47
|
+
eigenvalues_backend = ensure_array(eigenvalues_np.real.astype(float, copy=False), backend=backend)
|
|
48
|
+
|
|
49
|
+
operator = CoherenceOperator(eigenvalues_backend, c_min=c_min, backend=backend)
|
|
50
|
+
if not operator.is_hermitian(atol=_ATOL):
|
|
51
|
+
raise ValueError("Coherence operator must be Hermitian.")
|
|
52
|
+
if not operator.is_positive_semidefinite(atol=_ATOL):
|
|
53
|
+
raise ValueError("Coherence operator must be positive semidefinite.")
|
|
54
|
+
return operator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_frequency_operator(matrix: np.ndarray) -> FrequencyOperator:
|
|
58
|
+
"""Return a Hermitian PSD :class:`FrequencyOperator` from ``matrix``."""
|
|
59
|
+
|
|
60
|
+
backend = get_backend()
|
|
61
|
+
array_backend = ensure_array(matrix, dtype=np.complex128, backend=backend)
|
|
62
|
+
array_np = ensure_numpy(array_backend, backend=backend)
|
|
63
|
+
if array_np.ndim != 2 or array_np.shape[0] != array_np.shape[1]:
|
|
64
|
+
raise ValueError("Frequency operator matrix must be square.")
|
|
65
|
+
if not np.allclose(array_np, array_np.conj().T, atol=_ATOL):
|
|
66
|
+
raise ValueError("Frequency operator must be Hermitian within tolerance.")
|
|
67
|
+
|
|
68
|
+
operator = FrequencyOperator(array_backend, backend=backend)
|
|
69
|
+
if not operator.is_positive_semidefinite(atol=_ATOL):
|
|
70
|
+
raise ValueError("Frequency operator must be positive semidefinite.")
|
|
71
|
+
return operator
|