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/adaptation.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Any, cast
|
|
|
8
8
|
|
|
9
9
|
from ..alias import collect_attr, set_vf
|
|
10
10
|
from ..constants import get_graph_param
|
|
11
|
-
from ..
|
|
11
|
+
from ..utils import clamp, resolve_chunk_size
|
|
12
12
|
from ..metrics.common import ensure_neighbors_map
|
|
13
13
|
from ..types import CoherenceMetric, DeltaNFR, NodeId, TNFRGraph
|
|
14
14
|
from ..utils import get_numpy
|
|
@@ -18,7 +18,7 @@ __all__ = ("adapt_vf_by_coherence",)
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _vf_adapt_chunk(
|
|
21
|
-
args: tuple[list[tuple[Any, int, tuple[int, ...]]], tuple[float, ...], float]
|
|
21
|
+
args: tuple[list[tuple[Any, int, tuple[int, ...]]], tuple[float, ...], float],
|
|
22
22
|
) -> list[tuple[Any, float]]:
|
|
23
23
|
"""Return proposed νf updates for ``chunk`` of stable nodes."""
|
|
24
24
|
|
|
@@ -35,7 +35,71 @@ def _vf_adapt_chunk(
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def adapt_vf_by_coherence(G: TNFRGraph, n_jobs: int | None = None) -> None:
|
|
38
|
-
"""
|
|
38
|
+
"""Synchronise νf to the neighbour mean once ΔNFR and Si stay coherent.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
G : TNFRGraph
|
|
43
|
+
Graph that stores the TNFR nodes and configuration required for
|
|
44
|
+
adaptation. The routine reads ``VF_ADAPT_TAU`` (τ) to decide how many
|
|
45
|
+
consecutive stable steps a node must accumulate in ``stable_count``
|
|
46
|
+
before updating. The adaptation weight ``VF_ADAPT_MU`` (μ) controls how
|
|
47
|
+
quickly νf moves toward the neighbour mean. Stability is detected when
|
|
48
|
+
the absolute ΔNFR stays below ``EPS_DNFR_STABLE`` and the sense index Si
|
|
49
|
+
exceeds the selector threshold ``SELECTOR_THRESHOLDS['si_hi']`` (falling
|
|
50
|
+
back to ``GLYPH_THRESHOLDS['hi']``). Only nodes that satisfy both
|
|
51
|
+
thresholds for τ successive evaluations are eligible for μ-weighted
|
|
52
|
+
averaging.
|
|
53
|
+
n_jobs : int or None, optional
|
|
54
|
+
Number of worker processes used for eligible nodes. ``None`` (the
|
|
55
|
+
default) keeps the adaptation serial, ``1`` disables parallelism, and
|
|
56
|
+
any value greater than one dispatches chunks of nodes to a
|
|
57
|
+
:class:`~concurrent.futures.ProcessPoolExecutor` so large graphs can
|
|
58
|
+
adjust νf without blocking the main dynamic loop.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
None
|
|
63
|
+
The graph is updated in place; no value is returned.
|
|
64
|
+
|
|
65
|
+
Raises
|
|
66
|
+
------
|
|
67
|
+
KeyError
|
|
68
|
+
Raised when ``G.graph`` lacks the canonical adaptation parameters and
|
|
69
|
+
defaults have not been injected.
|
|
70
|
+
|
|
71
|
+
Examples
|
|
72
|
+
--------
|
|
73
|
+
>>> from tnfr.constants import inject_defaults
|
|
74
|
+
>>> from tnfr.dynamics import adapt_vf_by_coherence
|
|
75
|
+
>>> from tnfr.structural import create_nfr
|
|
76
|
+
>>> G, seed = create_nfr("seed", vf=0.2)
|
|
77
|
+
>>> _, anchor = create_nfr("anchor", graph=G, vf=1.0)
|
|
78
|
+
>>> G.add_edge(seed, anchor)
|
|
79
|
+
>>> inject_defaults(G)
|
|
80
|
+
>>> G.graph["VF_ADAPT_TAU"] = 2 # τ: consecutive stable steps
|
|
81
|
+
>>> G.graph["VF_ADAPT_MU"] = 0.5 # μ: neighbour coupling strength
|
|
82
|
+
>>> G.graph["SELECTOR_THRESHOLDS"] = {"si_hi": 0.8}
|
|
83
|
+
>>> for node in G.nodes:
|
|
84
|
+
... G.nodes[node]["Si"] = 0.9 # above ΔSi threshold
|
|
85
|
+
... G.nodes[node]["ΔNFR"] = 0.0 # within |ΔNFR| ≤ eps guard
|
|
86
|
+
... G.nodes[node]["stable_count"] = 1
|
|
87
|
+
>>> adapt_vf_by_coherence(G)
|
|
88
|
+
>>> round(G.nodes[seed]["νf"], 2), round(G.nodes[anchor]["νf"], 2)
|
|
89
|
+
(0.6, 0.6)
|
|
90
|
+
>>> G.nodes[seed]["stable_count"], G.nodes[anchor]["stable_count"] >= 2
|
|
91
|
+
(2, True)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
required_keys = ("VF_ADAPT_TAU", "VF_ADAPT_MU")
|
|
95
|
+
missing_keys = [key for key in required_keys if key not in G.graph]
|
|
96
|
+
if missing_keys:
|
|
97
|
+
missing_list = ", ".join(sorted(missing_keys))
|
|
98
|
+
raise KeyError(
|
|
99
|
+
"adapt_vf_by_coherence requires graph parameters "
|
|
100
|
+
f"{missing_list}; call tnfr.constants.inject_defaults(G) "
|
|
101
|
+
"before adaptation."
|
|
102
|
+
)
|
|
39
103
|
|
|
40
104
|
tau = get_graph_param(G, "VF_ADAPT_TAU", int)
|
|
41
105
|
mu = float(get_graph_param(G, "VF_ADAPT_MU"))
|
|
@@ -141,10 +205,11 @@ def adapt_vf_by_coherence(G: TNFRGraph, n_jobs: int | None = None) -> None:
|
|
|
141
205
|
|
|
142
206
|
prev_counts = [int(G.nodes[node].get("stable_count", 0)) for node in nodes]
|
|
143
207
|
stable_flags = [
|
|
144
|
-
si >= si_hi and dnfr <= eps_dnfr
|
|
145
|
-
|
|
208
|
+
si >= si_hi and dnfr <= eps_dnfr for si, dnfr in zip(si_list, dnfr_list)
|
|
209
|
+
]
|
|
210
|
+
new_counts = [
|
|
211
|
+
prev + 1 if flag else 0 for prev, flag in zip(prev_counts, stable_flags)
|
|
146
212
|
]
|
|
147
|
-
new_counts = [prev + 1 if flag else 0 for prev, flag in zip(prev_counts, stable_flags)]
|
|
148
213
|
|
|
149
214
|
for node, count in zip(nodes, new_counts):
|
|
150
215
|
G.nodes[node]["stable_count"] = int(count)
|
|
@@ -174,16 +239,18 @@ def adapt_vf_by_coherence(G: TNFRGraph, n_jobs: int | None = None) -> None:
|
|
|
174
239
|
for node in eligible_nodes:
|
|
175
240
|
idx = node_index[node]
|
|
176
241
|
neigh_indices = tuple(
|
|
177
|
-
node_index[nbr]
|
|
178
|
-
for nbr in neighbors_map.get(node, ())
|
|
179
|
-
if nbr in node_index
|
|
242
|
+
node_index[nbr] for nbr in neighbors_map.get(node, ()) if nbr in node_index
|
|
180
243
|
)
|
|
181
244
|
work_items.append((node, idx, neigh_indices))
|
|
182
245
|
|
|
183
|
-
|
|
246
|
+
approx_chunk = math.ceil(len(work_items) / jobs) if jobs else None
|
|
247
|
+
chunk_size = resolve_chunk_size(
|
|
248
|
+
approx_chunk,
|
|
249
|
+
len(work_items),
|
|
250
|
+
minimum=1,
|
|
251
|
+
)
|
|
184
252
|
chunks = [
|
|
185
|
-
work_items[i : i + chunk_size]
|
|
186
|
-
for i in range(0, len(work_items), chunk_size)
|
|
253
|
+
work_items[i : i + chunk_size] for i in range(0, len(work_items), chunk_size)
|
|
187
254
|
]
|
|
188
255
|
vf_tuple = tuple(vf_list)
|
|
189
256
|
updates: dict[Any, float] = {}
|
|
@@ -198,4 +265,3 @@ def adapt_vf_by_coherence(G: TNFRGraph, n_jobs: int | None = None) -> None:
|
|
|
198
265
|
if vf_new is None:
|
|
199
266
|
continue
|
|
200
267
|
set_vf(G, node, clamp(float(vf_new), vf_min, vf_max))
|
|
201
|
-
|
tnfr/dynamics/aliases.py
CHANGED
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from ..constants import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
ALIAS_EPI
|
|
10
|
-
ALIAS_SI
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
from ..constants.aliases import (
|
|
6
|
+
ALIAS_D2EPI,
|
|
7
|
+
ALIAS_DNFR,
|
|
8
|
+
ALIAS_DSI,
|
|
9
|
+
ALIAS_EPI,
|
|
10
|
+
ALIAS_SI,
|
|
11
|
+
ALIAS_THETA,
|
|
12
|
+
ALIAS_VF,
|
|
13
|
+
)
|
|
13
14
|
|
|
14
15
|
__all__ = (
|
|
15
16
|
"ALIAS_VF",
|
|
16
17
|
"ALIAS_DNFR",
|
|
17
18
|
"ALIAS_EPI",
|
|
18
19
|
"ALIAS_SI",
|
|
20
|
+
"ALIAS_THETA",
|
|
19
21
|
"ALIAS_D2EPI",
|
|
20
22
|
"ALIAS_DSI",
|
|
21
23
|
)
|
|
22
|
-
|
tnfr/dynamics/coordination.py
CHANGED
|
@@ -6,8 +6,7 @@ import math
|
|
|
6
6
|
from collections import deque
|
|
7
7
|
from collections.abc import Mapping, MutableMapping, Sequence
|
|
8
8
|
from concurrent.futures import ProcessPoolExecutor
|
|
9
|
-
from typing import
|
|
10
|
-
|
|
9
|
+
from typing import Any, TypeVar, cast
|
|
11
10
|
from ..alias import get_theta_attr, set_theta
|
|
12
11
|
from ..constants import (
|
|
13
12
|
DEFAULTS,
|
|
@@ -18,25 +17,13 @@ from ..constants import (
|
|
|
18
17
|
normalise_state_token,
|
|
19
18
|
)
|
|
20
19
|
from ..glyph_history import append_metric
|
|
21
|
-
from ..
|
|
20
|
+
from ..utils import angle_diff, resolve_chunk_size
|
|
22
21
|
from ..metrics.common import ensure_neighbors_map
|
|
23
22
|
from ..metrics.trig import neighbor_phase_mean_list
|
|
24
23
|
from ..metrics.trig_cache import get_trig_cache
|
|
25
24
|
from ..observers import DEFAULT_GLYPH_LOAD_SPAN, glyph_load, kuramoto_order
|
|
26
|
-
from ..types import NodeId, Phase, TNFRGraph
|
|
25
|
+
from ..types import FloatArray, NodeId, Phase, TNFRGraph
|
|
27
26
|
from ..utils import get_numpy
|
|
28
|
-
from .._compat import TypeAlias
|
|
29
|
-
|
|
30
|
-
if TYPE_CHECKING: # pragma: no cover - typing imports only
|
|
31
|
-
try:
|
|
32
|
-
import numpy as np_typing
|
|
33
|
-
import numpy.typing as npt
|
|
34
|
-
except ImportError: # pragma: no cover - optional typing dependency
|
|
35
|
-
FloatArray: TypeAlias = Any
|
|
36
|
-
else:
|
|
37
|
-
FloatArray: TypeAlias = npt.NDArray[np_typing.float_]
|
|
38
|
-
else: # pragma: no cover - runtime without numpy typing
|
|
39
|
-
FloatArray: TypeAlias = Any
|
|
40
27
|
|
|
41
28
|
_DequeT = TypeVar("_DequeT")
|
|
42
29
|
|
|
@@ -111,9 +98,7 @@ def _smooth_adjust_k(
|
|
|
111
98
|
|
|
112
99
|
if state == STATE_DISSONANT:
|
|
113
100
|
kG_t = kG_max
|
|
114
|
-
kL_t = 0.5 * (
|
|
115
|
-
kL_min + kL_max
|
|
116
|
-
) # local medio para no perder plasticidad
|
|
101
|
+
kL_t = 0.5 * (kL_min + kL_max) # keep kL mid-range to preserve local plasticity
|
|
117
102
|
elif state == STATE_STABLE:
|
|
118
103
|
kG_t = kG_min
|
|
119
104
|
kL_t = kL_min
|
|
@@ -172,13 +157,71 @@ def coordinate_global_local_phase(
|
|
|
172
157
|
*,
|
|
173
158
|
n_jobs: int | None = None,
|
|
174
159
|
) -> None:
|
|
175
|
-
"""Coordinate phase using a blend of global and neighbour coupling.
|
|
160
|
+
"""Coordinate phase using a blend of global and neighbour coupling.
|
|
161
|
+
|
|
162
|
+
This operator harmonises a TNFR graph by iteratively nudging each node's
|
|
163
|
+
phase toward the global Kuramoto mean while respecting the local
|
|
164
|
+
neighbourhood attractor. The global (``kG``) and local (``kL``) coupling
|
|
165
|
+
gains reshape phase coherence by modulating how strongly nodes follow the
|
|
166
|
+
network-wide synchrony versus immediate neighbours. When explicit coupling
|
|
167
|
+
overrides are not supplied, the gains adapt based on current ΔNFR telemetry
|
|
168
|
+
and the structural state recorded in the graph history. Adaptive updates
|
|
169
|
+
mutate the ``history`` buffers for phase state, order parameter, disruptor
|
|
170
|
+
load, and the stored coupling gains.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
G : TNFRGraph
|
|
175
|
+
Graph whose nodes expose TNFR phase attributes and ΔNFR telemetry. The
|
|
176
|
+
graph's ``history`` mapping is updated in-place when adaptive gain
|
|
177
|
+
smoothing is active.
|
|
178
|
+
global_force : float, optional
|
|
179
|
+
Override for the global coupling gain ``kG``. When provided, adaptive
|
|
180
|
+
gain estimation is skipped and the global history buffers are left
|
|
181
|
+
untouched.
|
|
182
|
+
local_force : float, optional
|
|
183
|
+
Override for the local coupling gain ``kL``. Analogous to
|
|
184
|
+
``global_force``, the adaptive pathway is bypassed when supplied.
|
|
185
|
+
n_jobs : int, optional
|
|
186
|
+
Maximum number of worker processes for distributing local updates.
|
|
187
|
+
Values of ``None`` or ``<=1`` perform updates sequentially. NumPy
|
|
188
|
+
availability forces sequential execution because vectorised updates are
|
|
189
|
+
faster than multiprocess handoffs.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
None
|
|
194
|
+
This operator updates node phases in-place and does not allocate a new
|
|
195
|
+
graph structure.
|
|
196
|
+
|
|
197
|
+
Examples
|
|
198
|
+
--------
|
|
199
|
+
Coordinate phase on a minimal TNFR network while inspecting ΔNFR telemetry
|
|
200
|
+
and history traces::
|
|
201
|
+
|
|
202
|
+
>>> import networkx as nx
|
|
203
|
+
>>> from tnfr.dynamics.coordination import coordinate_global_local_phase
|
|
204
|
+
>>> G = nx.Graph()
|
|
205
|
+
>>> G.add_nodes_from(("a", {"theta": 0.0, "ΔNFR": 0.08}),
|
|
206
|
+
... ("b", {"theta": 1.2, "ΔNFR": -0.05}))
|
|
207
|
+
>>> G.add_edge("a", "b")
|
|
208
|
+
>>> G.graph["history"] = {}
|
|
209
|
+
>>> coordinate_global_local_phase(G)
|
|
210
|
+
>>> list(round(G.nodes[n]["theta"], 3) for n in G)
|
|
211
|
+
[0.578, 0.622]
|
|
212
|
+
>>> history = G.graph["history"]
|
|
213
|
+
>>> sorted(history)
|
|
214
|
+
['phase_R', 'phase_disr', 'phase_kG', 'phase_kL', 'phase_state']
|
|
215
|
+
>>> history["phase_kG"][-1] <= history["phase_kL"][-1]
|
|
216
|
+
True
|
|
217
|
+
|
|
218
|
+
The resulting history buffers allow downstream observers to correlate
|
|
219
|
+
ΔNFR adjustments with phase telemetry snapshots.
|
|
220
|
+
"""
|
|
176
221
|
|
|
177
222
|
g = cast(dict[str, Any], G.graph)
|
|
178
223
|
hist = cast(dict[str, Any], g.setdefault("history", {}))
|
|
179
|
-
maxlen = int(
|
|
180
|
-
g.get("PHASE_HISTORY_MAXLEN", METRIC_DEFAULTS["PHASE_HISTORY_MAXLEN"])
|
|
181
|
-
)
|
|
224
|
+
maxlen = int(g.get("PHASE_HISTORY_MAXLEN", METRIC_DEFAULTS["PHASE_HISTORY_MAXLEN"]))
|
|
182
225
|
hist_state = cast(deque[str], _ensure_hist_deque(hist, "phase_state", maxlen))
|
|
183
226
|
if hist_state:
|
|
184
227
|
normalised_states = [normalise_state_token(item) for item in hist_state]
|
|
@@ -254,12 +297,10 @@ def coordinate_global_local_phase(
|
|
|
254
297
|
|
|
255
298
|
theta_vals = [_theta_value(n) for n in nodes]
|
|
256
299
|
cos_vals = [
|
|
257
|
-
float(cos_map.get(n, math.cos(theta_vals[idx])))
|
|
258
|
-
for idx, n in enumerate(nodes)
|
|
300
|
+
float(cos_map.get(n, math.cos(theta_vals[idx]))) for idx, n in enumerate(nodes)
|
|
259
301
|
]
|
|
260
302
|
sin_vals = [
|
|
261
|
-
float(sin_map.get(n, math.sin(theta_vals[idx])))
|
|
262
|
-
for idx, n in enumerate(nodes)
|
|
303
|
+
float(sin_map.get(n, math.sin(theta_vals[idx]))) for idx, n in enumerate(nodes)
|
|
263
304
|
]
|
|
264
305
|
|
|
265
306
|
if np is not None:
|
|
@@ -283,8 +324,8 @@ def coordinate_global_local_phase(
|
|
|
283
324
|
for idx, n in enumerate(nodes)
|
|
284
325
|
]
|
|
285
326
|
neighbor_arr = cast(FloatArray, np.fromiter(neighbor_means, dtype=float))
|
|
286
|
-
theta_updates =
|
|
287
|
-
neighbor_arr - theta_arr
|
|
327
|
+
theta_updates = (
|
|
328
|
+
theta_arr + kG * (thG - theta_arr) + kL * (neighbor_arr - theta_arr)
|
|
288
329
|
)
|
|
289
330
|
for idx, node in enumerate(nodes):
|
|
290
331
|
set_theta(G, node, float(theta_updates[int(idx)]))
|
|
@@ -313,11 +354,13 @@ def coordinate_global_local_phase(
|
|
|
313
354
|
set_theta(G, node, float(th + kG * dG + kL * dL))
|
|
314
355
|
return
|
|
315
356
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
357
|
+
approx_chunk = math.ceil(len(nodes) / jobs) if jobs else None
|
|
358
|
+
chunk_size = resolve_chunk_size(
|
|
359
|
+
approx_chunk,
|
|
360
|
+
len(nodes),
|
|
361
|
+
minimum=1,
|
|
362
|
+
)
|
|
363
|
+
chunks = [nodes[idx : idx + chunk_size] for idx in range(0, len(nodes), chunk_size)]
|
|
321
364
|
args: list[ChunkArgs] = [
|
|
322
365
|
(
|
|
323
366
|
chunk,
|
|
@@ -340,4 +383,3 @@ def coordinate_global_local_phase(
|
|
|
340
383
|
new_theta = results.get(node)
|
|
341
384
|
base_theta = theta_map.get(node, 0.0)
|
|
342
385
|
set_theta(G, node, float(new_theta if new_theta is not None else base_theta))
|
|
343
|
-
|