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.
Files changed (161) hide show
  1. tnfr/__init__.py +228 -49
  2. tnfr/__init__.pyi +40 -0
  3. tnfr/_compat.py +11 -0
  4. tnfr/_version.py +7 -0
  5. tnfr/_version.pyi +7 -0
  6. tnfr/alias.py +106 -21
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +666 -512
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +2 -9
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +21 -7
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +42 -20
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +54 -20
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +0 -2
  19. tnfr/cli/utils.pyi +8 -0
  20. tnfr/config/__init__.py +12 -0
  21. tnfr/config/__init__.pyi +8 -0
  22. tnfr/config/constants.py +104 -0
  23. tnfr/config/constants.pyi +12 -0
  24. tnfr/{config.py → config/init.py} +11 -7
  25. tnfr/config/init.pyi +8 -0
  26. tnfr/config/operator_names.py +106 -0
  27. tnfr/config/operator_names.pyi +28 -0
  28. tnfr/config/presets.py +104 -0
  29. tnfr/config/presets.pyi +7 -0
  30. tnfr/constants/__init__.py +78 -24
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +1 -2
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.pyi +12 -0
  35. tnfr/constants/metric.py +4 -12
  36. tnfr/constants/metric.pyi +19 -0
  37. tnfr/constants_glyphs.py +9 -91
  38. tnfr/constants_glyphs.pyi +12 -0
  39. tnfr/dynamics/__init__.py +112 -634
  40. tnfr/dynamics/__init__.pyi +83 -0
  41. tnfr/dynamics/adaptation.py +201 -0
  42. tnfr/dynamics/aliases.py +22 -0
  43. tnfr/dynamics/coordination.py +343 -0
  44. tnfr/dynamics/dnfr.py +1936 -354
  45. tnfr/dynamics/dnfr.pyi +33 -0
  46. tnfr/dynamics/integrators.py +369 -75
  47. tnfr/dynamics/integrators.pyi +35 -0
  48. tnfr/dynamics/runtime.py +521 -0
  49. tnfr/dynamics/sampling.py +8 -5
  50. tnfr/dynamics/sampling.pyi +7 -0
  51. tnfr/dynamics/selectors.py +680 -0
  52. tnfr/execution.py +56 -41
  53. tnfr/execution.pyi +65 -0
  54. tnfr/flatten.py +7 -7
  55. tnfr/flatten.pyi +28 -0
  56. tnfr/gamma.py +54 -37
  57. tnfr/gamma.pyi +40 -0
  58. tnfr/glyph_history.py +85 -38
  59. tnfr/glyph_history.pyi +53 -0
  60. tnfr/grammar.py +19 -338
  61. tnfr/grammar.pyi +13 -0
  62. tnfr/helpers/__init__.py +110 -30
  63. tnfr/helpers/__init__.pyi +66 -0
  64. tnfr/helpers/numeric.py +1 -0
  65. tnfr/helpers/numeric.pyi +12 -0
  66. tnfr/immutable.py +55 -19
  67. tnfr/immutable.pyi +37 -0
  68. tnfr/initialization.py +12 -10
  69. tnfr/initialization.pyi +73 -0
  70. tnfr/io.py +99 -34
  71. tnfr/io.pyi +11 -0
  72. tnfr/locking.pyi +7 -0
  73. tnfr/metrics/__init__.pyi +20 -0
  74. tnfr/metrics/coherence.py +934 -294
  75. tnfr/metrics/common.py +1 -3
  76. tnfr/metrics/common.pyi +15 -0
  77. tnfr/metrics/core.py +192 -34
  78. tnfr/metrics/core.pyi +13 -0
  79. tnfr/metrics/diagnosis.py +707 -101
  80. tnfr/metrics/diagnosis.pyi +89 -0
  81. tnfr/metrics/export.py +27 -13
  82. tnfr/metrics/glyph_timing.py +218 -38
  83. tnfr/metrics/reporting.py +22 -18
  84. tnfr/metrics/reporting.pyi +12 -0
  85. tnfr/metrics/sense_index.py +199 -25
  86. tnfr/metrics/sense_index.pyi +9 -0
  87. tnfr/metrics/trig.py +53 -18
  88. tnfr/metrics/trig.pyi +12 -0
  89. tnfr/metrics/trig_cache.py +3 -7
  90. tnfr/metrics/trig_cache.pyi +10 -0
  91. tnfr/node.py +148 -125
  92. tnfr/node.pyi +161 -0
  93. tnfr/observers.py +44 -30
  94. tnfr/observers.pyi +46 -0
  95. tnfr/ontosim.py +14 -13
  96. tnfr/ontosim.pyi +33 -0
  97. tnfr/operators/__init__.py +84 -52
  98. tnfr/operators/__init__.pyi +31 -0
  99. tnfr/operators/definitions.py +181 -0
  100. tnfr/operators/definitions.pyi +92 -0
  101. tnfr/operators/jitter.py +86 -23
  102. tnfr/operators/jitter.pyi +11 -0
  103. tnfr/operators/registry.py +80 -0
  104. tnfr/operators/registry.pyi +15 -0
  105. tnfr/operators/remesh.py +141 -57
  106. tnfr/presets.py +9 -54
  107. tnfr/presets.pyi +7 -0
  108. tnfr/py.typed +0 -0
  109. tnfr/rng.py +259 -73
  110. tnfr/rng.pyi +14 -0
  111. tnfr/selector.py +24 -17
  112. tnfr/selector.pyi +19 -0
  113. tnfr/sense.py +55 -43
  114. tnfr/sense.pyi +30 -0
  115. tnfr/structural.py +44 -267
  116. tnfr/structural.pyi +46 -0
  117. tnfr/telemetry/__init__.py +13 -0
  118. tnfr/telemetry/verbosity.py +37 -0
  119. tnfr/tokens.py +3 -2
  120. tnfr/tokens.pyi +41 -0
  121. tnfr/trace.py +272 -82
  122. tnfr/trace.pyi +68 -0
  123. tnfr/types.py +345 -6
  124. tnfr/types.pyi +145 -0
  125. tnfr/utils/__init__.py +158 -0
  126. tnfr/utils/__init__.pyi +133 -0
  127. tnfr/utils/cache.py +755 -0
  128. tnfr/utils/cache.pyi +156 -0
  129. tnfr/{collections_utils.py → utils/data.py} +57 -90
  130. tnfr/utils/data.pyi +73 -0
  131. tnfr/utils/graph.py +87 -0
  132. tnfr/utils/graph.pyi +10 -0
  133. tnfr/utils/init.py +746 -0
  134. tnfr/utils/init.pyi +85 -0
  135. tnfr/{json_utils.py → utils/io.py} +13 -18
  136. tnfr/utils/io.pyi +10 -0
  137. tnfr/utils/validators.py +130 -0
  138. tnfr/utils/validators.pyi +19 -0
  139. tnfr/validation/__init__.py +25 -0
  140. tnfr/validation/__init__.pyi +17 -0
  141. tnfr/validation/compatibility.py +59 -0
  142. tnfr/validation/compatibility.pyi +8 -0
  143. tnfr/validation/grammar.py +149 -0
  144. tnfr/validation/grammar.pyi +11 -0
  145. tnfr/validation/rules.py +194 -0
  146. tnfr/validation/rules.pyi +18 -0
  147. tnfr/validation/syntax.py +151 -0
  148. tnfr/validation/syntax.pyi +7 -0
  149. tnfr-6.0.0.dist-info/METADATA +135 -0
  150. tnfr-6.0.0.dist-info/RECORD +157 -0
  151. tnfr/graph_utils.py +0 -84
  152. tnfr/import_utils.py +0 -228
  153. tnfr/logging_utils.py +0 -116
  154. tnfr/validators.py +0 -84
  155. tnfr/value_utils.py +0 -59
  156. tnfr-4.5.2.dist-info/METADATA +0 -379
  157. tnfr-4.5.2.dist-info/RECORD +0 -67
  158. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  159. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  160. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  161. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
