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
tnfr/node.py CHANGED
@@ -1,31 +1,63 @@
1
1
  """Node utilities and structures for TNFR graphs."""
2
2
 
3
3
  from __future__ import annotations
4
+
5
+ import copy
6
+ import math
7
+ from collections.abc import Hashable
8
+ from dataclasses import dataclass
4
9
  from typing import (
5
10
  Any,
6
11
  Callable,
7
12
  Iterable,
13
+ Mapping,
8
14
  MutableMapping,
9
15
  Optional,
10
16
  Protocol,
17
+ Sequence,
11
18
  SupportsFloat,
12
19
  TypeVar,
13
20
  )
14
- from collections.abc import Hashable
15
- import math
16
- from dataclasses import dataclass
17
21
 
18
- from .constants import get_aliases
22
+ import numpy as np
23
+
19
24
  from .alias import (
20
25
  get_attr,
21
- get_theta_attr,
22
26
  get_attr_str,
27
+ get_theta_attr,
23
28
  set_attr,
24
29
  set_attr_str,
25
- set_vf,
30
+ set_attr_generic,
26
31
  set_dnfr,
27
32
  set_theta,
33
+ set_vf,
28
34
  )
35
+ from .config import context_flags, get_flags
36
+ from .constants.aliases import (
37
+ ALIAS_D2EPI,
38
+ ALIAS_DNFR,
39
+ ALIAS_EPI,
40
+ ALIAS_EPI_KIND,
41
+ ALIAS_SI,
42
+ ALIAS_THETA,
43
+ ALIAS_VF,
44
+ )
45
+ from .mathematics import (
46
+ BasicStateProjector,
47
+ CoherenceOperator,
48
+ FrequencyOperator,
49
+ HilbertSpace,
50
+ NFRValidator,
51
+ StateProjector,
52
+ )
53
+ from .mathematics.operators_factory import make_coherence_operator, make_frequency_operator
54
+ from .mathematics.runtime import (
55
+ coherence as runtime_coherence,
56
+ frequency_positive as runtime_frequency_positive,
57
+ normalized as runtime_normalized,
58
+ stable_unitary as runtime_stable_unitary,
59
+ )
60
+ from .locking import get_lock
29
61
  from .types import (
30
62
  CouplingWeight,
31
63
  DeltaNFR,
@@ -36,28 +68,26 @@ from .types import (
36
68
  SenseIndex,
37
69
  StructuralFrequency,
38
70
  TNFRGraph,
71
+ ZERO_BEPI_STORAGE,
72
+ ensure_bepi,
73
+ serialize_bepi,
39
74
  )
40
75
  from .utils import (
41
76
  cached_node_list,
42
77
  ensure_node_offset_map,
78
+ get_logger,
43
79
  increment_edge_version,
44
80
  supports_add_edge,
45
81
  )
46
- from .locking import get_lock
47
-
48
- ALIAS_EPI = get_aliases("EPI")
49
- ALIAS_VF = get_aliases("VF")
50
- ALIAS_THETA = get_aliases("THETA")
51
- ALIAS_SI = get_aliases("SI")
52
- ALIAS_EPI_KIND = get_aliases("EPI_KIND")
53
- ALIAS_DNFR = get_aliases("DNFR")
54
- ALIAS_D2EPI = get_aliases("D2EPI")
55
82
 
56
83
  T = TypeVar("T")
57
84
 
58
85
  __all__ = ("NodeNX", "NodeProtocol", "add_edge")
59
86
 
60
87
 
88
+ LOGGER = get_logger(__name__)
89
+
90
+
61
91
  @dataclass(frozen=True)
62
92
  class AttrSpec:
63
93
  """Configuration required to expose a ``networkx`` node attribute.
@@ -93,11 +123,43 @@ class AttrSpec:
93
123
  return property(fget, fset)
94
124
 
95
125
 
126
+ # Canonical adapters for BEPI storage ------------------------------------
127
+
128
+
129
+ def _epi_to_python(value: Any) -> EPIValue:
130
+ if value is None:
131
+ raise ValueError("EPI attribute is required for BEPI nodes")
132
+ return ensure_bepi(value)
133
+
134
+
135
+ def _epi_to_storage(value: Any) -> Mapping[str, tuple[complex, ...] | tuple[float, ...]]:
136
+ return serialize_bepi(value)
137
+
138
+
139
+ def _get_bepi_attr(
140
+ mapping: Mapping[str, Any], aliases: tuple[str, ...], default: Any
141
+ ) -> Any:
142
+ return get_attr(mapping, aliases, default, conv=lambda obj: obj)
143
+
144
+
145
+ def _set_bepi_attr(
146
+ mapping: MutableMapping[str, Any], aliases: tuple[str, ...], value: Any
147
+ ) -> Mapping[str, tuple[complex, ...] | tuple[float, ...]]:
148
+ return set_attr_generic(mapping, aliases, value, conv=lambda obj: obj)
149
+
150
+
96
151
  # Mapping of NodeNX attribute specifications used to generate property
97
152
  # descriptors. Each entry defines the keyword arguments passed to
98
153
  # ``AttrSpec.build_property`` for a given attribute name.
99
154
  ATTR_SPECS: dict[str, AttrSpec] = {
100
- "EPI": AttrSpec(aliases=ALIAS_EPI),
155
+ "EPI": AttrSpec(
156
+ aliases=ALIAS_EPI,
157
+ default=ZERO_BEPI_STORAGE,
158
+ getter=_get_bepi_attr,
159
+ to_python=_epi_to_python,
160
+ to_storage=_epi_to_storage,
161
+ setter=_set_bepi_attr,
162
+ ),
101
163
  "vf": AttrSpec(aliases=ALIAS_VF, setter=set_vf, use_graph_setter=True),
102
164
  "theta": AttrSpec(
103
165
  aliases=ALIAS_THETA,
@@ -178,12 +240,18 @@ class NodeProtocol(Protocol):
178
240
  graph: MutableMapping[str, Any]
179
241
 
180
242
  def neighbors(self) -> Iterable[NodeProtocol | Hashable]:
243
+ """Iterate structural neighbours coupled to this node."""
244
+
181
245
  ...
182
246
 
183
247
  def _glyph_storage(self) -> MutableMapping[str, object]:
248
+ """Return the mutable mapping storing glyph metadata."""
249
+
184
250
  ...
185
251
 
186
252
  def has_edge(self, other: "NodeProtocol") -> bool:
253
+ """Return ``True`` when an edge connects this node to ``other``."""
254
+
187
255
  ...
188
256
 
189
257
  def add_edge(
@@ -193,12 +261,18 @@ class NodeProtocol(Protocol):
193
261
  *,
194
262
  overwrite: bool = False,
195
263
  ) -> None:
264
+ """Couple ``other`` using ``weight`` optionally replacing existing links."""
265
+
196
266
  ...
197
267
 
198
268
  def offset(self) -> int:
269
+ """Return the node offset index within the canonical ordering."""
270
+
199
271
  ...
200
272
 
201
273
  def all_nodes(self) -> Iterable[NodeProtocol]:
274
+ """Iterate all nodes of the attached graph as :class:`NodeProtocol` objects."""
275
+
202
276
  ...
203
277
 
204
278
 
@@ -216,10 +290,116 @@ class NodeNX(NodeProtocol):
216
290
  dnfr: DeltaNFR = ATTR_SPECS["dnfr"].build_property()
217
291
  d2EPI: SecondDerivativeEPI = ATTR_SPECS["d2EPI"].build_property()
218
292
 
219
- def __init__(self, G: TNFRGraph, n: NodeId) -> None:
293
+ @staticmethod
294
+ def _prepare_coherence_operator(
295
+ operator: CoherenceOperator | None,
296
+ *,
297
+ dim: int | None = None,
298
+ spectrum: Sequence[float] | np.ndarray | None = None,
299
+ c_min: float | None = None,
300
+ ) -> CoherenceOperator | None:
301
+ if operator is not None:
302
+ return operator
303
+
304
+ spectrum_array: np.ndarray | None
305
+ if spectrum is None:
306
+ spectrum_array = None
307
+ else:
308
+ spectrum_array = np.asarray(spectrum, dtype=np.complex128)
309
+ if spectrum_array.ndim != 1:
310
+ raise ValueError("Coherence spectrum must be one-dimensional.")
311
+
312
+ effective_dim = dim
313
+ if spectrum_array is not None:
314
+ spectrum_length = spectrum_array.shape[0]
315
+ if effective_dim is None:
316
+ effective_dim = int(spectrum_length)
317
+ elif spectrum_length != int(effective_dim):
318
+ raise ValueError("Coherence spectrum size mismatch with requested dimension.")
319
+
320
+ if effective_dim is None:
321
+ return None
322
+
323
+ kwargs: dict[str, Any] = {}
324
+ if spectrum_array is not None:
325
+ kwargs["spectrum"] = spectrum_array
326
+ if c_min is not None:
327
+ kwargs["c_min"] = float(c_min)
328
+ return make_coherence_operator(int(effective_dim), **kwargs)
329
+
330
+ @staticmethod
331
+ def _prepare_frequency_operator(
332
+ operator: FrequencyOperator | None,
333
+ *,
334
+ matrix: Sequence[Sequence[complex]] | np.ndarray | None = None,
335
+ ) -> FrequencyOperator | None:
336
+ if operator is not None:
337
+ return operator
338
+ if matrix is None:
339
+ return None
340
+ return make_frequency_operator(np.asarray(matrix, dtype=np.complex128))
341
+
342
+ def __init__(
343
+ self,
344
+ G: TNFRGraph,
345
+ n: NodeId,
346
+ *,
347
+ state_projector: StateProjector | None = None,
348
+ enable_math_validation: Optional[bool] = None,
349
+ hilbert_space: HilbertSpace | None = None,
350
+ coherence_operator: CoherenceOperator | None = None,
351
+ coherence_dim: int | None = None,
352
+ coherence_spectrum: Sequence[float] | np.ndarray | None = None,
353
+ coherence_c_min: float | None = None,
354
+ frequency_operator: FrequencyOperator | None = None,
355
+ frequency_matrix: Sequence[Sequence[complex]] | np.ndarray | None = None,
356
+ coherence_threshold: float | None = None,
357
+ validator: NFRValidator | None = None,
358
+ rng: np.random.Generator | None = None,
359
+ ) -> None:
220
360
  self.G: TNFRGraph = G
221
361
  self.n: NodeId = n
222
362
  self.graph: MutableMapping[str, Any] = G.graph
363
+ self.state_projector: StateProjector = state_projector or BasicStateProjector()
364
+ self._math_validation_override: Optional[bool] = enable_math_validation
365
+ if enable_math_validation is None:
366
+ effective_validation = get_flags().enable_math_validation
367
+ else:
368
+ effective_validation = bool(enable_math_validation)
369
+ self.enable_math_validation: bool = effective_validation
370
+ default_dimension = (
371
+ G.number_of_nodes() if hasattr(G, "number_of_nodes") else len(tuple(G.nodes))
372
+ )
373
+ default_dimension = max(1, int(default_dimension))
374
+ self.hilbert_space: HilbertSpace = hilbert_space or HilbertSpace(default_dimension)
375
+ if coherence_operator is not None and (
376
+ coherence_dim is not None
377
+ or coherence_spectrum is not None
378
+ or coherence_c_min is not None
379
+ ):
380
+ raise ValueError(
381
+ "Provide either a coherence operator or factory parameters, not both."
382
+ )
383
+ if frequency_operator is not None and frequency_matrix is not None:
384
+ raise ValueError(
385
+ "Provide either a frequency operator or frequency matrix, not both."
386
+ )
387
+
388
+ self.coherence_operator: CoherenceOperator | None = self._prepare_coherence_operator(
389
+ coherence_operator,
390
+ dim=coherence_dim,
391
+ spectrum=coherence_spectrum,
392
+ c_min=coherence_c_min,
393
+ )
394
+ self.frequency_operator: FrequencyOperator | None = self._prepare_frequency_operator(
395
+ frequency_operator,
396
+ matrix=frequency_matrix,
397
+ )
398
+ self.coherence_threshold: float | None = (
399
+ float(coherence_threshold) if coherence_threshold is not None else None
400
+ )
401
+ self.validator: NFRValidator | None = validator
402
+ self.rng: np.random.Generator | None = rng
223
403
  G.graph.setdefault("_node_cache", {})[n] = self
224
404
 
225
405
  def _glyph_storage(self) -> MutableMapping[str, Any]:
@@ -245,6 +425,8 @@ class NodeNX(NodeProtocol):
245
425
  return self.G.neighbors(self.n)
246
426
 
247
427
  def has_edge(self, other: NodeProtocol) -> bool:
428
+ """Return ``True`` when an edge connects this node to ``other``."""
429
+
248
430
  if isinstance(other, NodeNX):
249
431
  return self.G.has_edge(self.n, other.n)
250
432
  raise NotImplementedError
@@ -256,6 +438,8 @@ class NodeNX(NodeProtocol):
256
438
  *,
257
439
  overwrite: bool = False,
258
440
  ) -> None:
441
+ """Couple ``other`` using ``weight`` optionally replacing existing links."""
442
+
259
443
  if isinstance(other, NodeNX):
260
444
  add_edge(
261
445
  self.G,
@@ -268,13 +452,212 @@ class NodeNX(NodeProtocol):
268
452
  raise NotImplementedError
269
453
 
270
454
  def offset(self) -> int:
455
+ """Return the cached node offset within the canonical ordering."""
456
+
271
457
  mapping = ensure_node_offset_map(self.G)
272
458
  return mapping.get(self.n, 0)
273
459
 
274
460
  def all_nodes(self) -> Iterable[NodeProtocol]:
461
+ """Iterate all nodes of ``self.G`` as ``NodeNX`` adapters."""
462
+
275
463
  override = self.graph.get("_all_nodes")
276
464
  if override is not None:
277
465
  return override
278
466
 
279
467
  nodes = cached_node_list(self.G)
280
468
  return tuple(NodeNX.from_graph(self.G, v) for v in nodes)
469
+
470
+ def run_sequence_with_validation(
471
+ self,
472
+ ops: Iterable[Callable[[TNFRGraph, NodeId], None]],
473
+ *,
474
+ projector: StateProjector | None = None,
475
+ hilbert_space: HilbertSpace | None = None,
476
+ coherence_operator: CoherenceOperator | None = None,
477
+ coherence_dim: int | None = None,
478
+ coherence_spectrum: Sequence[float] | np.ndarray | None = None,
479
+ coherence_c_min: float | None = None,
480
+ coherence_threshold: float | None = None,
481
+ frequency_operator: FrequencyOperator | None = None,
482
+ frequency_matrix: Sequence[Sequence[complex]] | np.ndarray | None = None,
483
+ validator: NFRValidator | None = None,
484
+ enforce_frequency_positivity: bool | None = None,
485
+ enable_validation: bool | None = None,
486
+ rng: np.random.Generator | None = None,
487
+ log_metrics: bool = False,
488
+ ) -> dict[str, Any]:
489
+ """Run ``ops`` then return pre/post metrics with optional validation."""
490
+
491
+ from .structural import run_sequence as structural_run_sequence
492
+
493
+ projector = projector or self.state_projector
494
+ hilbert = hilbert_space or self.hilbert_space
495
+
496
+ effective_coherence = (
497
+ self._prepare_coherence_operator(
498
+ coherence_operator,
499
+ dim=coherence_dim,
500
+ spectrum=coherence_spectrum,
501
+ c_min=(
502
+ coherence_c_min
503
+ if coherence_c_min is not None
504
+ else (
505
+ self.coherence_operator.c_min
506
+ if self.coherence_operator is not None
507
+ else None
508
+ )
509
+ ),
510
+ )
511
+ if any(
512
+ parameter is not None
513
+ for parameter in (
514
+ coherence_operator,
515
+ coherence_dim,
516
+ coherence_spectrum,
517
+ coherence_c_min,
518
+ )
519
+ )
520
+ else self.coherence_operator
521
+ )
522
+ effective_freq = (
523
+ self._prepare_frequency_operator(
524
+ frequency_operator,
525
+ matrix=frequency_matrix,
526
+ )
527
+ if frequency_operator is not None or frequency_matrix is not None
528
+ else self.frequency_operator
529
+ )
530
+ threshold = (
531
+ float(coherence_threshold)
532
+ if coherence_threshold is not None
533
+ else self.coherence_threshold
534
+ )
535
+ validator = validator or self.validator
536
+ rng = rng or self.rng
537
+
538
+ if enable_validation is None:
539
+ if self._math_validation_override is not None:
540
+ should_validate = bool(self._math_validation_override)
541
+ else:
542
+ should_validate = bool(get_flags().enable_math_validation)
543
+ else:
544
+ should_validate = bool(enable_validation)
545
+ self.enable_math_validation = should_validate
546
+
547
+ enforce_frequency = (
548
+ bool(enforce_frequency_positivity)
549
+ if enforce_frequency_positivity is not None
550
+ else bool(effective_freq is not None)
551
+ )
552
+
553
+ def _project(epi: float, vf: float, theta: float) -> np.ndarray:
554
+ local_rng = None
555
+ if rng is not None:
556
+ bit_generator = rng.bit_generator
557
+ cloned_state = copy.deepcopy(bit_generator.state)
558
+ local_bit_generator = type(bit_generator)()
559
+ local_bit_generator.state = cloned_state
560
+ local_rng = np.random.Generator(local_bit_generator)
561
+ vector = projector(
562
+ epi=epi,
563
+ nu_f=vf,
564
+ theta=theta,
565
+ dim=hilbert.dimension,
566
+ rng=local_rng,
567
+ )
568
+ return np.asarray(vector, dtype=np.complex128)
569
+
570
+ active_flags = get_flags()
571
+ should_log_metrics = bool(log_metrics and active_flags.log_performance)
572
+
573
+ def _metrics(state: np.ndarray, label: str) -> dict[str, Any]:
574
+ metrics: dict[str, Any] = {}
575
+ with context_flags(log_performance=False):
576
+ norm_passed, norm_value = runtime_normalized(state, hilbert, label=label)
577
+ metrics["normalized"] = bool(norm_passed)
578
+ metrics["norm"] = float(norm_value)
579
+ if effective_coherence is not None and threshold is not None:
580
+ coh_passed, coh_value = runtime_coherence(
581
+ state, effective_coherence, threshold, label=label
582
+ )
583
+ metrics["coherence"] = bool(coh_passed)
584
+ metrics["coherence_expectation"] = float(coh_value)
585
+ metrics["coherence_threshold"] = float(threshold)
586
+ if effective_freq is not None:
587
+ freq_summary = runtime_frequency_positive(
588
+ state,
589
+ effective_freq,
590
+ enforce=enforce_frequency,
591
+ label=label,
592
+ )
593
+ metrics["frequency_positive"] = bool(freq_summary["passed"])
594
+ metrics["frequency_expectation"] = float(freq_summary["value"])
595
+ metrics["frequency_projection_passed"] = bool(
596
+ freq_summary["projection_passed"]
597
+ )
598
+ metrics["frequency_spectrum_psd"] = bool(freq_summary["spectrum_psd"])
599
+ metrics["frequency_spectrum_min"] = float(freq_summary["spectrum_min"])
600
+ metrics["frequency_enforced"] = bool(freq_summary["enforce"])
601
+ if effective_coherence is not None:
602
+ unitary_passed, unitary_norm = runtime_stable_unitary(
603
+ state,
604
+ effective_coherence,
605
+ hilbert,
606
+ label=label,
607
+ )
608
+ metrics["stable_unitary"] = bool(unitary_passed)
609
+ metrics["stable_unitary_norm_after"] = float(unitary_norm)
610
+ if should_log_metrics:
611
+ LOGGER.debug(
612
+ "node_metrics.%s normalized=%s coherence=%s frequency_positive=%s stable_unitary=%s coherence_expectation=%s frequency_expectation=%s",
613
+ label,
614
+ metrics.get("normalized"),
615
+ metrics.get("coherence"),
616
+ metrics.get("frequency_positive"),
617
+ metrics.get("stable_unitary"),
618
+ metrics.get("coherence_expectation"),
619
+ metrics.get("frequency_expectation"),
620
+ )
621
+ return metrics
622
+
623
+ pre_state = _project(self.EPI, self.vf, self.theta)
624
+ pre_metrics = _metrics(pre_state, "pre")
625
+
626
+ structural_run_sequence(self.G, self.n, ops)
627
+
628
+ post_state = _project(self.EPI, self.vf, self.theta)
629
+ post_metrics = _metrics(post_state, "post")
630
+
631
+ validation_summary: dict[str, Any] | None = None
632
+ if should_validate:
633
+ validator_instance = validator
634
+ if validator_instance is None:
635
+ if effective_coherence is None:
636
+ raise ValueError("Validation requires a coherence operator.")
637
+ validator_instance = NFRValidator(
638
+ hilbert,
639
+ effective_coherence,
640
+ threshold if threshold is not None else 0.0,
641
+ frequency_operator=effective_freq,
642
+ )
643
+ outcome = validator_instance.validate(
644
+ post_state,
645
+ enforce_frequency_positivity=enforce_frequency,
646
+ )
647
+ validation_summary = {
648
+ "passed": bool(outcome.passed),
649
+ "summary": outcome.summary,
650
+ "report": validator_instance.report(outcome),
651
+ }
652
+
653
+ result = {
654
+ "pre_state": pre_state,
655
+ "post_state": post_state,
656
+ "pre_metrics": pre_metrics,
657
+ "post_metrics": post_metrics,
658
+ "validation": validation_summary,
659
+ }
660
+ # Preserve legacy structure for downstream compatibility.
661
+ result["pre"] = {"state": pre_state, "metrics": pre_metrics}
662
+ result["post"] = {"state": post_state, "metrics": post_metrics}
663
+ return result