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
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|