@@ -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
- "dSi_ddisp_fase": -beta,
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
- disp_fase: float,
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 = alpha * vf_norm + beta * (1.0 - disp_fase) + gamma * (1.0 - dnfr_norm)
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 compute_Si(G: GraphLike, *, inplace: bool = True) -> dict[Any, float]:
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
- out: dict[Any, float] = {}
105
- for n, nd in G.nodes(data=True):
106
- neigh = neighbors[n]
107
- th_bar = pm_fn(neigh, fallback=thetas[n])
108
- disp_fase = abs(angle_diff(thetas[n], th_bar)) / math.pi
109
- out[n] = compute_Si_node(
110
- n,
111
- nd,
112
- alpha=alpha,
113
- beta=beta,
114
- gamma=gamma,
115
- vfmax=vfmax,
116
- dnfrmax=dnfrmax,
117
- disp_fase=disp_fase,
118
- inplace=inplace,
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
@@ -0,0 +1,9 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ compute_Si: Any
8
+ compute_Si_node: Any
9
+ get_Si_weights: Any
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
- def neighbor_phase_mean(obj, n=None) -> float:
172
- """Circular mean of neighbour phases.
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
- The :class:`NodoNX` import is cached after the first call.
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
- NodoNX = cached_import("tnfr.node", "NodoNX")
178
- if NodoNX is None:
179
- raise ImportError("NodoNX is unavailable")
180
- node = NodoNX(obj, n) if n is not None else obj
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
@@ -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 get_attr
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, get_attr(data, ALIAS_THETA, 0.0)
36
+ yield n, get_theta_attr(data, 0.0) or 0.0
41
37
  else:
42
38
  yield n, float(data)
43
39
 
@@ -0,0 +1,10 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ TrigCache: Any
8
+ _compute_trig_python: Any
9
+ compute_theta_trig: Any
10
+ get_trig_cache: Any