tnfr 4.5.2__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 +275 -51
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +117 -31
- tnfr/alias.pyi +108 -0
- tnfr/cache.py +6 -572
- tnfr/cache.pyi +16 -0
- tnfr/callback_utils.py +16 -38
- tnfr/callback_utils.pyi +79 -0
- tnfr/cli/__init__.py +34 -14
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +211 -28
- tnfr/cli/arguments.pyi +27 -0
- tnfr/cli/execution.py +470 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/utils.py +18 -3
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +13 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/feature_flags.py +83 -0
- tnfr/{config.py → config/init.py} +11 -7
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +93 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +84 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +80 -29
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +31 -0
- tnfr/constants/core.py +4 -4
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +1 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +7 -15
- tnfr/constants/metric.pyi +19 -0
- tnfr/dynamics/__init__.py +165 -633
- tnfr/dynamics/__init__.pyi +82 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/dnfr.py +2283 -400
- tnfr/dynamics/dnfr.pyi +24 -0
- tnfr/dynamics/integrators.py +406 -98
- tnfr/dynamics/integrators.pyi +34 -0
- tnfr/dynamics/runtime.py +881 -0
- tnfr/dynamics/sampling.py +10 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +719 -0
- tnfr/execution.py +70 -48
- tnfr/execution.pyi +45 -0
- tnfr/flatten.py +13 -9
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +34 -0
- tnfr/glyph_history.py +110 -52
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +16 -0
- tnfr/glyph_runtime.pyi +9 -0
- tnfr/immutable.py +69 -28
- tnfr/immutable.pyi +34 -0
- tnfr/initialization.py +16 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +6 -240
- tnfr/io.pyi +16 -0
- tnfr/locking.pyi +7 -0
- 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/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +993 -324
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +46 -0
- tnfr/metrics/core.py +251 -35
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +708 -111
- tnfr/metrics/diagnosis.pyi +85 -0
- tnfr/metrics/export.py +27 -15
- tnfr/metrics/glyph_timing.py +232 -42
- tnfr/metrics/reporting.py +33 -22
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +987 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +214 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +115 -22
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +542 -136
- tnfr/node.pyi +178 -0
- tnfr/observers.py +152 -35
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +601 -82
- tnfr/operators/__init__.pyi +45 -0
- tnfr/operators/definitions.py +513 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +760 -0
- tnfr/operators/jitter.py +107 -38
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +75 -0
- tnfr/operators/registry.pyi +13 -0
- tnfr/operators/remesh.py +149 -88
- tnfr/py.typed +0 -0
- tnfr/rng.py +46 -143
- tnfr/rng.pyi +14 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/selector.py +25 -19
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +72 -62
- tnfr/sense.pyi +23 -0
- tnfr/structural.py +522 -262
- tnfr/structural.pyi +69 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/nu_f.py +423 -0
- tnfr/telemetry/nu_f.pyi +123 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +1 -3
- tnfr/tokens.pyi +36 -0
- tnfr/trace.py +270 -113
- tnfr/trace.pyi +40 -0
- tnfr/types.py +574 -6
- tnfr/types.pyi +331 -0
- tnfr/units.py +69 -0
- tnfr/units.pyi +16 -0
- tnfr/utils/__init__.py +217 -0
- tnfr/utils/__init__.pyi +202 -0
- tnfr/utils/cache.py +2395 -0
- tnfr/utils/cache.pyi +468 -0
- tnfr/utils/chunks.py +104 -0
- tnfr/utils/chunks.pyi +21 -0
- tnfr/{collections_utils.py → utils/data.py} +147 -90
- tnfr/utils/data.pyi +64 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +770 -0
- tnfr/utils/init.pyi +78 -0
- tnfr/utils/io.py +456 -0
- tnfr/{helpers → utils}/numeric.py +51 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +113 -0
- tnfr/validation/__init__.pyi +77 -0
- tnfr/validation/compatibility.py +95 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/grammar.py +71 -0
- tnfr/validation/grammar.pyi +40 -0
- tnfr/validation/graph.py +138 -0
- tnfr/validation/graph.pyi +17 -0
- tnfr/validation/rules.py +281 -0
- tnfr/validation/rules.pyi +55 -0
- 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 +40 -0
- tnfr/validation/syntax.pyi +10 -0
- 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-7.0.0.dist-info/METADATA +179 -0
- tnfr-7.0.0.dist-info/RECORD +185 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/grammar.py +0 -344
- tnfr/graph_utils.py +0 -84
- tnfr/helpers/__init__.py +0 -71
- tnfr/import_utils.py +0 -228
- tnfr/json_utils.py +0 -162
- tnfr/logging_utils.py +0 -116
- tnfr/presets.py +0 -60
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Phase coordination helpers for TNFR dynamics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections import deque
|
|
7
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
8
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
9
|
+
from typing import Any, TypeVar, cast
|
|
10
|
+
from ..alias import get_theta_attr, set_theta
|
|
11
|
+
from ..constants import (
|
|
12
|
+
DEFAULTS,
|
|
13
|
+
METRIC_DEFAULTS,
|
|
14
|
+
STATE_DISSONANT,
|
|
15
|
+
STATE_STABLE,
|
|
16
|
+
STATE_TRANSITION,
|
|
17
|
+
normalise_state_token,
|
|
18
|
+
)
|
|
19
|
+
from ..glyph_history import append_metric
|
|
20
|
+
from ..utils import angle_diff, resolve_chunk_size
|
|
21
|
+
from ..metrics.common import ensure_neighbors_map
|
|
22
|
+
from ..metrics.trig import neighbor_phase_mean_list
|
|
23
|
+
from ..metrics.trig_cache import get_trig_cache
|
|
24
|
+
from ..observers import DEFAULT_GLYPH_LOAD_SPAN, glyph_load, kuramoto_order
|
|
25
|
+
from ..types import FloatArray, NodeId, Phase, TNFRGraph
|
|
26
|
+
from ..utils import get_numpy
|
|
27
|
+
|
|
28
|
+
_DequeT = TypeVar("_DequeT")
|
|
29
|
+
|
|
30
|
+
ChunkArgs = tuple[
|
|
31
|
+
Sequence[NodeId],
|
|
32
|
+
Mapping[NodeId, Phase],
|
|
33
|
+
Mapping[NodeId, float],
|
|
34
|
+
Mapping[NodeId, float],
|
|
35
|
+
Mapping[NodeId, Sequence[NodeId]],
|
|
36
|
+
float,
|
|
37
|
+
float,
|
|
38
|
+
float,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
__all__ = ("coordinate_global_local_phase",)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ensure_hist_deque(
|
|
45
|
+
hist: MutableMapping[str, Any], key: str, maxlen: int
|
|
46
|
+
) -> deque[_DequeT]:
|
|
47
|
+
"""Ensure history entry ``key`` is a deque with ``maxlen``."""
|
|
48
|
+
|
|
49
|
+
dq = hist.setdefault(key, deque(maxlen=maxlen))
|
|
50
|
+
if not isinstance(dq, deque):
|
|
51
|
+
dq = deque(dq, maxlen=maxlen)
|
|
52
|
+
hist[key] = dq
|
|
53
|
+
return cast("deque[_DequeT]", dq)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_adaptive_params(
|
|
57
|
+
g: Mapping[str, Any],
|
|
58
|
+
) -> tuple[Mapping[str, Any], float, float]:
|
|
59
|
+
"""Obtain configuration and current values for phase adaptation."""
|
|
60
|
+
|
|
61
|
+
cfg = g.get("PHASE_ADAPT", DEFAULTS.get("PHASE_ADAPT", {}))
|
|
62
|
+
kG = float(g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"]))
|
|
63
|
+
kL = float(g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"]))
|
|
64
|
+
return cast(Mapping[str, Any], cfg), kG, kL
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _compute_state(G: TNFRGraph, cfg: Mapping[str, Any]) -> tuple[str, float, float]:
|
|
68
|
+
"""Return the canonical network state and supporting metrics."""
|
|
69
|
+
|
|
70
|
+
R = kuramoto_order(G)
|
|
71
|
+
dist = glyph_load(G, window=DEFAULT_GLYPH_LOAD_SPAN)
|
|
72
|
+
disr = float(dist.get("_disruptors", 0.0)) if dist else 0.0
|
|
73
|
+
|
|
74
|
+
R_hi = float(cfg.get("R_hi", 0.90))
|
|
75
|
+
R_lo = float(cfg.get("R_lo", 0.60))
|
|
76
|
+
disr_hi = float(cfg.get("disr_hi", 0.50))
|
|
77
|
+
disr_lo = float(cfg.get("disr_lo", 0.25))
|
|
78
|
+
if (R >= R_hi) and (disr <= disr_lo):
|
|
79
|
+
state = STATE_STABLE
|
|
80
|
+
elif (R <= R_lo) or (disr >= disr_hi):
|
|
81
|
+
state = STATE_DISSONANT
|
|
82
|
+
else:
|
|
83
|
+
state = STATE_TRANSITION
|
|
84
|
+
return state, float(R), disr
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _smooth_adjust_k(
|
|
88
|
+
kG: float, kL: float, state: str, cfg: Mapping[str, Any]
|
|
89
|
+
) -> tuple[float, float]:
|
|
90
|
+
"""Smoothly update kG/kL toward targets according to state."""
|
|
91
|
+
|
|
92
|
+
kG_min = float(cfg.get("kG_min", 0.01))
|
|
93
|
+
kG_max = float(cfg.get("kG_max", 0.20))
|
|
94
|
+
kL_min = float(cfg.get("kL_min", 0.05))
|
|
95
|
+
kL_max = float(cfg.get("kL_max", 0.25))
|
|
96
|
+
|
|
97
|
+
state = normalise_state_token(state)
|
|
98
|
+
|
|
99
|
+
if state == STATE_DISSONANT:
|
|
100
|
+
kG_t = kG_max
|
|
101
|
+
kL_t = 0.5 * (kL_min + kL_max) # keep kL mid-range to preserve local plasticity
|
|
102
|
+
elif state == STATE_STABLE:
|
|
103
|
+
kG_t = kG_min
|
|
104
|
+
kL_t = kL_min
|
|
105
|
+
else:
|
|
106
|
+
kG_t = 0.5 * (kG_min + kG_max)
|
|
107
|
+
kL_t = 0.5 * (kL_min + kL_max)
|
|
108
|
+
|
|
109
|
+
up = float(cfg.get("up", 0.10))
|
|
110
|
+
down = float(cfg.get("down", 0.07))
|
|
111
|
+
|
|
112
|
+
def _step(curr: float, target: float, mn: float, mx: float) -> float:
|
|
113
|
+
gain = up if target > curr else down
|
|
114
|
+
nxt = curr + gain * (target - curr)
|
|
115
|
+
return max(mn, min(mx, nxt))
|
|
116
|
+
|
|
117
|
+
return _step(kG, kG_t, kG_min, kG_max), _step(kL, kL_t, kL_min, kL_max)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _phase_adjust_chunk(args: ChunkArgs) -> list[tuple[NodeId, Phase]]:
|
|
121
|
+
"""Return coordinated phase updates for the provided chunk."""
|
|
122
|
+
|
|
123
|
+
(
|
|
124
|
+
nodes,
|
|
125
|
+
theta_map,
|
|
126
|
+
cos_map,
|
|
127
|
+
sin_map,
|
|
128
|
+
neighbors_map,
|
|
129
|
+
thG,
|
|
130
|
+
kG,
|
|
131
|
+
kL,
|
|
132
|
+
) = args
|
|
133
|
+
updates: list[tuple[NodeId, Phase]] = []
|
|
134
|
+
for node in nodes:
|
|
135
|
+
th = float(theta_map.get(node, 0.0))
|
|
136
|
+
neigh = neighbors_map.get(node, ())
|
|
137
|
+
if neigh:
|
|
138
|
+
thL = neighbor_phase_mean_list(
|
|
139
|
+
neigh,
|
|
140
|
+
cos_map,
|
|
141
|
+
sin_map,
|
|
142
|
+
np=None,
|
|
143
|
+
fallback=th,
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
thL = th
|
|
147
|
+
dG = angle_diff(thG, th)
|
|
148
|
+
dL = angle_diff(thL, th)
|
|
149
|
+
updates.append((node, cast(Phase, th + kG * dG + kL * dL)))
|
|
150
|
+
return updates
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def coordinate_global_local_phase(
|
|
154
|
+
G: TNFRGraph,
|
|
155
|
+
global_force: float | None = None,
|
|
156
|
+
local_force: float | None = None,
|
|
157
|
+
*,
|
|
158
|
+
n_jobs: int | None = None,
|
|
159
|
+
) -> None:
|
|
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
|
+
"""
|
|
221
|
+
|
|
222
|
+
g = cast(dict[str, Any], G.graph)
|
|
223
|
+
hist = cast(dict[str, Any], g.setdefault("history", {}))
|
|
224
|
+
maxlen = int(g.get("PHASE_HISTORY_MAXLEN", METRIC_DEFAULTS["PHASE_HISTORY_MAXLEN"]))
|
|
225
|
+
hist_state = cast(deque[str], _ensure_hist_deque(hist, "phase_state", maxlen))
|
|
226
|
+
if hist_state:
|
|
227
|
+
normalised_states = [normalise_state_token(item) for item in hist_state]
|
|
228
|
+
if normalised_states != list(hist_state):
|
|
229
|
+
hist_state.clear()
|
|
230
|
+
hist_state.extend(normalised_states)
|
|
231
|
+
hist_R = cast(deque[float], _ensure_hist_deque(hist, "phase_R", maxlen))
|
|
232
|
+
hist_disr = cast(deque[float], _ensure_hist_deque(hist, "phase_disr", maxlen))
|
|
233
|
+
|
|
234
|
+
if (global_force is not None) or (local_force is not None):
|
|
235
|
+
kG = float(
|
|
236
|
+
global_force
|
|
237
|
+
if global_force is not None
|
|
238
|
+
else g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"])
|
|
239
|
+
)
|
|
240
|
+
kL = float(
|
|
241
|
+
local_force
|
|
242
|
+
if local_force is not None
|
|
243
|
+
else g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"])
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
cfg, kG, kL = _read_adaptive_params(g)
|
|
247
|
+
|
|
248
|
+
if bool(cfg.get("enabled", False)):
|
|
249
|
+
state, R, disr = _compute_state(G, cfg)
|
|
250
|
+
kG, kL = _smooth_adjust_k(kG, kL, state, cfg)
|
|
251
|
+
|
|
252
|
+
hist_state.append(state)
|
|
253
|
+
hist_R.append(float(R))
|
|
254
|
+
hist_disr.append(float(disr))
|
|
255
|
+
|
|
256
|
+
g["PHASE_K_GLOBAL"] = kG
|
|
257
|
+
g["PHASE_K_LOCAL"] = kL
|
|
258
|
+
append_metric(hist, "phase_kG", float(kG))
|
|
259
|
+
append_metric(hist, "phase_kL", float(kL))
|
|
260
|
+
|
|
261
|
+
jobs: int | None
|
|
262
|
+
try:
|
|
263
|
+
jobs = None if n_jobs is None else int(n_jobs)
|
|
264
|
+
except (TypeError, ValueError):
|
|
265
|
+
jobs = None
|
|
266
|
+
if jobs is not None and jobs <= 1:
|
|
267
|
+
jobs = None
|
|
268
|
+
|
|
269
|
+
np = get_numpy()
|
|
270
|
+
if np is not None:
|
|
271
|
+
jobs = None
|
|
272
|
+
|
|
273
|
+
nodes: list[NodeId] = [cast(NodeId, node) for node in G.nodes()]
|
|
274
|
+
num_nodes = len(nodes)
|
|
275
|
+
if not num_nodes:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
trig = get_trig_cache(G, np=np)
|
|
279
|
+
theta_map = cast(dict[NodeId, Phase], trig.theta)
|
|
280
|
+
cos_map = cast(dict[NodeId, float], trig.cos)
|
|
281
|
+
sin_map = cast(dict[NodeId, float], trig.sin)
|
|
282
|
+
|
|
283
|
+
neighbors_proxy = ensure_neighbors_map(G)
|
|
284
|
+
neighbors_map: dict[NodeId, tuple[NodeId, ...]] = {}
|
|
285
|
+
for n in nodes:
|
|
286
|
+
try:
|
|
287
|
+
neighbors_map[n] = tuple(cast(Sequence[NodeId], neighbors_proxy[n]))
|
|
288
|
+
except KeyError:
|
|
289
|
+
neighbors_map[n] = ()
|
|
290
|
+
|
|
291
|
+
def _theta_value(node: NodeId) -> float:
|
|
292
|
+
cached = theta_map.get(node)
|
|
293
|
+
if cached is not None:
|
|
294
|
+
return float(cached)
|
|
295
|
+
attr_val = get_theta_attr(G.nodes[node], 0.0)
|
|
296
|
+
return float(attr_val if attr_val is not None else 0.0)
|
|
297
|
+
|
|
298
|
+
theta_vals = [_theta_value(n) for n in nodes]
|
|
299
|
+
cos_vals = [
|
|
300
|
+
float(cos_map.get(n, math.cos(theta_vals[idx]))) for idx, n in enumerate(nodes)
|
|
301
|
+
]
|
|
302
|
+
sin_vals = [
|
|
303
|
+
float(sin_map.get(n, math.sin(theta_vals[idx]))) for idx, n in enumerate(nodes)
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
if np is not None:
|
|
307
|
+
theta_arr = cast(FloatArray, np.fromiter(theta_vals, dtype=float))
|
|
308
|
+
cos_arr = cast(FloatArray, np.fromiter(cos_vals, dtype=float))
|
|
309
|
+
sin_arr = cast(FloatArray, np.fromiter(sin_vals, dtype=float))
|
|
310
|
+
if cos_arr.size:
|
|
311
|
+
mean_cos = float(np.mean(cos_arr))
|
|
312
|
+
mean_sin = float(np.mean(sin_arr))
|
|
313
|
+
thG = float(np.arctan2(mean_sin, mean_cos))
|
|
314
|
+
else:
|
|
315
|
+
thG = 0.0
|
|
316
|
+
neighbor_means = [
|
|
317
|
+
neighbor_phase_mean_list(
|
|
318
|
+
neighbors_map.get(n, ()),
|
|
319
|
+
cos_map,
|
|
320
|
+
sin_map,
|
|
321
|
+
np=np,
|
|
322
|
+
fallback=theta_vals[idx],
|
|
323
|
+
)
|
|
324
|
+
for idx, n in enumerate(nodes)
|
|
325
|
+
]
|
|
326
|
+
neighbor_arr = cast(FloatArray, np.fromiter(neighbor_means, dtype=float))
|
|
327
|
+
theta_updates = (
|
|
328
|
+
theta_arr + kG * (thG - theta_arr) + kL * (neighbor_arr - theta_arr)
|
|
329
|
+
)
|
|
330
|
+
for idx, node in enumerate(nodes):
|
|
331
|
+
set_theta(G, node, float(theta_updates[int(idx)]))
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
mean_cos = math.fsum(cos_vals) / num_nodes
|
|
335
|
+
mean_sin = math.fsum(sin_vals) / num_nodes
|
|
336
|
+
thG = math.atan2(mean_sin, mean_cos)
|
|
337
|
+
|
|
338
|
+
if jobs is None:
|
|
339
|
+
for node in nodes:
|
|
340
|
+
th = float(theta_map.get(node, 0.0))
|
|
341
|
+
neigh = neighbors_map.get(node, ())
|
|
342
|
+
if neigh:
|
|
343
|
+
thL = neighbor_phase_mean_list(
|
|
344
|
+
neigh,
|
|
345
|
+
cos_map,
|
|
346
|
+
sin_map,
|
|
347
|
+
np=None,
|
|
348
|
+
fallback=th,
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
thL = th
|
|
352
|
+
dG = angle_diff(thG, th)
|
|
353
|
+
dL = angle_diff(thL, th)
|
|
354
|
+
set_theta(G, node, float(th + kG * dG + kL * dL))
|
|
355
|
+
return
|
|
356
|
+
|
|
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)]
|
|
364
|
+
args: list[ChunkArgs] = [
|
|
365
|
+
(
|
|
366
|
+
chunk,
|
|
367
|
+
theta_map,
|
|
368
|
+
cos_map,
|
|
369
|
+
sin_map,
|
|
370
|
+
neighbors_map,
|
|
371
|
+
thG,
|
|
372
|
+
kG,
|
|
373
|
+
kL,
|
|
374
|
+
)
|
|
375
|
+
for chunk in chunks
|
|
376
|
+
]
|
|
377
|
+
results: dict[NodeId, Phase] = {}
|
|
378
|
+
with ProcessPoolExecutor(max_workers=jobs) as executor:
|
|
379
|
+
for res in executor.map(_phase_adjust_chunk, args):
|
|
380
|
+
for node, value in res:
|
|
381
|
+
results[node] = value
|
|
382
|
+
for node in nodes:
|
|
383
|
+
new_theta = results.get(node)
|
|
384
|
+
base_theta = theta_map.get(node, 0.0)
|
|
385
|
+
set_theta(G, node, float(new_theta if new_theta is not None else base_theta))
|