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.

Files changed (176) hide show
  1. tnfr/__init__.py +50 -5
  2. tnfr/__init__.pyi +0 -7
  3. tnfr/_compat.py +0 -1
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +44 -2
  6. tnfr/alias.py +14 -13
  7. tnfr/alias.pyi +5 -37
  8. tnfr/cache.py +9 -729
  9. tnfr/cache.pyi +8 -224
  10. tnfr/callback_utils.py +16 -31
  11. tnfr/callback_utils.pyi +3 -29
  12. tnfr/cli/__init__.py +17 -11
  13. tnfr/cli/__init__.pyi +0 -21
  14. tnfr/cli/arguments.py +175 -14
  15. tnfr/cli/arguments.pyi +5 -11
  16. tnfr/cli/execution.py +434 -48
  17. tnfr/cli/execution.pyi +14 -24
  18. tnfr/cli/utils.py +20 -3
  19. tnfr/cli/utils.pyi +5 -5
  20. tnfr/config/__init__.py +2 -1
  21. tnfr/config/__init__.pyi +2 -0
  22. tnfr/config/feature_flags.py +83 -0
  23. tnfr/config/init.py +1 -1
  24. tnfr/config/operator_names.py +1 -14
  25. tnfr/config/presets.py +6 -26
  26. tnfr/constants/__init__.py +10 -13
  27. tnfr/constants/__init__.pyi +10 -22
  28. tnfr/constants/aliases.py +31 -0
  29. tnfr/constants/core.py +4 -3
  30. tnfr/constants/init.py +1 -1
  31. tnfr/constants/metric.py +3 -3
  32. tnfr/dynamics/__init__.py +64 -10
  33. tnfr/dynamics/__init__.pyi +3 -4
  34. tnfr/dynamics/adaptation.py +79 -13
  35. tnfr/dynamics/aliases.py +10 -9
  36. tnfr/dynamics/coordination.py +77 -35
  37. tnfr/dynamics/dnfr.py +575 -274
  38. tnfr/dynamics/dnfr.pyi +1 -10
  39. tnfr/dynamics/integrators.py +47 -33
  40. tnfr/dynamics/integrators.pyi +0 -1
  41. tnfr/dynamics/runtime.py +489 -129
  42. tnfr/dynamics/sampling.py +2 -0
  43. tnfr/dynamics/selectors.py +101 -62
  44. tnfr/execution.py +15 -8
  45. tnfr/execution.pyi +5 -25
  46. tnfr/flatten.py +7 -3
  47. tnfr/flatten.pyi +1 -8
  48. tnfr/gamma.py +22 -26
  49. tnfr/gamma.pyi +0 -6
  50. tnfr/glyph_history.py +37 -26
  51. tnfr/glyph_history.pyi +1 -19
  52. tnfr/glyph_runtime.py +16 -0
  53. tnfr/glyph_runtime.pyi +9 -0
  54. tnfr/immutable.py +20 -15
  55. tnfr/immutable.pyi +4 -7
  56. tnfr/initialization.py +5 -7
  57. tnfr/initialization.pyi +1 -9
  58. tnfr/io.py +6 -305
  59. tnfr/io.pyi +13 -8
  60. tnfr/mathematics/__init__.py +81 -0
  61. tnfr/mathematics/backend.py +426 -0
  62. tnfr/mathematics/dynamics.py +398 -0
  63. tnfr/mathematics/epi.py +254 -0
  64. tnfr/mathematics/generators.py +222 -0
  65. tnfr/mathematics/metrics.py +119 -0
  66. tnfr/mathematics/operators.py +233 -0
  67. tnfr/mathematics/operators_factory.py +71 -0
  68. tnfr/mathematics/projection.py +78 -0
  69. tnfr/mathematics/runtime.py +173 -0
  70. tnfr/mathematics/spaces.py +247 -0
  71. tnfr/mathematics/transforms.py +292 -0
  72. tnfr/metrics/__init__.py +10 -10
  73. tnfr/metrics/coherence.py +123 -94
  74. tnfr/metrics/common.py +22 -13
  75. tnfr/metrics/common.pyi +42 -11
  76. tnfr/metrics/core.py +72 -14
  77. tnfr/metrics/diagnosis.py +48 -57
  78. tnfr/metrics/diagnosis.pyi +3 -7
  79. tnfr/metrics/export.py +3 -5
  80. tnfr/metrics/glyph_timing.py +41 -31
  81. tnfr/metrics/reporting.py +13 -6
  82. tnfr/metrics/sense_index.py +884 -114
  83. tnfr/metrics/trig.py +167 -11
  84. tnfr/metrics/trig.pyi +1 -0
  85. tnfr/metrics/trig_cache.py +112 -15
  86. tnfr/node.py +400 -17
  87. tnfr/node.pyi +55 -38
  88. tnfr/observers.py +111 -8
  89. tnfr/observers.pyi +0 -15
  90. tnfr/ontosim.py +9 -6
  91. tnfr/ontosim.pyi +0 -5
  92. tnfr/operators/__init__.py +529 -42
  93. tnfr/operators/__init__.pyi +14 -0
  94. tnfr/operators/definitions.py +350 -18
  95. tnfr/operators/definitions.pyi +0 -14
  96. tnfr/operators/grammar.py +760 -0
  97. tnfr/operators/jitter.py +28 -22
  98. tnfr/operators/registry.py +7 -12
  99. tnfr/operators/registry.pyi +0 -2
  100. tnfr/operators/remesh.py +38 -61
  101. tnfr/rng.py +17 -300
  102. tnfr/schemas/__init__.py +8 -0
  103. tnfr/schemas/grammar.json +94 -0
  104. tnfr/selector.py +3 -4
  105. tnfr/selector.pyi +1 -1
  106. tnfr/sense.py +22 -24
  107. tnfr/sense.pyi +0 -7
  108. tnfr/structural.py +504 -21
  109. tnfr/structural.pyi +41 -18
  110. tnfr/telemetry/__init__.py +23 -1
  111. tnfr/telemetry/cache_metrics.py +226 -0
  112. tnfr/telemetry/nu_f.py +423 -0
  113. tnfr/telemetry/nu_f.pyi +123 -0
  114. tnfr/tokens.py +1 -4
  115. tnfr/tokens.pyi +1 -6
  116. tnfr/trace.py +20 -53
  117. tnfr/trace.pyi +9 -37
  118. tnfr/types.py +244 -15
  119. tnfr/types.pyi +200 -14
  120. tnfr/units.py +69 -0
  121. tnfr/units.pyi +16 -0
  122. tnfr/utils/__init__.py +107 -48
  123. tnfr/utils/__init__.pyi +80 -11
  124. tnfr/utils/cache.py +1705 -65
  125. tnfr/utils/cache.pyi +370 -58
  126. tnfr/utils/chunks.py +104 -0
  127. tnfr/utils/chunks.pyi +21 -0
  128. tnfr/utils/data.py +95 -5
  129. tnfr/utils/data.pyi +8 -17
  130. tnfr/utils/graph.py +2 -4
  131. tnfr/utils/init.py +31 -7
  132. tnfr/utils/init.pyi +4 -11
  133. tnfr/utils/io.py +313 -14
  134. tnfr/{helpers → utils}/numeric.py +50 -24
  135. tnfr/utils/numeric.pyi +21 -0
  136. tnfr/validation/__init__.py +92 -4
  137. tnfr/validation/__init__.pyi +77 -17
  138. tnfr/validation/compatibility.py +79 -43
  139. tnfr/validation/compatibility.pyi +4 -6
  140. tnfr/validation/grammar.py +55 -133
  141. tnfr/validation/grammar.pyi +37 -8
  142. tnfr/validation/graph.py +138 -0
  143. tnfr/validation/graph.pyi +17 -0
  144. tnfr/validation/rules.py +161 -74
  145. tnfr/validation/rules.pyi +55 -18
  146. tnfr/validation/runtime.py +263 -0
  147. tnfr/validation/runtime.pyi +31 -0
  148. tnfr/validation/soft_filters.py +170 -0
  149. tnfr/validation/soft_filters.pyi +37 -0
  150. tnfr/validation/spectral.py +159 -0
  151. tnfr/validation/spectral.pyi +46 -0
  152. tnfr/validation/syntax.py +28 -139
  153. tnfr/validation/syntax.pyi +7 -4
  154. tnfr/validation/window.py +39 -0
  155. tnfr/validation/window.pyi +1 -0
  156. tnfr/viz/__init__.py +9 -0
  157. tnfr/viz/matplotlib.py +246 -0
  158. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
  159. tnfr-7.0.0.dist-info/RECORD +185 -0
  160. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  161. tnfr/constants_glyphs.py +0 -16
  162. tnfr/constants_glyphs.pyi +0 -12
  163. tnfr/grammar.py +0 -25
  164. tnfr/grammar.pyi +0 -13
  165. tnfr/helpers/__init__.py +0 -151
  166. tnfr/helpers/__init__.pyi +0 -66
  167. tnfr/helpers/numeric.pyi +0 -12
  168. tnfr/presets.py +0 -15
  169. tnfr/presets.pyi +0 -7
  170. tnfr/utils/io.pyi +0 -10
  171. tnfr/utils/validators.py +0 -130
  172. tnfr/utils/validators.pyi +0 -19
  173. tnfr-6.0.0.dist-info/RECORD +0 -157
  174. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  175. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  176. {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