tnfr 4.5.2__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 +228 -49
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +106 -21
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +666 -512
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +2 -9
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +21 -7
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +42 -20
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +54 -20
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +0 -2
- 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.py → config/init.py} +11 -7
- 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 +78 -24
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +1 -2
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +4 -12
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +9 -91
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +112 -634
- 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 +1936 -354
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +369 -75
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +8 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +56 -41
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +7 -7
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +54 -37
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +85 -38
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +19 -338
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +110 -30
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +1 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +55 -19
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +12 -10
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +99 -34
- tnfr/io.pyi +11 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +934 -294
- tnfr/metrics/common.py +1 -3
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +192 -34
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +707 -101
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +27 -13
- tnfr/metrics/glyph_timing.py +218 -38
- tnfr/metrics/reporting.py +22 -18
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +199 -25
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +53 -18
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +3 -7
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +148 -125
- tnfr/node.pyi +161 -0
- tnfr/observers.py +44 -30
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +14 -13
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +84 -52
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +86 -23
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +141 -57
- tnfr/presets.py +9 -54
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +259 -73
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +24 -17
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +55 -43
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +44 -267
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +3 -2
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +272 -82
- tnfr/trace.pyi +68 -0
- tnfr/types.py +345 -6
- 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/{collections_utils.py → utils/data.py} +57 -90
- 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/{json_utils.py → utils/io.py} +13 -18
- 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/graph_utils.py +0 -84
- tnfr/import_utils.py +0 -228
- tnfr/logging_utils.py +0 -116
- 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-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/metrics/sense_index.py
CHANGED
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
6
7
|
from functools import partial
|
|
7
|
-
from typing import Any
|
|
8
|
+
from typing import Any, Iterable, Mapping
|
|
8
9
|
|
|
9
10
|
from ..alias import get_attr, set_attr
|
|
10
|
-
from ..collections_utils import normalize_weights
|
|
11
11
|
from ..constants import get_aliases
|
|
12
|
-
from ..cache import edge_version_cache, stable_json
|
|
13
12
|
from ..helpers.numeric import angle_diff, clamp01
|
|
14
|
-
from .trig import neighbor_phase_mean_list
|
|
15
|
-
from ..import_utils import get_numpy
|
|
16
13
|
from ..types import GraphLike
|
|
14
|
+
from ..utils import (
|
|
15
|
+
edge_version_cache,
|
|
16
|
+
get_numpy,
|
|
17
|
+
normalize_weights,
|
|
18
|
+
stable_json,
|
|
19
|
+
)
|
|
20
|
+
from .trig import neighbor_phase_mean_list
|
|
17
21
|
|
|
18
22
|
from .common import (
|
|
19
23
|
ensure_neighbors_map,
|
|
@@ -25,17 +29,44 @@ from .trig_cache import get_trig_cache
|
|
|
25
29
|
ALIAS_VF = get_aliases("VF")
|
|
26
30
|
ALIAS_DNFR = get_aliases("DNFR")
|
|
27
31
|
ALIAS_SI = get_aliases("SI")
|
|
28
|
-
ALIAS_THETA = get_aliases("THETA")
|
|
29
32
|
|
|
33
|
+
PHASE_DISPERSION_KEY = "dSi_dphase_disp"
|
|
34
|
+
_VALID_SENSITIVITY_KEYS = frozenset(
|
|
35
|
+
{"dSi_dvf_norm", PHASE_DISPERSION_KEY, "dSi_ddnfr_norm"}
|
|
36
|
+
)
|
|
30
37
|
__all__ = ("get_Si_weights", "compute_Si_node", "compute_Si")
|
|
31
38
|
|
|
32
39
|
|
|
40
|
+
def _normalise_si_sensitivity_mapping(
|
|
41
|
+
mapping: Mapping[str, float], *, warn: bool
|
|
42
|
+
) -> dict[str, float]:
|
|
43
|
+
"""Return a mapping containing only supported Si sensitivity keys."""
|
|
44
|
+
|
|
45
|
+
normalised = dict(mapping)
|
|
46
|
+
_ = warn # kept for API compatibility with trace helpers
|
|
47
|
+
unexpected = sorted(k for k in normalised if k not in _VALID_SENSITIVITY_KEYS)
|
|
48
|
+
if unexpected:
|
|
49
|
+
allowed = ", ".join(sorted(_VALID_SENSITIVITY_KEYS))
|
|
50
|
+
received = ", ".join(unexpected)
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"Si sensitivity mappings accept only {%s}; unexpected key(s): %s"
|
|
53
|
+
% (allowed, received)
|
|
54
|
+
)
|
|
55
|
+
return normalised
|
|
56
|
+
|
|
57
|
+
|
|
33
58
|
def _cache_weights(G: GraphLike) -> tuple[float, float, float]:
|
|
34
59
|
"""Normalise and cache Si weights, delegating persistence."""
|
|
35
60
|
|
|
36
61
|
w = merge_graph_weights(G, "SI_WEIGHTS")
|
|
37
62
|
cfg_key = stable_json(w)
|
|
38
63
|
|
|
64
|
+
existing = G.graph.get("_Si_sensitivity")
|
|
65
|
+
if isinstance(existing, Mapping):
|
|
66
|
+
migrated = _normalise_si_sensitivity_mapping(existing, warn=True)
|
|
67
|
+
if migrated != existing:
|
|
68
|
+
G.graph["_Si_sensitivity"] = migrated
|
|
69
|
+
|
|
39
70
|
def builder() -> tuple[float, float, float]:
|
|
40
71
|
weights = normalize_weights(w, ("alpha", "beta", "gamma"), default=0.0)
|
|
41
72
|
alpha = weights["alpha"]
|
|
@@ -45,7 +76,7 @@ def _cache_weights(G: GraphLike) -> tuple[float, float, float]:
|
|
|
45
76
|
G.graph["_Si_weights_key"] = cfg_key
|
|
46
77
|
G.graph["_Si_sensitivity"] = {
|
|
47
78
|
"dSi_dvf_norm": alpha,
|
|
48
|
-
|
|
79
|
+
PHASE_DISPERSION_KEY: -beta,
|
|
49
80
|
"dSi_ddnfr_norm": -gamma,
|
|
50
81
|
}
|
|
51
82
|
return alpha, beta, gamma
|
|
@@ -68,25 +99,84 @@ def compute_Si_node(
|
|
|
68
99
|
gamma: float,
|
|
69
100
|
vfmax: float,
|
|
70
101
|
dnfrmax: float,
|
|
71
|
-
|
|
102
|
+
phase_dispersion: float | None = None,
|
|
72
103
|
inplace: bool,
|
|
104
|
+
**kwargs: Any,
|
|
73
105
|
) -> float:
|
|
74
106
|
"""Compute ``Si`` for a single node."""
|
|
75
107
|
|
|
108
|
+
if kwargs:
|
|
109
|
+
unexpected = ", ".join(sorted(kwargs))
|
|
110
|
+
raise TypeError(f"Unexpected keyword argument(s): {unexpected}")
|
|
111
|
+
|
|
112
|
+
if phase_dispersion is None:
|
|
113
|
+
raise TypeError("Missing required keyword-only argument: 'phase_dispersion'")
|
|
114
|
+
|
|
76
115
|
vf = get_attr(nd, ALIAS_VF, 0.0)
|
|
77
116
|
vf_norm = clamp01(abs(vf) / vfmax)
|
|
78
117
|
|
|
79
118
|
dnfr = get_attr(nd, ALIAS_DNFR, 0.0)
|
|
80
119
|
dnfr_norm = clamp01(abs(dnfr) / dnfrmax)
|
|
81
120
|
|
|
82
|
-
Si =
|
|
121
|
+
Si = (
|
|
122
|
+
alpha * vf_norm
|
|
123
|
+
+ beta * (1.0 - phase_dispersion)
|
|
124
|
+
+ gamma * (1.0 - dnfr_norm)
|
|
125
|
+
)
|
|
83
126
|
Si = clamp01(Si)
|
|
84
127
|
if inplace:
|
|
85
128
|
set_attr(nd, ALIAS_SI, Si)
|
|
86
129
|
return Si
|
|
87
130
|
|
|
88
131
|
|
|
89
|
-
def
|
|
132
|
+
def _coerce_jobs(raw_jobs: Any | None) -> int | None:
|
|
133
|
+
"""Normalise ``n_jobs`` values coming from user configuration."""
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
jobs = None if raw_jobs is None else int(raw_jobs)
|
|
137
|
+
except (TypeError, ValueError):
|
|
138
|
+
return None
|
|
139
|
+
if jobs is not None and jobs <= 0:
|
|
140
|
+
return None
|
|
141
|
+
return jobs
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _compute_si_python_chunk(
|
|
145
|
+
chunk: Iterable[tuple[Any, tuple[Any, ...], float, float, float]],
|
|
146
|
+
*,
|
|
147
|
+
cos_th: dict[Any, float],
|
|
148
|
+
sin_th: dict[Any, float],
|
|
149
|
+
alpha: float,
|
|
150
|
+
beta: float,
|
|
151
|
+
gamma: float,
|
|
152
|
+
vfmax: float,
|
|
153
|
+
dnfrmax: float,
|
|
154
|
+
) -> dict[Any, float]:
|
|
155
|
+
"""Compute Si values for a chunk of nodes using pure Python math."""
|
|
156
|
+
|
|
157
|
+
results: dict[Any, float] = {}
|
|
158
|
+
for n, neigh, theta, vf, dnfr in chunk:
|
|
159
|
+
th_bar = neighbor_phase_mean_list(
|
|
160
|
+
neigh, cos_th=cos_th, sin_th=sin_th, np=None, fallback=theta
|
|
161
|
+
)
|
|
162
|
+
phase_dispersion = abs(angle_diff(theta, th_bar)) / math.pi
|
|
163
|
+
vf_norm = clamp01(abs(vf) / vfmax)
|
|
164
|
+
dnfr_norm = clamp01(abs(dnfr) / dnfrmax)
|
|
165
|
+
Si = (
|
|
166
|
+
alpha * vf_norm
|
|
167
|
+
+ beta * (1.0 - phase_dispersion)
|
|
168
|
+
+ gamma * (1.0 - dnfr_norm)
|
|
169
|
+
)
|
|
170
|
+
results[n] = clamp01(Si)
|
|
171
|
+
return results
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def compute_Si(
|
|
175
|
+
G: GraphLike,
|
|
176
|
+
*,
|
|
177
|
+
inplace: bool = True,
|
|
178
|
+
n_jobs: int | None = None,
|
|
179
|
+
) -> dict[Any, float]:
|
|
90
180
|
"""Compute ``Si`` per node and optionally store it on the graph."""
|
|
91
181
|
|
|
92
182
|
neighbors = ensure_neighbors_map(G)
|
|
@@ -101,20 +191,104 @@ def compute_Si(G: GraphLike, *, inplace: bool = True) -> dict[Any, float]:
|
|
|
101
191
|
neighbor_phase_mean_list, cos_th=cos_th, sin_th=sin_th, np=np
|
|
102
192
|
)
|
|
103
193
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
194
|
+
if n_jobs is None:
|
|
195
|
+
n_jobs = _coerce_jobs(G.graph.get("SI_N_JOBS"))
|
|
196
|
+
else:
|
|
197
|
+
n_jobs = _coerce_jobs(n_jobs)
|
|
198
|
+
|
|
199
|
+
supports_vector = (
|
|
200
|
+
np is not None
|
|
201
|
+
and hasattr(np, "ndarray")
|
|
202
|
+
and all(hasattr(np, attr) for attr in ("fromiter", "abs", "clip", "remainder"))
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
nodes_data = list(G.nodes(data=True))
|
|
206
|
+
if not nodes_data:
|
|
207
|
+
return {}
|
|
208
|
+
|
|
209
|
+
if supports_vector:
|
|
210
|
+
node_ids: list[Any] = []
|
|
211
|
+
theta_vals: list[float] = []
|
|
212
|
+
mean_vals: list[float] = []
|
|
213
|
+
vf_vals: list[float] = []
|
|
214
|
+
dnfr_vals: list[float] = []
|
|
215
|
+
for n, nd in nodes_data:
|
|
216
|
+
theta = thetas.get(n, 0.0)
|
|
217
|
+
neigh = neighbors[n]
|
|
218
|
+
node_ids.append(n)
|
|
219
|
+
theta_vals.append(theta)
|
|
220
|
+
mean_vals.append(pm_fn(neigh, fallback=theta))
|
|
221
|
+
vf_vals.append(get_attr(nd, ALIAS_VF, 0.0))
|
|
222
|
+
dnfr_vals.append(get_attr(nd, ALIAS_DNFR, 0.0))
|
|
223
|
+
|
|
224
|
+
count = len(node_ids)
|
|
225
|
+
theta_arr = np.fromiter(theta_vals, dtype=float, count=count)
|
|
226
|
+
mean_arr = np.fromiter(mean_vals, dtype=float, count=count)
|
|
227
|
+
diff = np.remainder(theta_arr - mean_arr + math.pi, math.tau) - math.pi
|
|
228
|
+
phase_dispersion_arr = np.abs(diff) / math.pi
|
|
229
|
+
|
|
230
|
+
vf_arr = np.fromiter(vf_vals, dtype=float, count=count)
|
|
231
|
+
dnfr_arr = np.fromiter(dnfr_vals, dtype=float, count=count)
|
|
232
|
+
vf_norm = np.clip(np.abs(vf_arr) / vfmax, 0.0, 1.0)
|
|
233
|
+
dnfr_norm = np.clip(np.abs(dnfr_arr) / dnfrmax, 0.0, 1.0)
|
|
234
|
+
|
|
235
|
+
si_arr = np.clip(
|
|
236
|
+
alpha * vf_norm + beta * (1.0 - phase_dispersion_arr)
|
|
237
|
+
+ gamma * (1.0 - dnfr_norm),
|
|
238
|
+
0.0,
|
|
239
|
+
1.0,
|
|
119
240
|
)
|
|
241
|
+
|
|
242
|
+
out = {node_ids[i]: float(si_arr[i]) for i in range(count)}
|
|
243
|
+
else:
|
|
244
|
+
out: dict[Any, float] = {}
|
|
245
|
+
if n_jobs is not None and n_jobs > 1:
|
|
246
|
+
node_payload: list[tuple[Any, tuple[Any, ...], float, float, float]] = []
|
|
247
|
+
for n, nd in nodes_data:
|
|
248
|
+
theta = thetas.get(n, 0.0)
|
|
249
|
+
vf = float(get_attr(nd, ALIAS_VF, 0.0))
|
|
250
|
+
dnfr = float(get_attr(nd, ALIAS_DNFR, 0.0))
|
|
251
|
+
neigh = neighbors[n]
|
|
252
|
+
node_payload.append((n, tuple(neigh), theta, vf, dnfr))
|
|
253
|
+
|
|
254
|
+
if node_payload:
|
|
255
|
+
chunk_size = math.ceil(len(node_payload) / n_jobs)
|
|
256
|
+
with ProcessPoolExecutor(max_workers=n_jobs) as executor:
|
|
257
|
+
futures = [
|
|
258
|
+
executor.submit(
|
|
259
|
+
_compute_si_python_chunk,
|
|
260
|
+
node_payload[idx:idx + chunk_size],
|
|
261
|
+
cos_th=cos_th,
|
|
262
|
+
sin_th=sin_th,
|
|
263
|
+
alpha=alpha,
|
|
264
|
+
beta=beta,
|
|
265
|
+
gamma=gamma,
|
|
266
|
+
vfmax=vfmax,
|
|
267
|
+
dnfrmax=dnfrmax,
|
|
268
|
+
)
|
|
269
|
+
for idx in range(0, len(node_payload), chunk_size)
|
|
270
|
+
]
|
|
271
|
+
for future in futures:
|
|
272
|
+
out.update(future.result())
|
|
273
|
+
else:
|
|
274
|
+
for n, nd in nodes_data:
|
|
275
|
+
theta = thetas.get(n, 0.0)
|
|
276
|
+
neigh = neighbors[n]
|
|
277
|
+
th_bar = pm_fn(neigh, fallback=theta)
|
|
278
|
+
phase_dispersion = abs(angle_diff(theta, th_bar)) / math.pi
|
|
279
|
+
out[n] = compute_Si_node(
|
|
280
|
+
n,
|
|
281
|
+
nd,
|
|
282
|
+
alpha=alpha,
|
|
283
|
+
beta=beta,
|
|
284
|
+
gamma=gamma,
|
|
285
|
+
vfmax=vfmax,
|
|
286
|
+
dnfrmax=dnfrmax,
|
|
287
|
+
phase_dispersion=phase_dispersion,
|
|
288
|
+
inplace=False,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if inplace:
|
|
292
|
+
for n, value in out.items():
|
|
293
|
+
set_attr(G.nodes[n], ALIAS_SI, value)
|
|
120
294
|
return out
|
tnfr/metrics/trig.py
CHANGED
|
@@ -7,12 +7,16 @@ Caching of cosine/sine values lives in :mod:`tnfr.metrics.trig_cache`.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import math
|
|
10
|
-
from collections.abc import Iterable, Sequence
|
|
10
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
11
11
|
from itertools import tee
|
|
12
|
-
from typing import Any
|
|
12
|
+
from typing import TYPE_CHECKING, Any, overload, cast
|
|
13
13
|
|
|
14
|
-
from ..import_utils import cached_import, get_numpy
|
|
15
14
|
from ..helpers.numeric import kahan_sum_nd
|
|
15
|
+
from ..utils import cached_import, get_numpy
|
|
16
|
+
from ..types import NodeId, Phase, TNFRGraph
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
19
|
+
from ..node import NodeProtocol
|
|
16
20
|
|
|
17
21
|
__all__ = (
|
|
18
22
|
"accumulate_cos_sin",
|
|
@@ -37,7 +41,7 @@ def accumulate_cos_sin(
|
|
|
37
41
|
|
|
38
42
|
processed = False
|
|
39
43
|
|
|
40
|
-
def iter_real_pairs():
|
|
44
|
+
def iter_real_pairs() -> Iterator[tuple[float, float]]:
|
|
41
45
|
nonlocal processed
|
|
42
46
|
for cs in it:
|
|
43
47
|
if cs is None:
|
|
@@ -82,12 +86,12 @@ def _neighbor_phase_mean_core(
|
|
|
82
86
|
neigh: Sequence[Any],
|
|
83
87
|
cos_map: dict[Any, float],
|
|
84
88
|
sin_map: dict[Any, float],
|
|
85
|
-
np,
|
|
89
|
+
np: Any | None,
|
|
86
90
|
fallback: float,
|
|
87
91
|
) -> float:
|
|
88
92
|
"""Return circular mean of neighbour phases given trig mappings."""
|
|
89
93
|
|
|
90
|
-
def _iter_pairs():
|
|
94
|
+
def _iter_pairs() -> Iterator[tuple[float, float]]:
|
|
91
95
|
for v in neigh:
|
|
92
96
|
c = cos_map.get(v)
|
|
93
97
|
s = sin_map.get(v)
|
|
@@ -113,10 +117,10 @@ def _neighbor_phase_mean_core(
|
|
|
113
117
|
|
|
114
118
|
|
|
115
119
|
def _neighbor_phase_mean_generic(
|
|
116
|
-
obj,
|
|
120
|
+
obj: "NodeProtocol" | Sequence[Any],
|
|
117
121
|
cos_map: dict[Any, float] | None = None,
|
|
118
122
|
sin_map: dict[Any, float] | None = None,
|
|
119
|
-
np=None,
|
|
123
|
+
np: Any | None = None,
|
|
120
124
|
fallback: float = 0.0,
|
|
121
125
|
) -> float:
|
|
122
126
|
"""Internal helper delegating to :func:`_neighbor_phase_mean_core`.
|
|
@@ -132,7 +136,7 @@ def _neighbor_phase_mean_generic(
|
|
|
132
136
|
np = get_numpy()
|
|
133
137
|
|
|
134
138
|
if cos_map is None or sin_map is None:
|
|
135
|
-
node = obj
|
|
139
|
+
node = cast("NodeProtocol", obj)
|
|
136
140
|
if getattr(node, "G", None) is None:
|
|
137
141
|
raise TypeError(
|
|
138
142
|
"neighbor_phase_mean requires nodes bound to a graph"
|
|
@@ -145,7 +149,7 @@ def _neighbor_phase_mean_generic(
|
|
|
145
149
|
sin_map = trig.sin
|
|
146
150
|
neigh = node.G[node.n]
|
|
147
151
|
else:
|
|
148
|
-
neigh = obj
|
|
152
|
+
neigh = cast(Sequence[Any], obj)
|
|
149
153
|
|
|
150
154
|
return _neighbor_phase_mean_core(neigh, cos_map, sin_map, np, fallback)
|
|
151
155
|
|
|
@@ -154,7 +158,7 @@ def neighbor_phase_mean_list(
|
|
|
154
158
|
neigh: Sequence[Any],
|
|
155
159
|
cos_th: dict[Any, float],
|
|
156
160
|
sin_th: dict[Any, float],
|
|
157
|
-
np=None,
|
|
161
|
+
np: Any | None = None,
|
|
158
162
|
fallback: float = 0.0,
|
|
159
163
|
) -> float:
|
|
160
164
|
"""Return circular mean of neighbour phases from cosine/sine mappings.
|
|
@@ -168,14 +172,45 @@ def neighbor_phase_mean_list(
|
|
|
168
172
|
)
|
|
169
173
|
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
@overload
|
|
176
|
+
def neighbor_phase_mean(obj: "NodeProtocol", n: None = ...) -> Phase:
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@overload
|
|
181
|
+
def neighbor_phase_mean(obj: TNFRGraph, n: NodeId) -> Phase:
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def neighbor_phase_mean(
|
|
186
|
+
obj: "NodeProtocol" | TNFRGraph, n: NodeId | None = None
|
|
187
|
+
) -> Phase:
|
|
188
|
+
"""Circular mean of neighbour phases for ``obj``.
|
|
173
189
|
|
|
174
|
-
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
obj:
|
|
193
|
+
Either a :class:`~tnfr.node.NodeProtocol` instance bound to a graph or a
|
|
194
|
+
:class:`~tnfr.types.TNFRGraph` from which the node ``n`` will be wrapped.
|
|
195
|
+
n:
|
|
196
|
+
Optional node identifier. Required when ``obj`` is a graph. Providing a
|
|
197
|
+
node identifier for a node object raises :class:`TypeError`.
|
|
175
198
|
"""
|
|
176
199
|
|
|
177
|
-
|
|
178
|
-
if
|
|
179
|
-
raise ImportError("
|
|
180
|
-
|
|
200
|
+
NodeNX = cached_import("tnfr.node", "NodeNX")
|
|
201
|
+
if NodeNX is None:
|
|
202
|
+
raise ImportError("NodeNX is unavailable")
|
|
203
|
+
if n is None:
|
|
204
|
+
if hasattr(obj, "nodes"):
|
|
205
|
+
raise TypeError(
|
|
206
|
+
"neighbor_phase_mean requires a node identifier when passing a graph"
|
|
207
|
+
)
|
|
208
|
+
node = obj
|
|
209
|
+
else:
|
|
210
|
+
if hasattr(obj, "nodes"):
|
|
211
|
+
node = NodeNX(obj, n)
|
|
212
|
+
else:
|
|
213
|
+
raise TypeError(
|
|
214
|
+
"neighbor_phase_mean received a node and an explicit identifier"
|
|
215
|
+
)
|
|
181
216
|
return _neighbor_phase_mean_generic(node)
|
tnfr/metrics/trig.pyi
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
__all__: Any
|
|
4
|
+
|
|
5
|
+
def __getattr__(name: str) -> Any: ...
|
|
6
|
+
|
|
7
|
+
_neighbor_phase_mean_core: Any
|
|
8
|
+
_neighbor_phase_mean_generic: Any
|
|
9
|
+
_phase_mean_from_iter: Any
|
|
10
|
+
accumulate_cos_sin: Any
|
|
11
|
+
neighbor_phase_mean: Any
|
|
12
|
+
neighbor_phase_mean_list: Any
|
tnfr/metrics/trig_cache.py
CHANGED
|
@@ -10,13 +10,9 @@ import math
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from typing import Any, Iterable, Mapping
|
|
12
12
|
|
|
13
|
-
from ..alias import
|
|
14
|
-
from ..constants import get_aliases
|
|
15
|
-
from ..cache import edge_version_cache
|
|
16
|
-
from ..import_utils import get_numpy
|
|
13
|
+
from ..alias import get_theta_attr
|
|
17
14
|
from ..types import GraphLike
|
|
18
|
-
|
|
19
|
-
ALIAS_THETA = get_aliases("THETA")
|
|
15
|
+
from ..utils import edge_version_cache, get_numpy
|
|
20
16
|
|
|
21
17
|
__all__ = ("TrigCache", "compute_theta_trig", "get_trig_cache", "_compute_trig_python")
|
|
22
18
|
|
|
@@ -37,7 +33,7 @@ def _iter_theta_pairs(
|
|
|
37
33
|
|
|
38
34
|
for n, data in nodes:
|
|
39
35
|
if isinstance(data, Mapping):
|
|
40
|
-
yield n,
|
|
36
|
+
yield n, get_theta_attr(data, 0.0) or 0.0
|
|
41
37
|
else:
|
|
42
38
|
yield n, float(data)
|
|
43
39
|
|