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/dynamics/runtime.py
CHANGED
|
@@ -3,34 +3,47 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
-
import math
|
|
7
6
|
import sys
|
|
7
|
+
from copy import deepcopy
|
|
8
8
|
from collections import deque
|
|
9
|
-
from collections.abc import Mapping, MutableMapping
|
|
9
|
+
from collections.abc import Iterable, Mapping, MutableMapping
|
|
10
|
+
from numbers import Real
|
|
10
11
|
from typing import Any, cast
|
|
11
12
|
|
|
12
|
-
from ..alias import
|
|
13
|
-
get_attr,
|
|
14
|
-
get_theta_attr,
|
|
15
|
-
set_attr,
|
|
16
|
-
set_theta,
|
|
17
|
-
set_theta_attr,
|
|
18
|
-
set_vf,
|
|
19
|
-
multi_recompute_abs_max,
|
|
20
|
-
)
|
|
13
|
+
from ..alias import get_attr
|
|
21
14
|
from ..callback_utils import CallbackEvent, callback_manager
|
|
22
|
-
from ..constants import
|
|
15
|
+
from ..constants import get_graph_param, get_param
|
|
23
16
|
from ..glyph_history import ensure_history
|
|
24
|
-
from ..helpers.numeric import clamp
|
|
25
17
|
from ..metrics.sense_index import compute_Si
|
|
18
|
+
from ..operators import apply_remesh_if_globally_stable
|
|
19
|
+
from ..telemetry import publish_graph_cache_metrics
|
|
26
20
|
from ..types import HistoryState, NodeId, TNFRGraph
|
|
27
|
-
from
|
|
21
|
+
from ..utils import normalize_optional_int
|
|
22
|
+
from ..validation import apply_canonical_clamps, validate_canon
|
|
28
23
|
from . import adaptation, coordination, integrators, selectors
|
|
24
|
+
from .aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_THETA, ALIAS_VF
|
|
25
|
+
|
|
26
|
+
try: # pragma: no cover - optional NumPy dependency
|
|
27
|
+
import numpy as np
|
|
28
|
+
except ImportError: # pragma: no cover - optional dependency missing
|
|
29
|
+
np = None # type: ignore[assignment]
|
|
30
|
+
|
|
31
|
+
try: # pragma: no cover - optional math extras
|
|
32
|
+
from ..mathematics.dynamics import MathematicalDynamicsEngine
|
|
33
|
+
from ..mathematics.projection import BasicStateProjector
|
|
34
|
+
from ..mathematics.runtime import (
|
|
35
|
+
coherence as runtime_coherence,
|
|
36
|
+
frequency_positive as runtime_frequency_positive,
|
|
37
|
+
normalized as runtime_normalized,
|
|
38
|
+
)
|
|
39
|
+
except Exception: # pragma: no cover - fallback when extras not available
|
|
40
|
+
MathematicalDynamicsEngine = None # type: ignore[assignment]
|
|
41
|
+
BasicStateProjector = None # type: ignore[assignment]
|
|
42
|
+
runtime_coherence = None # type: ignore[assignment]
|
|
43
|
+
runtime_frequency_positive = None # type: ignore[assignment]
|
|
44
|
+
runtime_normalized = None # type: ignore[assignment]
|
|
29
45
|
from .dnfr import default_compute_delta_nfr
|
|
30
46
|
from .sampling import update_node_sample as _update_node_sample
|
|
31
|
-
from ..operators import apply_remesh_if_globally_stable
|
|
32
|
-
|
|
33
|
-
HistoryLog = MutableSequence[MutableMapping[str, object]]
|
|
34
47
|
|
|
35
48
|
__all__ = (
|
|
36
49
|
"ALIAS_VF",
|
|
@@ -51,23 +64,36 @@ __all__ = (
|
|
|
51
64
|
"step",
|
|
52
65
|
"run",
|
|
53
66
|
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _log_clamp(
|
|
57
|
-
hist: HistoryLog,
|
|
58
|
-
node: NodeId | None,
|
|
59
|
-
attr: str,
|
|
60
|
-
value: float,
|
|
61
|
-
lo: float,
|
|
62
|
-
hi: float,
|
|
63
|
-
) -> None:
|
|
64
|
-
if value < lo or value > hi:
|
|
65
|
-
hist.append({"node": node, "attr": attr, "value": float(value)})
|
|
66
|
-
|
|
67
|
-
|
|
68
67
|
def _normalize_job_overrides(
|
|
69
68
|
job_overrides: Mapping[str, Any] | None,
|
|
70
69
|
) -> dict[str, Any]:
|
|
70
|
+
"""Canonicalise job override keys for ΔNFR, νf and phase orchestration.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
job_overrides : Mapping[str, Any] | None
|
|
75
|
+
User-provided mapping whose keys may use legacy ``*_N_JOBS`` forms or
|
|
76
|
+
mixed casing. The values tune the parallel workloads that update ΔNFR,
|
|
77
|
+
νf adaptation and global phase coordination.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
dict[str, Any]
|
|
82
|
+
A dictionary where keys are upper-cased without the ``_N_JOBS`` suffix,
|
|
83
|
+
ready for downstream lookup in the runtime schedulers.
|
|
84
|
+
|
|
85
|
+
Notes
|
|
86
|
+
-----
|
|
87
|
+
``None`` keys are silently skipped to preserve resiliency when
|
|
88
|
+
orchestrating ΔNFR workers.
|
|
89
|
+
|
|
90
|
+
Examples
|
|
91
|
+
--------
|
|
92
|
+
>>> _normalize_job_overrides({"dnfr_n_jobs": 2, "vf_adapt": 4})
|
|
93
|
+
{'DNFR': 2, 'VF_ADAPT': 4}
|
|
94
|
+
>>> _normalize_job_overrides(None)
|
|
95
|
+
{}
|
|
96
|
+
"""
|
|
71
97
|
if not job_overrides:
|
|
72
98
|
return {}
|
|
73
99
|
|
|
@@ -82,23 +108,6 @@ def _normalize_job_overrides(
|
|
|
82
108
|
return normalized
|
|
83
109
|
|
|
84
110
|
|
|
85
|
-
def _coerce_jobs_value(raw: Any) -> int | None:
|
|
86
|
-
if raw is None:
|
|
87
|
-
return None
|
|
88
|
-
try:
|
|
89
|
-
return int(raw)
|
|
90
|
-
except (TypeError, ValueError):
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _sanitize_jobs(value: int | None, *, allow_non_positive: bool) -> int | None:
|
|
95
|
-
if value is None:
|
|
96
|
-
return None
|
|
97
|
-
if not allow_non_positive and value <= 0:
|
|
98
|
-
return None
|
|
99
|
-
return value
|
|
100
|
-
|
|
101
|
-
|
|
102
111
|
def _resolve_jobs_override(
|
|
103
112
|
overrides: Mapping[str, Any],
|
|
104
113
|
key: str,
|
|
@@ -106,16 +115,54 @@ def _resolve_jobs_override(
|
|
|
106
115
|
*,
|
|
107
116
|
allow_non_positive: bool,
|
|
108
117
|
) -> int | None:
|
|
118
|
+
"""Resolve job overrides prioritising user hints over graph defaults.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
overrides : Mapping[str, Any]
|
|
123
|
+
Normalised overrides produced by :func:`_normalize_job_overrides` that
|
|
124
|
+
steer the ΔNFR computation, νf adaptation or phase coupling workers.
|
|
125
|
+
key : str
|
|
126
|
+
Logical subsystem key such as ``"DNFR"`` or ``"VF_ADAPT"``.
|
|
127
|
+
graph_value : Any
|
|
128
|
+
Baseline job count stored in the graph configuration.
|
|
129
|
+
allow_non_positive : bool
|
|
130
|
+
Propagated policy describing whether zero or negative values are valid
|
|
131
|
+
for the subsystem.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
int | None
|
|
136
|
+
Final job count that each scheduler will use, or ``None`` when no
|
|
137
|
+
explicit override or valid fallback exists.
|
|
138
|
+
|
|
139
|
+
Notes
|
|
140
|
+
-----
|
|
141
|
+
Preference resolution is pure and returns ``None`` instead of raising when
|
|
142
|
+
overrides cannot be coerced into valid integers.
|
|
143
|
+
|
|
144
|
+
Examples
|
|
145
|
+
--------
|
|
146
|
+
>>> overrides = _normalize_job_overrides({"phase": 0})
|
|
147
|
+
>>> _resolve_jobs_override(overrides, "phase", 2, allow_non_positive=True)
|
|
148
|
+
0
|
|
149
|
+
>>> _resolve_jobs_override({}, "vf_adapt", 4, allow_non_positive=False)
|
|
150
|
+
4
|
|
151
|
+
"""
|
|
109
152
|
norm_key = key.upper()
|
|
110
153
|
if overrides and norm_key in overrides:
|
|
111
|
-
return
|
|
112
|
-
|
|
154
|
+
return normalize_optional_int(
|
|
155
|
+
overrides.get(norm_key),
|
|
113
156
|
allow_non_positive=allow_non_positive,
|
|
157
|
+
strict=False,
|
|
158
|
+
sentinels=None,
|
|
114
159
|
)
|
|
115
160
|
|
|
116
|
-
return
|
|
117
|
-
|
|
161
|
+
return normalize_optional_int(
|
|
162
|
+
graph_value,
|
|
118
163
|
allow_non_positive=allow_non_positive,
|
|
164
|
+
strict=False,
|
|
165
|
+
sentinels=None,
|
|
119
166
|
)
|
|
120
167
|
|
|
121
168
|
|
|
@@ -220,64 +267,6 @@ def _resolve_integrator_instance(G: TNFRGraph) -> integrators.AbstractIntegrator
|
|
|
220
267
|
return instance
|
|
221
268
|
|
|
222
269
|
|
|
223
|
-
def apply_canonical_clamps(
|
|
224
|
-
nd: MutableMapping[str, Any],
|
|
225
|
-
G: TNFRGraph | None = None,
|
|
226
|
-
node: NodeId | None = None,
|
|
227
|
-
) -> None:
|
|
228
|
-
if G is not None:
|
|
229
|
-
graph_dict = cast(MutableMapping[str, Any], G.graph)
|
|
230
|
-
graph_data: Mapping[str, Any] = graph_dict
|
|
231
|
-
else:
|
|
232
|
-
graph_dict = None
|
|
233
|
-
graph_data = DEFAULTS
|
|
234
|
-
eps_min = float(graph_data.get("EPI_MIN", DEFAULTS["EPI_MIN"]))
|
|
235
|
-
eps_max = float(graph_data.get("EPI_MAX", DEFAULTS["EPI_MAX"]))
|
|
236
|
-
vf_min = float(graph_data.get("VF_MIN", DEFAULTS["VF_MIN"]))
|
|
237
|
-
vf_max = float(graph_data.get("VF_MAX", DEFAULTS["VF_MAX"]))
|
|
238
|
-
theta_wrap = bool(graph_data.get("THETA_WRAP", DEFAULTS["THETA_WRAP"]))
|
|
239
|
-
|
|
240
|
-
epi = cast(float, get_attr(nd, ALIAS_EPI, 0.0))
|
|
241
|
-
vf = get_attr(nd, ALIAS_VF, 0.0)
|
|
242
|
-
th_val = get_theta_attr(nd, 0.0)
|
|
243
|
-
th = 0.0 if th_val is None else float(th_val)
|
|
244
|
-
|
|
245
|
-
strict = bool(
|
|
246
|
-
graph_data.get("VALIDATORS_STRICT", DEFAULTS.get("VALIDATORS_STRICT", False))
|
|
247
|
-
)
|
|
248
|
-
if strict and graph_dict is not None:
|
|
249
|
-
history = cast(MutableMapping[str, Any], graph_dict.setdefault("history", {}))
|
|
250
|
-
hist = cast(
|
|
251
|
-
HistoryLog,
|
|
252
|
-
history.setdefault("clamp_alerts", []),
|
|
253
|
-
)
|
|
254
|
-
_log_clamp(hist, node, "EPI", float(epi), eps_min, eps_max)
|
|
255
|
-
_log_clamp(hist, node, "VF", float(vf), vf_min, vf_max)
|
|
256
|
-
|
|
257
|
-
set_attr(nd, ALIAS_EPI, clamp(epi, eps_min, eps_max))
|
|
258
|
-
|
|
259
|
-
vf_val = float(clamp(vf, vf_min, vf_max))
|
|
260
|
-
if G is not None and node is not None:
|
|
261
|
-
set_vf(G, node, vf_val, update_max=False)
|
|
262
|
-
else:
|
|
263
|
-
set_attr(nd, ALIAS_VF, vf_val)
|
|
264
|
-
|
|
265
|
-
if theta_wrap:
|
|
266
|
-
new_th = (th + math.pi) % (2 * math.pi) - math.pi
|
|
267
|
-
if G is not None and node is not None:
|
|
268
|
-
set_theta(G, node, new_th)
|
|
269
|
-
else:
|
|
270
|
-
set_theta_attr(nd, new_th)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def validate_canon(G: TNFRGraph) -> TNFRGraph:
|
|
274
|
-
for n, nd in G.nodes(data=True):
|
|
275
|
-
apply_canonical_clamps(cast(MutableMapping[str, Any], nd), G, cast(NodeId, n))
|
|
276
|
-
maxes = multi_recompute_abs_max(G, {"_vfmax": ALIAS_VF})
|
|
277
|
-
G.graph.update(maxes)
|
|
278
|
-
return G
|
|
279
|
-
|
|
280
|
-
|
|
281
270
|
def _run_before_callbacks(
|
|
282
271
|
G: TNFRGraph,
|
|
283
272
|
*,
|
|
@@ -286,6 +275,8 @@ def _run_before_callbacks(
|
|
|
286
275
|
use_Si: bool,
|
|
287
276
|
apply_glyphs: bool,
|
|
288
277
|
) -> None:
|
|
278
|
+
"""Notify ``BEFORE_STEP`` observers with execution context."""
|
|
279
|
+
|
|
289
280
|
callback_manager.invoke_callbacks(
|
|
290
281
|
G,
|
|
291
282
|
CallbackEvent.BEFORE_STEP.value,
|
|
@@ -304,9 +295,9 @@ def _prepare_dnfr(
|
|
|
304
295
|
use_Si: bool,
|
|
305
296
|
job_overrides: Mapping[str, Any] | None = None,
|
|
306
297
|
) -> None:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
298
|
+
"""Recompute ΔNFR (and optionally Si) ahead of an integration step."""
|
|
299
|
+
|
|
300
|
+
compute_dnfr_cb = G.graph.get("compute_delta_nfr", default_compute_delta_nfr)
|
|
310
301
|
overrides = job_overrides or {}
|
|
311
302
|
n_jobs = _resolve_jobs_override(
|
|
312
303
|
overrides,
|
|
@@ -328,9 +319,7 @@ def _prepare_dnfr(
|
|
|
328
319
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
329
320
|
inspect.Parameter.KEYWORD_ONLY,
|
|
330
321
|
)
|
|
331
|
-
elif any(
|
|
332
|
-
p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
|
|
333
|
-
):
|
|
322
|
+
elif any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()):
|
|
334
323
|
supports_n_jobs = True
|
|
335
324
|
|
|
336
325
|
if supports_n_jobs:
|
|
@@ -372,6 +361,8 @@ def _update_nodes(
|
|
|
372
361
|
hist: HistoryState,
|
|
373
362
|
job_overrides: Mapping[str, Any] | None = None,
|
|
374
363
|
) -> None:
|
|
364
|
+
"""Apply glyphs, integrate ΔNFR and refresh derived nodal state."""
|
|
365
|
+
|
|
375
366
|
_update_node_sample(G, step=step_idx)
|
|
376
367
|
overrides = job_overrides or {}
|
|
377
368
|
_prepare_dnfr(G, use_Si=use_Si, job_overrides=overrides)
|
|
@@ -395,9 +386,7 @@ def _update_nodes(
|
|
|
395
386
|
n_jobs=n_jobs,
|
|
396
387
|
)
|
|
397
388
|
for n, nd in G.nodes(data=True):
|
|
398
|
-
apply_canonical_clamps(
|
|
399
|
-
cast(MutableMapping[str, Any], nd), G, cast(NodeId, n)
|
|
400
|
-
)
|
|
389
|
+
apply_canonical_clamps(cast(MutableMapping[str, Any], nd), G, cast(NodeId, n))
|
|
401
390
|
phase_jobs = _resolve_jobs_override(
|
|
402
391
|
overrides,
|
|
403
392
|
"PHASE",
|
|
@@ -415,6 +404,8 @@ def _update_nodes(
|
|
|
415
404
|
|
|
416
405
|
|
|
417
406
|
def _update_epi_hist(G: TNFRGraph) -> None:
|
|
407
|
+
"""Maintain the rolling EPI history used by remeshing heuristics."""
|
|
408
|
+
|
|
418
409
|
tau_g = int(get_param(G, "REMESH_TAU_GLOBAL"))
|
|
419
410
|
tau_l = int(get_param(G, "REMESH_TAU_LOCAL"))
|
|
420
411
|
tau = max(tau_g, tau_l)
|
|
@@ -423,22 +414,26 @@ def _update_epi_hist(G: TNFRGraph) -> None:
|
|
|
423
414
|
if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
|
|
424
415
|
epi_hist = deque(list(epi_hist or [])[-maxlen:], maxlen=maxlen)
|
|
425
416
|
G.graph["_epi_hist"] = epi_hist
|
|
426
|
-
epi_hist.append(
|
|
427
|
-
{n: get_attr(nd, ALIAS_EPI, 0.0) for n, nd in G.nodes(data=True)}
|
|
428
|
-
)
|
|
417
|
+
epi_hist.append({n: get_attr(nd, ALIAS_EPI, 0.0) for n, nd in G.nodes(data=True)})
|
|
429
418
|
|
|
430
419
|
|
|
431
420
|
def _maybe_remesh(G: TNFRGraph) -> None:
|
|
421
|
+
"""Trigger remeshing when stability thresholds are satisfied."""
|
|
422
|
+
|
|
432
423
|
apply_remesh_if_globally_stable(G)
|
|
433
424
|
|
|
434
425
|
|
|
435
426
|
def _run_validators(G: TNFRGraph) -> None:
|
|
436
|
-
|
|
427
|
+
"""Execute registered validators ensuring canonical invariants hold."""
|
|
428
|
+
|
|
429
|
+
from ..validation import run_validators
|
|
437
430
|
|
|
438
431
|
run_validators(G)
|
|
439
432
|
|
|
440
433
|
|
|
441
434
|
def _run_after_callbacks(G, *, step_idx: int) -> None:
|
|
435
|
+
"""Notify ``AFTER_STEP`` observers with the latest structural metrics."""
|
|
436
|
+
|
|
442
437
|
h = ensure_history(G)
|
|
443
438
|
ctx = {"step": step_idx}
|
|
444
439
|
metric_pairs = [
|
|
@@ -454,6 +449,254 @@ def _run_after_callbacks(G, *, step_idx: int) -> None:
|
|
|
454
449
|
ctx[dst] = values[-1]
|
|
455
450
|
callback_manager.invoke_callbacks(G, CallbackEvent.AFTER_STEP.value, ctx)
|
|
456
451
|
|
|
452
|
+
telemetry = G.graph.get("telemetry")
|
|
453
|
+
if isinstance(telemetry, MutableMapping):
|
|
454
|
+
history = telemetry.get("nu_f")
|
|
455
|
+
history_key = "nu_f_history"
|
|
456
|
+
if isinstance(history, list) and history_key not in telemetry:
|
|
457
|
+
telemetry[history_key] = history
|
|
458
|
+
payload = telemetry.get("nu_f_snapshot")
|
|
459
|
+
if isinstance(payload, Mapping):
|
|
460
|
+
bridge_raw = telemetry.get("nu_f_bridge")
|
|
461
|
+
try:
|
|
462
|
+
bridge = float(bridge_raw) if bridge_raw is not None else None
|
|
463
|
+
except (TypeError, ValueError):
|
|
464
|
+
bridge = None
|
|
465
|
+
nu_f_summary = {
|
|
466
|
+
"total_reorganisations": payload.get("total_reorganisations"),
|
|
467
|
+
"total_duration": payload.get("total_duration"),
|
|
468
|
+
"rate_hz_str": payload.get("rate_hz_str"),
|
|
469
|
+
"rate_hz": payload.get("rate_hz"),
|
|
470
|
+
"variance_hz_str": payload.get("variance_hz_str"),
|
|
471
|
+
"variance_hz": payload.get("variance_hz"),
|
|
472
|
+
"confidence_level": payload.get("confidence_level"),
|
|
473
|
+
"ci_hz_str": {
|
|
474
|
+
"lower": payload.get("ci_lower_hz_str"),
|
|
475
|
+
"upper": payload.get("ci_upper_hz_str"),
|
|
476
|
+
},
|
|
477
|
+
"ci_hz": {
|
|
478
|
+
"lower": payload.get("ci_lower_hz"),
|
|
479
|
+
"upper": payload.get("ci_upper_hz"),
|
|
480
|
+
},
|
|
481
|
+
"bridge": bridge,
|
|
482
|
+
}
|
|
483
|
+
telemetry["nu_f"] = nu_f_summary
|
|
484
|
+
math_summary = telemetry.get("math_engine")
|
|
485
|
+
if isinstance(math_summary, MutableMapping):
|
|
486
|
+
math_summary["nu_f"] = dict(nu_f_summary)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _get_math_engine_config(G: TNFRGraph) -> MutableMapping[str, Any] | None:
|
|
490
|
+
"""Return the mutable math-engine configuration stored on ``G``."""
|
|
491
|
+
|
|
492
|
+
cfg_raw = G.graph.get("MATH_ENGINE")
|
|
493
|
+
if not isinstance(cfg_raw, Mapping) or not cfg_raw.get("enabled", False):
|
|
494
|
+
return None
|
|
495
|
+
if isinstance(cfg_raw, MutableMapping):
|
|
496
|
+
return cfg_raw
|
|
497
|
+
cfg_mutable: MutableMapping[str, Any] = dict(cfg_raw)
|
|
498
|
+
G.graph["MATH_ENGINE"] = cfg_mutable
|
|
499
|
+
return cfg_mutable
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _initialise_math_state(
|
|
503
|
+
G: TNFRGraph,
|
|
504
|
+
cfg: MutableMapping[str, Any],
|
|
505
|
+
*,
|
|
506
|
+
hilbert_space: Any,
|
|
507
|
+
projector: BasicStateProjector,
|
|
508
|
+
) -> np.ndarray | None:
|
|
509
|
+
"""Project graph nodes into the Hilbert space to seed the math engine."""
|
|
510
|
+
|
|
511
|
+
dimension = getattr(hilbert_space, "dimension", None)
|
|
512
|
+
if dimension is None:
|
|
513
|
+
raise AttributeError("Hilbert space configuration is missing 'dimension'.")
|
|
514
|
+
|
|
515
|
+
vectors: list[np.ndarray] = []
|
|
516
|
+
for _, nd in G.nodes(data=True):
|
|
517
|
+
epi = float(get_attr(nd, ALIAS_EPI, 0.0) or 0.0)
|
|
518
|
+
nu_f = float(get_attr(nd, ALIAS_VF, 0.0) or 0.0)
|
|
519
|
+
theta_val = float(get_attr(nd, ALIAS_THETA, 0.0) or 0.0)
|
|
520
|
+
try:
|
|
521
|
+
vector = projector(epi=epi, nu_f=nu_f, theta=theta_val, dim=int(dimension))
|
|
522
|
+
except ValueError:
|
|
523
|
+
continue
|
|
524
|
+
vectors.append(np.asarray(vector, dtype=np.complex128))
|
|
525
|
+
|
|
526
|
+
if not vectors:
|
|
527
|
+
return None
|
|
528
|
+
|
|
529
|
+
stacked = np.vstack(vectors)
|
|
530
|
+
averaged = np.mean(stacked, axis=0)
|
|
531
|
+
atol = float(getattr(projector, "atol", 1e-9))
|
|
532
|
+
norm = float(getattr(hilbert_space, "norm")(averaged))
|
|
533
|
+
if np.isclose(norm, 0.0, atol=atol):
|
|
534
|
+
averaged = vectors[0]
|
|
535
|
+
norm = float(getattr(hilbert_space, "norm")(averaged))
|
|
536
|
+
if np.isclose(norm, 0.0, atol=atol):
|
|
537
|
+
return None
|
|
538
|
+
normalised = averaged / norm
|
|
539
|
+
cfg.setdefault("_state_origin", "projected")
|
|
540
|
+
return normalised
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _advance_math_engine(
|
|
544
|
+
G: TNFRGraph,
|
|
545
|
+
*,
|
|
546
|
+
dt: float,
|
|
547
|
+
step_idx: int,
|
|
548
|
+
hist: HistoryState,
|
|
549
|
+
) -> None:
|
|
550
|
+
"""Advance the optional math engine and record spectral telemetry."""
|
|
551
|
+
|
|
552
|
+
cfg = _get_math_engine_config(G)
|
|
553
|
+
if cfg is None:
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
if (
|
|
557
|
+
np is None
|
|
558
|
+
or MathematicalDynamicsEngine is None
|
|
559
|
+
or runtime_normalized is None
|
|
560
|
+
or runtime_coherence is None
|
|
561
|
+
):
|
|
562
|
+
raise RuntimeError(
|
|
563
|
+
"Mathematical dynamics require NumPy and tnfr.mathematics extras to be installed."
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
hilbert_space = cfg.get("hilbert_space")
|
|
567
|
+
coherence_operator = cfg.get("coherence_operator")
|
|
568
|
+
coherence_threshold = cfg.get("coherence_threshold")
|
|
569
|
+
if hilbert_space is None or coherence_operator is None or coherence_threshold is None:
|
|
570
|
+
raise ValueError(
|
|
571
|
+
"MATH_ENGINE requires 'hilbert_space', 'coherence_operator' and "
|
|
572
|
+
"'coherence_threshold' entries."
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
if BasicStateProjector is None: # pragma: no cover - guarded by import above
|
|
576
|
+
raise RuntimeError("Mathematical dynamics require the BasicStateProjector helper.")
|
|
577
|
+
|
|
578
|
+
projector = cfg.get("state_projector")
|
|
579
|
+
if not isinstance(projector, BasicStateProjector):
|
|
580
|
+
projector = BasicStateProjector()
|
|
581
|
+
cfg["state_projector"] = projector
|
|
582
|
+
|
|
583
|
+
engine = cfg.get("dynamics_engine")
|
|
584
|
+
if not isinstance(engine, MathematicalDynamicsEngine):
|
|
585
|
+
generator = cfg.get("generator_matrix")
|
|
586
|
+
if generator is None:
|
|
587
|
+
raise ValueError(
|
|
588
|
+
"MATH_ENGINE requires either a 'dynamics_engine' instance or a "
|
|
589
|
+
"'generator_matrix'."
|
|
590
|
+
)
|
|
591
|
+
generator_matrix = np.asarray(generator, dtype=np.complex128)
|
|
592
|
+
engine = MathematicalDynamicsEngine(generator_matrix, hilbert_space=hilbert_space)
|
|
593
|
+
cfg["dynamics_engine"] = engine
|
|
594
|
+
|
|
595
|
+
state_vector = cfg.get("_state_vector")
|
|
596
|
+
if state_vector is None:
|
|
597
|
+
state_vector = _initialise_math_state(
|
|
598
|
+
G,
|
|
599
|
+
cfg,
|
|
600
|
+
hilbert_space=hilbert_space,
|
|
601
|
+
projector=projector,
|
|
602
|
+
)
|
|
603
|
+
if state_vector is None:
|
|
604
|
+
return
|
|
605
|
+
else:
|
|
606
|
+
state_vector = np.asarray(state_vector, dtype=np.complex128)
|
|
607
|
+
dimension = getattr(hilbert_space, "dimension", state_vector.shape[0])
|
|
608
|
+
if state_vector.shape != (int(dimension),):
|
|
609
|
+
state_vector = _initialise_math_state(
|
|
610
|
+
G,
|
|
611
|
+
cfg,
|
|
612
|
+
hilbert_space=hilbert_space,
|
|
613
|
+
projector=projector,
|
|
614
|
+
)
|
|
615
|
+
if state_vector is None:
|
|
616
|
+
return
|
|
617
|
+
|
|
618
|
+
advanced = engine.step(state_vector, dt=float(dt), normalize=True)
|
|
619
|
+
cfg["_state_vector"] = advanced
|
|
620
|
+
|
|
621
|
+
atol = float(cfg.get("atol", getattr(engine, "atol", 1e-9)))
|
|
622
|
+
label = f"step[{step_idx}]"
|
|
623
|
+
|
|
624
|
+
normalized_passed, norm_value = runtime_normalized(
|
|
625
|
+
advanced,
|
|
626
|
+
hilbert_space,
|
|
627
|
+
atol=atol,
|
|
628
|
+
label=label,
|
|
629
|
+
)
|
|
630
|
+
coherence_passed, coherence_value = runtime_coherence(
|
|
631
|
+
advanced,
|
|
632
|
+
coherence_operator,
|
|
633
|
+
float(coherence_threshold),
|
|
634
|
+
normalise=False,
|
|
635
|
+
atol=atol,
|
|
636
|
+
label=label,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
frequency_operator = cfg.get("frequency_operator")
|
|
640
|
+
frequency_summary: dict[str, Any] | None = None
|
|
641
|
+
if frequency_operator is not None:
|
|
642
|
+
if runtime_frequency_positive is None: # pragma: no cover - guarded above
|
|
643
|
+
raise RuntimeError(
|
|
644
|
+
"Frequency positivity checks require tnfr.mathematics extras."
|
|
645
|
+
)
|
|
646
|
+
freq_raw = runtime_frequency_positive(
|
|
647
|
+
advanced,
|
|
648
|
+
frequency_operator,
|
|
649
|
+
normalise=False,
|
|
650
|
+
enforce=True,
|
|
651
|
+
atol=atol,
|
|
652
|
+
label=label,
|
|
653
|
+
)
|
|
654
|
+
frequency_summary = {
|
|
655
|
+
"passed": bool(freq_raw.get("passed", False)),
|
|
656
|
+
"value": float(freq_raw.get("value", 0.0)),
|
|
657
|
+
"projection_passed": bool(freq_raw.get("projection_passed", False)),
|
|
658
|
+
"spectrum_psd": bool(freq_raw.get("spectrum_psd", False)),
|
|
659
|
+
"enforced": bool(freq_raw.get("enforce", True)),
|
|
660
|
+
}
|
|
661
|
+
if "spectrum_min" in freq_raw:
|
|
662
|
+
frequency_summary["spectrum_min"] = float(freq_raw.get("spectrum_min", 0.0))
|
|
663
|
+
|
|
664
|
+
summary = {
|
|
665
|
+
"step": step_idx,
|
|
666
|
+
"normalized": bool(normalized_passed),
|
|
667
|
+
"norm": float(norm_value),
|
|
668
|
+
"coherence": {
|
|
669
|
+
"passed": bool(coherence_passed),
|
|
670
|
+
"value": float(coherence_value),
|
|
671
|
+
"threshold": float(coherence_threshold),
|
|
672
|
+
},
|
|
673
|
+
"frequency": frequency_summary,
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
hist.setdefault("math_engine_summary", []).append(summary)
|
|
677
|
+
hist.setdefault("math_engine_norm", []).append(summary["norm"])
|
|
678
|
+
hist.setdefault("math_engine_normalized", []).append(summary["normalized"])
|
|
679
|
+
hist.setdefault("math_engine_coherence", []).append(summary["coherence"]["value"])
|
|
680
|
+
hist.setdefault("math_engine_coherence_passed", []).append(
|
|
681
|
+
summary["coherence"]["passed"]
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
if frequency_summary is None:
|
|
685
|
+
hist.setdefault("math_engine_frequency", []).append(None)
|
|
686
|
+
hist.setdefault("math_engine_frequency_passed", []).append(None)
|
|
687
|
+
hist.setdefault("math_engine_frequency_projection_passed", []).append(None)
|
|
688
|
+
else:
|
|
689
|
+
hist.setdefault("math_engine_frequency", []).append(frequency_summary["value"])
|
|
690
|
+
hist.setdefault("math_engine_frequency_passed", []).append(
|
|
691
|
+
frequency_summary["passed"]
|
|
692
|
+
)
|
|
693
|
+
hist.setdefault("math_engine_frequency_projection_passed", []).append(
|
|
694
|
+
frequency_summary["projection_passed"]
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
cfg["last_summary"] = summary
|
|
698
|
+
telemetry = G.graph.setdefault("telemetry", {})
|
|
699
|
+
telemetry["math_engine"] = deepcopy(summary)
|
|
457
700
|
|
|
458
701
|
def step(
|
|
459
702
|
G: TNFRGraph,
|
|
@@ -463,6 +706,59 @@ def step(
|
|
|
463
706
|
apply_glyphs: bool = True,
|
|
464
707
|
n_jobs: Mapping[str, Any] | None = None,
|
|
465
708
|
) -> None:
|
|
709
|
+
"""Advance the runtime one ΔNFR step updating νf, phase and glyphs.
|
|
710
|
+
|
|
711
|
+
Parameters
|
|
712
|
+
----------
|
|
713
|
+
G : TNFRGraph
|
|
714
|
+
Graph whose nodes store EPI, νf and phase metadata. The graph must
|
|
715
|
+
expose a ΔNFR hook under ``G.graph['compute_delta_nfr']`` and optional
|
|
716
|
+
selector or callback registrations.
|
|
717
|
+
dt : float | None, optional
|
|
718
|
+
Time increment injected into the integrator. ``None`` falls back to the
|
|
719
|
+
``DT`` attribute stored in ``G.graph`` which keeps ΔNFR integration
|
|
720
|
+
aligned with the nodal equation.
|
|
721
|
+
use_Si : bool, default True
|
|
722
|
+
When ``True`` the Sense Index (Si) is recomputed to modulate ΔNFR and
|
|
723
|
+
νf adaptation heuristics.
|
|
724
|
+
apply_glyphs : bool, default True
|
|
725
|
+
Enables canonical glyph selection so that phase and coherence glyphs
|
|
726
|
+
continue to modulate ΔNFR.
|
|
727
|
+
n_jobs : Mapping[str, Any] | None, optional
|
|
728
|
+
Optional overrides that tune the parallel workers used for ΔNFR, phase
|
|
729
|
+
coordination and νf adaptation. The mapping is processed by
|
|
730
|
+
:func:`_normalize_job_overrides`.
|
|
731
|
+
|
|
732
|
+
Returns
|
|
733
|
+
-------
|
|
734
|
+
None
|
|
735
|
+
Mutates ``G`` in place by recomputing ΔNFR, νf and phase metrics.
|
|
736
|
+
|
|
737
|
+
Notes
|
|
738
|
+
-----
|
|
739
|
+
Registered callbacks execute within :func:`step` and any exceptions they
|
|
740
|
+
raise propagate according to the callback manager configuration.
|
|
741
|
+
|
|
742
|
+
Examples
|
|
743
|
+
--------
|
|
744
|
+
Register a hook that records phase synchrony while using the parametric
|
|
745
|
+
selector to choose glyphs before advancing one runtime step.
|
|
746
|
+
|
|
747
|
+
>>> from tnfr.callback_utils import CallbackEvent, callback_manager
|
|
748
|
+
>>> from tnfr.dynamics import selectors
|
|
749
|
+
>>> from tnfr.dynamics.runtime import ALIAS_VF
|
|
750
|
+
>>> from tnfr.structural import create_nfr
|
|
751
|
+
>>> G, node = create_nfr("seed", epi=0.2, vf=1.5)
|
|
752
|
+
>>> callback_manager.register_callback(
|
|
753
|
+
... G,
|
|
754
|
+
... CallbackEvent.AFTER_STEP,
|
|
755
|
+
... lambda graph, ctx: graph.graph.setdefault("phase_log", []).append(ctx.get("phase_sync")),
|
|
756
|
+
... )
|
|
757
|
+
>>> G.graph["glyph_selector"] = selectors.ParametricGlyphSelector()
|
|
758
|
+
>>> step(G, dt=0.05, n_jobs={"dnfr_n_jobs": 1})
|
|
759
|
+
>>> ALIAS_VF in G.nodes[node]
|
|
760
|
+
True
|
|
761
|
+
"""
|
|
466
762
|
job_overrides = _normalize_job_overrides(n_jobs)
|
|
467
763
|
hist = ensure_history(G)
|
|
468
764
|
step_idx = len(hist.setdefault("C_steps", []))
|
|
@@ -478,10 +774,18 @@ def step(
|
|
|
478
774
|
hist=hist,
|
|
479
775
|
job_overrides=job_overrides,
|
|
480
776
|
)
|
|
777
|
+
resolved_dt = get_graph_param(G, "DT") if dt is None else float(dt)
|
|
778
|
+
_advance_math_engine(
|
|
779
|
+
G,
|
|
780
|
+
dt=resolved_dt,
|
|
781
|
+
step_idx=step_idx,
|
|
782
|
+
hist=hist,
|
|
783
|
+
)
|
|
481
784
|
_update_epi_hist(G)
|
|
482
785
|
_maybe_remesh(G)
|
|
483
786
|
_run_validators(G)
|
|
484
787
|
_run_after_callbacks(G, step_idx=step_idx)
|
|
788
|
+
publish_graph_cache_metrics(G)
|
|
485
789
|
|
|
486
790
|
|
|
487
791
|
def run(
|
|
@@ -493,13 +797,63 @@ def run(
|
|
|
493
797
|
apply_glyphs: bool = True,
|
|
494
798
|
n_jobs: Mapping[str, Any] | None = None,
|
|
495
799
|
) -> None:
|
|
800
|
+
"""Iterate :func:`step` to evolve ΔNFR, νf and phase trajectories.
|
|
801
|
+
|
|
802
|
+
Parameters
|
|
803
|
+
----------
|
|
804
|
+
G : TNFRGraph
|
|
805
|
+
Graph that stores the coherent structures. Callbacks and selectors
|
|
806
|
+
configured on ``G.graph`` orchestrate glyph application and telemetry.
|
|
807
|
+
steps : int
|
|
808
|
+
Number of times :func:`step` is invoked. Each iteration integrates ΔNFR
|
|
809
|
+
and νf according to ``dt`` and the configured selector.
|
|
810
|
+
dt : float | None, optional
|
|
811
|
+
Time increment for each step. ``None`` uses the graph's default ``DT``.
|
|
812
|
+
use_Si : bool, default True
|
|
813
|
+
Recompute the Sense Index during each iteration to keep ΔNFR feedback
|
|
814
|
+
loops tied to νf adjustments.
|
|
815
|
+
apply_glyphs : bool, default True
|
|
816
|
+
Enables glyph selection and application per step.
|
|
817
|
+
n_jobs : Mapping[str, Any] | None, optional
|
|
818
|
+
Shared overrides forwarded to each :func:`step` call.
|
|
819
|
+
|
|
820
|
+
Returns
|
|
821
|
+
-------
|
|
822
|
+
None
|
|
823
|
+
The graph ``G`` is updated in place.
|
|
824
|
+
|
|
825
|
+
Raises
|
|
826
|
+
------
|
|
827
|
+
ValueError
|
|
828
|
+
Raised when ``steps`` is negative because the runtime cannot evolve a
|
|
829
|
+
negative number of ΔNFR updates.
|
|
830
|
+
|
|
831
|
+
Examples
|
|
832
|
+
--------
|
|
833
|
+
Install a before-step callback and use the default glyph selector while
|
|
834
|
+
running two iterations that synchronise phase and νf.
|
|
835
|
+
|
|
836
|
+
>>> from tnfr.callback_utils import CallbackEvent, callback_manager
|
|
837
|
+
>>> from tnfr.dynamics import selectors
|
|
838
|
+
>>> from tnfr.structural import create_nfr
|
|
839
|
+
>>> G, node = create_nfr("seed", epi=0.3, vf=1.2)
|
|
840
|
+
>>> callback_manager.register_callback(
|
|
841
|
+
... G,
|
|
842
|
+
... CallbackEvent.BEFORE_STEP,
|
|
843
|
+
... lambda graph, ctx: graph.graph.setdefault("dt_trace", []).append(ctx["dt"]),
|
|
844
|
+
... )
|
|
845
|
+
>>> G.graph["glyph_selector"] = selectors.default_glyph_selector
|
|
846
|
+
>>> run(G, 2, dt=0.1)
|
|
847
|
+
>>> len(G.graph["dt_trace"])
|
|
848
|
+
2
|
|
849
|
+
"""
|
|
496
850
|
steps_int = int(steps)
|
|
497
851
|
if steps_int < 0:
|
|
498
852
|
raise ValueError("'steps' must be non-negative")
|
|
499
853
|
stop_cfg = get_graph_param(G, "STOP_EARLY", dict)
|
|
500
854
|
stop_enabled = False
|
|
501
855
|
if stop_cfg and stop_cfg.get("enabled", False):
|
|
502
|
-
w = int(stop_cfg.get("window", 25))
|
|
856
|
+
w = max(1, int(stop_cfg.get("window", 25)))
|
|
503
857
|
frac = float(stop_cfg.get("fraction", 0.90))
|
|
504
858
|
stop_enabled = True
|
|
505
859
|
job_overrides = _normalize_job_overrides(n_jobs)
|
|
@@ -513,9 +867,15 @@ def run(
|
|
|
513
867
|
)
|
|
514
868
|
if stop_enabled:
|
|
515
869
|
history = ensure_history(G)
|
|
516
|
-
|
|
517
|
-
if not isinstance(
|
|
518
|
-
series =
|
|
519
|
-
|
|
870
|
+
raw_series = dict.get(history, "stable_frac", [])
|
|
871
|
+
if not isinstance(raw_series, Iterable):
|
|
872
|
+
series = []
|
|
873
|
+
elif isinstance(raw_series, list):
|
|
874
|
+
series = raw_series
|
|
875
|
+
else:
|
|
876
|
+
series = list(raw_series)
|
|
877
|
+
numeric_series = [v for v in series if isinstance(v, Real)]
|
|
878
|
+
if len(numeric_series) >= w and all(
|
|
879
|
+
v >= frac for v in numeric_series[-w:]
|
|
880
|
+
):
|
|
520
881
|
break
|
|
521
|
-
|