tnfr 4.5.1__py3-none-any.whl → 6.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.
- tnfr/__init__.py +270 -90
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +631 -0
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +732 -0
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +381 -0
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +89 -0
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +199 -0
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +322 -0
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +34 -0
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +12 -0
- tnfr/config/__init__.pyi +8 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/init.py +36 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +106 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +104 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +228 -0
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +158 -0
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +102 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +16 -0
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +136 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +201 -0
- tnfr/dynamics/aliases.py +22 -0
- tnfr/dynamics/coordination.py +343 -0
- tnfr/dynamics/dnfr.py +2315 -0
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +561 -0
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +34 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +216 -0
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +283 -0
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +320 -89
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +337 -0
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +23 -153
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +151 -0
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +88 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +214 -0
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +199 -0
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +311 -0
- tnfr/io.pyi +11 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +1469 -0
- tnfr/metrics/common.py +149 -0
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +259 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +840 -0
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +151 -0
- tnfr/metrics/glyph_timing.py +369 -0
- tnfr/metrics/reporting.py +152 -0
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +294 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +216 -0
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +105 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +255 -177
- tnfr/node.pyi +161 -0
- tnfr/observers.py +154 -150
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +135 -134
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +452 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +266 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +569 -0
- tnfr/presets.py +10 -23
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +440 -0
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +217 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +307 -142
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +69 -164
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +61 -0
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +520 -95
- tnfr/trace.pyi +68 -0
- tnfr/types.py +382 -17
- tnfr/types.pyi +145 -0
- tnfr/utils/__init__.py +158 -0
- tnfr/utils/__init__.pyi +133 -0
- tnfr/utils/cache.py +755 -0
- tnfr/utils/cache.pyi +156 -0
- tnfr/utils/data.py +267 -0
- tnfr/utils/data.pyi +73 -0
- tnfr/utils/graph.py +87 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +746 -0
- tnfr/utils/init.pyi +85 -0
- tnfr/utils/io.py +157 -0
- tnfr/utils/io.pyi +10 -0
- tnfr/utils/validators.py +130 -0
- tnfr/utils/validators.pyi +19 -0
- tnfr/validation/__init__.py +25 -0
- tnfr/validation/__init__.pyi +17 -0
- tnfr/validation/compatibility.py +59 -0
- tnfr/validation/compatibility.pyi +8 -0
- tnfr/validation/grammar.py +149 -0
- tnfr/validation/grammar.pyi +11 -0
- tnfr/validation/rules.py +194 -0
- tnfr/validation/rules.pyi +18 -0
- tnfr/validation/syntax.py +151 -0
- tnfr/validation/syntax.pyi +7 -0
- tnfr-6.0.0.dist-info/METADATA +135 -0
- tnfr-6.0.0.dist-info/RECORD +157 -0
- tnfr/cli.py +0 -322
- tnfr/config.py +0 -41
- tnfr/constants.py +0 -277
- tnfr/dynamics.py +0 -814
- tnfr/helpers.py +0 -264
- tnfr/main.py +0 -47
- tnfr/metrics.py +0 -597
- tnfr/operators.py +0 -525
- tnfr/program.py +0 -176
- tnfr/scenarios.py +0 -34
- tnfr/validators.py +0 -38
- tnfr-4.5.1.dist-info/METADATA +0 -221
- tnfr-4.5.1.dist-info/RECORD +0 -28
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
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 TYPE_CHECKING, Any, TypeVar, cast
|
|
10
|
+
|
|
11
|
+
from ..alias import get_theta_attr, set_theta
|
|
12
|
+
from ..constants import (
|
|
13
|
+
DEFAULTS,
|
|
14
|
+
METRIC_DEFAULTS,
|
|
15
|
+
STATE_DISSONANT,
|
|
16
|
+
STATE_STABLE,
|
|
17
|
+
STATE_TRANSITION,
|
|
18
|
+
normalise_state_token,
|
|
19
|
+
)
|
|
20
|
+
from ..glyph_history import append_metric
|
|
21
|
+
from ..helpers.numeric import angle_diff
|
|
22
|
+
from ..metrics.common import ensure_neighbors_map
|
|
23
|
+
from ..metrics.trig import neighbor_phase_mean_list
|
|
24
|
+
from ..metrics.trig_cache import get_trig_cache
|
|
25
|
+
from ..observers import DEFAULT_GLYPH_LOAD_SPAN, glyph_load, kuramoto_order
|
|
26
|
+
from ..types import NodeId, Phase, TNFRGraph
|
|
27
|
+
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
|
+
|
|
41
|
+
_DequeT = TypeVar("_DequeT")
|
|
42
|
+
|
|
43
|
+
ChunkArgs = tuple[
|
|
44
|
+
Sequence[NodeId],
|
|
45
|
+
Mapping[NodeId, Phase],
|
|
46
|
+
Mapping[NodeId, float],
|
|
47
|
+
Mapping[NodeId, float],
|
|
48
|
+
Mapping[NodeId, Sequence[NodeId]],
|
|
49
|
+
float,
|
|
50
|
+
float,
|
|
51
|
+
float,
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
__all__ = ("coordinate_global_local_phase",)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ensure_hist_deque(
|
|
58
|
+
hist: MutableMapping[str, Any], key: str, maxlen: int
|
|
59
|
+
) -> deque[_DequeT]:
|
|
60
|
+
"""Ensure history entry ``key`` is a deque with ``maxlen``."""
|
|
61
|
+
|
|
62
|
+
dq = hist.setdefault(key, deque(maxlen=maxlen))
|
|
63
|
+
if not isinstance(dq, deque):
|
|
64
|
+
dq = deque(dq, maxlen=maxlen)
|
|
65
|
+
hist[key] = dq
|
|
66
|
+
return cast("deque[_DequeT]", dq)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_adaptive_params(
|
|
70
|
+
g: Mapping[str, Any],
|
|
71
|
+
) -> tuple[Mapping[str, Any], float, float]:
|
|
72
|
+
"""Obtain configuration and current values for phase adaptation."""
|
|
73
|
+
|
|
74
|
+
cfg = g.get("PHASE_ADAPT", DEFAULTS.get("PHASE_ADAPT", {}))
|
|
75
|
+
kG = float(g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"]))
|
|
76
|
+
kL = float(g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"]))
|
|
77
|
+
return cast(Mapping[str, Any], cfg), kG, kL
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _compute_state(G: TNFRGraph, cfg: Mapping[str, Any]) -> tuple[str, float, float]:
|
|
81
|
+
"""Return the canonical network state and supporting metrics."""
|
|
82
|
+
|
|
83
|
+
R = kuramoto_order(G)
|
|
84
|
+
dist = glyph_load(G, window=DEFAULT_GLYPH_LOAD_SPAN)
|
|
85
|
+
disr = float(dist.get("_disruptors", 0.0)) if dist else 0.0
|
|
86
|
+
|
|
87
|
+
R_hi = float(cfg.get("R_hi", 0.90))
|
|
88
|
+
R_lo = float(cfg.get("R_lo", 0.60))
|
|
89
|
+
disr_hi = float(cfg.get("disr_hi", 0.50))
|
|
90
|
+
disr_lo = float(cfg.get("disr_lo", 0.25))
|
|
91
|
+
if (R >= R_hi) and (disr <= disr_lo):
|
|
92
|
+
state = STATE_STABLE
|
|
93
|
+
elif (R <= R_lo) or (disr >= disr_hi):
|
|
94
|
+
state = STATE_DISSONANT
|
|
95
|
+
else:
|
|
96
|
+
state = STATE_TRANSITION
|
|
97
|
+
return state, float(R), disr
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _smooth_adjust_k(
|
|
101
|
+
kG: float, kL: float, state: str, cfg: Mapping[str, Any]
|
|
102
|
+
) -> tuple[float, float]:
|
|
103
|
+
"""Smoothly update kG/kL toward targets according to state."""
|
|
104
|
+
|
|
105
|
+
kG_min = float(cfg.get("kG_min", 0.01))
|
|
106
|
+
kG_max = float(cfg.get("kG_max", 0.20))
|
|
107
|
+
kL_min = float(cfg.get("kL_min", 0.05))
|
|
108
|
+
kL_max = float(cfg.get("kL_max", 0.25))
|
|
109
|
+
|
|
110
|
+
state = normalise_state_token(state)
|
|
111
|
+
|
|
112
|
+
if state == STATE_DISSONANT:
|
|
113
|
+
kG_t = kG_max
|
|
114
|
+
kL_t = 0.5 * (
|
|
115
|
+
kL_min + kL_max
|
|
116
|
+
) # local medio para no perder plasticidad
|
|
117
|
+
elif state == STATE_STABLE:
|
|
118
|
+
kG_t = kG_min
|
|
119
|
+
kL_t = kL_min
|
|
120
|
+
else:
|
|
121
|
+
kG_t = 0.5 * (kG_min + kG_max)
|
|
122
|
+
kL_t = 0.5 * (kL_min + kL_max)
|
|
123
|
+
|
|
124
|
+
up = float(cfg.get("up", 0.10))
|
|
125
|
+
down = float(cfg.get("down", 0.07))
|
|
126
|
+
|
|
127
|
+
def _step(curr: float, target: float, mn: float, mx: float) -> float:
|
|
128
|
+
gain = up if target > curr else down
|
|
129
|
+
nxt = curr + gain * (target - curr)
|
|
130
|
+
return max(mn, min(mx, nxt))
|
|
131
|
+
|
|
132
|
+
return _step(kG, kG_t, kG_min, kG_max), _step(kL, kL_t, kL_min, kL_max)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _phase_adjust_chunk(args: ChunkArgs) -> list[tuple[NodeId, Phase]]:
|
|
136
|
+
"""Return coordinated phase updates for the provided chunk."""
|
|
137
|
+
|
|
138
|
+
(
|
|
139
|
+
nodes,
|
|
140
|
+
theta_map,
|
|
141
|
+
cos_map,
|
|
142
|
+
sin_map,
|
|
143
|
+
neighbors_map,
|
|
144
|
+
thG,
|
|
145
|
+
kG,
|
|
146
|
+
kL,
|
|
147
|
+
) = args
|
|
148
|
+
updates: list[tuple[NodeId, Phase]] = []
|
|
149
|
+
for node in nodes:
|
|
150
|
+
th = float(theta_map.get(node, 0.0))
|
|
151
|
+
neigh = neighbors_map.get(node, ())
|
|
152
|
+
if neigh:
|
|
153
|
+
thL = neighbor_phase_mean_list(
|
|
154
|
+
neigh,
|
|
155
|
+
cos_map,
|
|
156
|
+
sin_map,
|
|
157
|
+
np=None,
|
|
158
|
+
fallback=th,
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
thL = th
|
|
162
|
+
dG = angle_diff(thG, th)
|
|
163
|
+
dL = angle_diff(thL, th)
|
|
164
|
+
updates.append((node, cast(Phase, th + kG * dG + kL * dL)))
|
|
165
|
+
return updates
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def coordinate_global_local_phase(
|
|
169
|
+
G: TNFRGraph,
|
|
170
|
+
global_force: float | None = None,
|
|
171
|
+
local_force: float | None = None,
|
|
172
|
+
*,
|
|
173
|
+
n_jobs: int | None = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Coordinate phase using a blend of global and neighbour coupling."""
|
|
176
|
+
|
|
177
|
+
g = cast(dict[str, Any], G.graph)
|
|
178
|
+
hist = cast(dict[str, Any], g.setdefault("history", {}))
|
|
179
|
+
maxlen = int(
|
|
180
|
+
g.get("PHASE_HISTORY_MAXLEN", METRIC_DEFAULTS["PHASE_HISTORY_MAXLEN"])
|
|
181
|
+
)
|
|
182
|
+
hist_state = cast(deque[str], _ensure_hist_deque(hist, "phase_state", maxlen))
|
|
183
|
+
if hist_state:
|
|
184
|
+
normalised_states = [normalise_state_token(item) for item in hist_state]
|
|
185
|
+
if normalised_states != list(hist_state):
|
|
186
|
+
hist_state.clear()
|
|
187
|
+
hist_state.extend(normalised_states)
|
|
188
|
+
hist_R = cast(deque[float], _ensure_hist_deque(hist, "phase_R", maxlen))
|
|
189
|
+
hist_disr = cast(deque[float], _ensure_hist_deque(hist, "phase_disr", maxlen))
|
|
190
|
+
|
|
191
|
+
if (global_force is not None) or (local_force is not None):
|
|
192
|
+
kG = float(
|
|
193
|
+
global_force
|
|
194
|
+
if global_force is not None
|
|
195
|
+
else g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"])
|
|
196
|
+
)
|
|
197
|
+
kL = float(
|
|
198
|
+
local_force
|
|
199
|
+
if local_force is not None
|
|
200
|
+
else g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"])
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
cfg, kG, kL = _read_adaptive_params(g)
|
|
204
|
+
|
|
205
|
+
if bool(cfg.get("enabled", False)):
|
|
206
|
+
state, R, disr = _compute_state(G, cfg)
|
|
207
|
+
kG, kL = _smooth_adjust_k(kG, kL, state, cfg)
|
|
208
|
+
|
|
209
|
+
hist_state.append(state)
|
|
210
|
+
hist_R.append(float(R))
|
|
211
|
+
hist_disr.append(float(disr))
|
|
212
|
+
|
|
213
|
+
g["PHASE_K_GLOBAL"] = kG
|
|
214
|
+
g["PHASE_K_LOCAL"] = kL
|
|
215
|
+
append_metric(hist, "phase_kG", float(kG))
|
|
216
|
+
append_metric(hist, "phase_kL", float(kL))
|
|
217
|
+
|
|
218
|
+
jobs: int | None
|
|
219
|
+
try:
|
|
220
|
+
jobs = None if n_jobs is None else int(n_jobs)
|
|
221
|
+
except (TypeError, ValueError):
|
|
222
|
+
jobs = None
|
|
223
|
+
if jobs is not None and jobs <= 1:
|
|
224
|
+
jobs = None
|
|
225
|
+
|
|
226
|
+
np = get_numpy()
|
|
227
|
+
if np is not None:
|
|
228
|
+
jobs = None
|
|
229
|
+
|
|
230
|
+
nodes: list[NodeId] = [cast(NodeId, node) for node in G.nodes()]
|
|
231
|
+
num_nodes = len(nodes)
|
|
232
|
+
if not num_nodes:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
trig = get_trig_cache(G, np=np)
|
|
236
|
+
theta_map = cast(dict[NodeId, Phase], trig.theta)
|
|
237
|
+
cos_map = cast(dict[NodeId, float], trig.cos)
|
|
238
|
+
sin_map = cast(dict[NodeId, float], trig.sin)
|
|
239
|
+
|
|
240
|
+
neighbors_proxy = ensure_neighbors_map(G)
|
|
241
|
+
neighbors_map: dict[NodeId, tuple[NodeId, ...]] = {}
|
|
242
|
+
for n in nodes:
|
|
243
|
+
try:
|
|
244
|
+
neighbors_map[n] = tuple(cast(Sequence[NodeId], neighbors_proxy[n]))
|
|
245
|
+
except KeyError:
|
|
246
|
+
neighbors_map[n] = ()
|
|
247
|
+
|
|
248
|
+
def _theta_value(node: NodeId) -> float:
|
|
249
|
+
cached = theta_map.get(node)
|
|
250
|
+
if cached is not None:
|
|
251
|
+
return float(cached)
|
|
252
|
+
attr_val = get_theta_attr(G.nodes[node], 0.0)
|
|
253
|
+
return float(attr_val if attr_val is not None else 0.0)
|
|
254
|
+
|
|
255
|
+
theta_vals = [_theta_value(n) for n in nodes]
|
|
256
|
+
cos_vals = [
|
|
257
|
+
float(cos_map.get(n, math.cos(theta_vals[idx])))
|
|
258
|
+
for idx, n in enumerate(nodes)
|
|
259
|
+
]
|
|
260
|
+
sin_vals = [
|
|
261
|
+
float(sin_map.get(n, math.sin(theta_vals[idx])))
|
|
262
|
+
for idx, n in enumerate(nodes)
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
if np is not None:
|
|
266
|
+
theta_arr = cast(FloatArray, np.fromiter(theta_vals, dtype=float))
|
|
267
|
+
cos_arr = cast(FloatArray, np.fromiter(cos_vals, dtype=float))
|
|
268
|
+
sin_arr = cast(FloatArray, np.fromiter(sin_vals, dtype=float))
|
|
269
|
+
if cos_arr.size:
|
|
270
|
+
mean_cos = float(np.mean(cos_arr))
|
|
271
|
+
mean_sin = float(np.mean(sin_arr))
|
|
272
|
+
thG = float(np.arctan2(mean_sin, mean_cos))
|
|
273
|
+
else:
|
|
274
|
+
thG = 0.0
|
|
275
|
+
neighbor_means = [
|
|
276
|
+
neighbor_phase_mean_list(
|
|
277
|
+
neighbors_map.get(n, ()),
|
|
278
|
+
cos_map,
|
|
279
|
+
sin_map,
|
|
280
|
+
np=np,
|
|
281
|
+
fallback=theta_vals[idx],
|
|
282
|
+
)
|
|
283
|
+
for idx, n in enumerate(nodes)
|
|
284
|
+
]
|
|
285
|
+
neighbor_arr = cast(FloatArray, np.fromiter(neighbor_means, dtype=float))
|
|
286
|
+
theta_updates = theta_arr + kG * (thG - theta_arr) + kL * (
|
|
287
|
+
neighbor_arr - theta_arr
|
|
288
|
+
)
|
|
289
|
+
for idx, node in enumerate(nodes):
|
|
290
|
+
set_theta(G, node, float(theta_updates[int(idx)]))
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
mean_cos = math.fsum(cos_vals) / num_nodes
|
|
294
|
+
mean_sin = math.fsum(sin_vals) / num_nodes
|
|
295
|
+
thG = math.atan2(mean_sin, mean_cos)
|
|
296
|
+
|
|
297
|
+
if jobs is None:
|
|
298
|
+
for node in nodes:
|
|
299
|
+
th = float(theta_map.get(node, 0.0))
|
|
300
|
+
neigh = neighbors_map.get(node, ())
|
|
301
|
+
if neigh:
|
|
302
|
+
thL = neighbor_phase_mean_list(
|
|
303
|
+
neigh,
|
|
304
|
+
cos_map,
|
|
305
|
+
sin_map,
|
|
306
|
+
np=None,
|
|
307
|
+
fallback=th,
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
thL = th
|
|
311
|
+
dG = angle_diff(thG, th)
|
|
312
|
+
dL = angle_diff(thL, th)
|
|
313
|
+
set_theta(G, node, float(th + kG * dG + kL * dL))
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
chunk_size = max(1, math.ceil(len(nodes) / jobs))
|
|
317
|
+
chunks = [
|
|
318
|
+
nodes[idx : idx + chunk_size]
|
|
319
|
+
for idx in range(0, len(nodes), chunk_size)
|
|
320
|
+
]
|
|
321
|
+
args: list[ChunkArgs] = [
|
|
322
|
+
(
|
|
323
|
+
chunk,
|
|
324
|
+
theta_map,
|
|
325
|
+
cos_map,
|
|
326
|
+
sin_map,
|
|
327
|
+
neighbors_map,
|
|
328
|
+
thG,
|
|
329
|
+
kG,
|
|
330
|
+
kL,
|
|
331
|
+
)
|
|
332
|
+
for chunk in chunks
|
|
333
|
+
]
|
|
334
|
+
results: dict[NodeId, Phase] = {}
|
|
335
|
+
with ProcessPoolExecutor(max_workers=jobs) as executor:
|
|
336
|
+
for res in executor.map(_phase_adjust_chunk, args):
|
|
337
|
+
for node, value in res:
|
|
338
|
+
results[node] = value
|
|
339
|
+
for node in nodes:
|
|
340
|
+
new_theta = results.get(node)
|
|
341
|
+
base_theta = theta_map.get(node, 0.0)
|
|
342
|
+
set_theta(G, node, float(new_theta if new_theta is not None else base_theta))
|
|
343
|
+
|