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.

Files changed (176) hide show
  1. tnfr/__init__.py +50 -5
  2. tnfr/__init__.pyi +0 -7
  3. tnfr/_compat.py +0 -1
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +44 -2
  6. tnfr/alias.py +14 -13
  7. tnfr/alias.pyi +5 -37
  8. tnfr/cache.py +9 -729
  9. tnfr/cache.pyi +8 -224
  10. tnfr/callback_utils.py +16 -31
  11. tnfr/callback_utils.pyi +3 -29
  12. tnfr/cli/__init__.py +17 -11
  13. tnfr/cli/__init__.pyi +0 -21
  14. tnfr/cli/arguments.py +175 -14
  15. tnfr/cli/arguments.pyi +5 -11
  16. tnfr/cli/execution.py +434 -48
  17. tnfr/cli/execution.pyi +14 -24
  18. tnfr/cli/utils.py +20 -3
  19. tnfr/cli/utils.pyi +5 -5
  20. tnfr/config/__init__.py +2 -1
  21. tnfr/config/__init__.pyi +2 -0
  22. tnfr/config/feature_flags.py +83 -0
  23. tnfr/config/init.py +1 -1
  24. tnfr/config/operator_names.py +1 -14
  25. tnfr/config/presets.py +6 -26
  26. tnfr/constants/__init__.py +10 -13
  27. tnfr/constants/__init__.pyi +10 -22
  28. tnfr/constants/aliases.py +31 -0
  29. tnfr/constants/core.py +4 -3
  30. tnfr/constants/init.py +1 -1
  31. tnfr/constants/metric.py +3 -3
  32. tnfr/dynamics/__init__.py +64 -10
  33. tnfr/dynamics/__init__.pyi +3 -4
  34. tnfr/dynamics/adaptation.py +79 -13
  35. tnfr/dynamics/aliases.py +10 -9
  36. tnfr/dynamics/coordination.py +77 -35
  37. tnfr/dynamics/dnfr.py +575 -274
  38. tnfr/dynamics/dnfr.pyi +1 -10
  39. tnfr/dynamics/integrators.py +47 -33
  40. tnfr/dynamics/integrators.pyi +0 -1
  41. tnfr/dynamics/runtime.py +489 -129
  42. tnfr/dynamics/sampling.py +2 -0
  43. tnfr/dynamics/selectors.py +101 -62
  44. tnfr/execution.py +15 -8
  45. tnfr/execution.pyi +5 -25
  46. tnfr/flatten.py +7 -3
  47. tnfr/flatten.pyi +1 -8
  48. tnfr/gamma.py +22 -26
  49. tnfr/gamma.pyi +0 -6
  50. tnfr/glyph_history.py +37 -26
  51. tnfr/glyph_history.pyi +1 -19
  52. tnfr/glyph_runtime.py +16 -0
  53. tnfr/glyph_runtime.pyi +9 -0
  54. tnfr/immutable.py +20 -15
  55. tnfr/immutable.pyi +4 -7
  56. tnfr/initialization.py +5 -7
  57. tnfr/initialization.pyi +1 -9
  58. tnfr/io.py +6 -305
  59. tnfr/io.pyi +13 -8
  60. tnfr/mathematics/__init__.py +81 -0
  61. tnfr/mathematics/backend.py +426 -0
  62. tnfr/mathematics/dynamics.py +398 -0
  63. tnfr/mathematics/epi.py +254 -0
  64. tnfr/mathematics/generators.py +222 -0
  65. tnfr/mathematics/metrics.py +119 -0
  66. tnfr/mathematics/operators.py +233 -0
  67. tnfr/mathematics/operators_factory.py +71 -0
  68. tnfr/mathematics/projection.py +78 -0
  69. tnfr/mathematics/runtime.py +173 -0
  70. tnfr/mathematics/spaces.py +247 -0
  71. tnfr/mathematics/transforms.py +292 -0
  72. tnfr/metrics/__init__.py +10 -10
  73. tnfr/metrics/coherence.py +123 -94
  74. tnfr/metrics/common.py +22 -13
  75. tnfr/metrics/common.pyi +42 -11
  76. tnfr/metrics/core.py +72 -14
  77. tnfr/metrics/diagnosis.py +48 -57
  78. tnfr/metrics/diagnosis.pyi +3 -7
  79. tnfr/metrics/export.py +3 -5
  80. tnfr/metrics/glyph_timing.py +41 -31
  81. tnfr/metrics/reporting.py +13 -6
  82. tnfr/metrics/sense_index.py +884 -114
  83. tnfr/metrics/trig.py +167 -11
  84. tnfr/metrics/trig.pyi +1 -0
  85. tnfr/metrics/trig_cache.py +112 -15
  86. tnfr/node.py +400 -17
  87. tnfr/node.pyi +55 -38
  88. tnfr/observers.py +111 -8
  89. tnfr/observers.pyi +0 -15
  90. tnfr/ontosim.py +9 -6
  91. tnfr/ontosim.pyi +0 -5
  92. tnfr/operators/__init__.py +529 -42
  93. tnfr/operators/__init__.pyi +14 -0
  94. tnfr/operators/definitions.py +350 -18
  95. tnfr/operators/definitions.pyi +0 -14
  96. tnfr/operators/grammar.py +760 -0
  97. tnfr/operators/jitter.py +28 -22
  98. tnfr/operators/registry.py +7 -12
  99. tnfr/operators/registry.pyi +0 -2
  100. tnfr/operators/remesh.py +38 -61
  101. tnfr/rng.py +17 -300
  102. tnfr/schemas/__init__.py +8 -0
  103. tnfr/schemas/grammar.json +94 -0
  104. tnfr/selector.py +3 -4
  105. tnfr/selector.pyi +1 -1
  106. tnfr/sense.py +22 -24
  107. tnfr/sense.pyi +0 -7
  108. tnfr/structural.py +504 -21
  109. tnfr/structural.pyi +41 -18
  110. tnfr/telemetry/__init__.py +23 -1
  111. tnfr/telemetry/cache_metrics.py +226 -0
  112. tnfr/telemetry/nu_f.py +423 -0
  113. tnfr/telemetry/nu_f.pyi +123 -0
  114. tnfr/tokens.py +1 -4
  115. tnfr/tokens.pyi +1 -6
  116. tnfr/trace.py +20 -53
  117. tnfr/trace.pyi +9 -37
  118. tnfr/types.py +244 -15
  119. tnfr/types.pyi +200 -14
  120. tnfr/units.py +69 -0
  121. tnfr/units.pyi +16 -0
  122. tnfr/utils/__init__.py +107 -48
  123. tnfr/utils/__init__.pyi +80 -11
  124. tnfr/utils/cache.py +1705 -65
  125. tnfr/utils/cache.pyi +370 -58
  126. tnfr/utils/chunks.py +104 -0
  127. tnfr/utils/chunks.pyi +21 -0
  128. tnfr/utils/data.py +95 -5
  129. tnfr/utils/data.pyi +8 -17
  130. tnfr/utils/graph.py +2 -4
  131. tnfr/utils/init.py +31 -7
  132. tnfr/utils/init.pyi +4 -11
  133. tnfr/utils/io.py +313 -14
  134. tnfr/{helpers → utils}/numeric.py +50 -24
  135. tnfr/utils/numeric.pyi +21 -0
  136. tnfr/validation/__init__.py +92 -4
  137. tnfr/validation/__init__.pyi +77 -17
  138. tnfr/validation/compatibility.py +79 -43
  139. tnfr/validation/compatibility.pyi +4 -6
  140. tnfr/validation/grammar.py +55 -133
  141. tnfr/validation/grammar.pyi +37 -8
  142. tnfr/validation/graph.py +138 -0
  143. tnfr/validation/graph.pyi +17 -0
  144. tnfr/validation/rules.py +161 -74
  145. tnfr/validation/rules.pyi +55 -18
  146. tnfr/validation/runtime.py +263 -0
  147. tnfr/validation/runtime.pyi +31 -0
  148. tnfr/validation/soft_filters.py +170 -0
  149. tnfr/validation/soft_filters.pyi +37 -0
  150. tnfr/validation/spectral.py +159 -0
  151. tnfr/validation/spectral.pyi +46 -0
  152. tnfr/validation/syntax.py +28 -139
  153. tnfr/validation/syntax.pyi +7 -4
  154. tnfr/validation/window.py +39 -0
  155. tnfr/validation/window.pyi +1 -0
  156. tnfr/viz/__init__.py +9 -0
  157. tnfr/viz/matplotlib.py +246 -0
  158. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
  159. tnfr-7.0.0.dist-info/RECORD +185 -0
  160. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  161. tnfr/constants_glyphs.py +0 -16
  162. tnfr/constants_glyphs.pyi +0 -12
  163. tnfr/grammar.py +0 -25
  164. tnfr/grammar.pyi +0 -13
  165. tnfr/helpers/__init__.py +0 -151
  166. tnfr/helpers/__init__.pyi +0 -66
  167. tnfr/helpers/numeric.pyi +0 -12
  168. tnfr/presets.py +0 -15
  169. tnfr/presets.pyi +0 -7
  170. tnfr/utils/io.pyi +0 -10
  171. tnfr/utils/validators.py +0 -130
  172. tnfr/utils/validators.pyi +0 -19
  173. tnfr-6.0.0.dist-info/RECORD +0 -157
  174. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  175. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  176. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/metrics/coherence.py CHANGED
@@ -7,19 +7,32 @@ from collections.abc import Callable, Iterable, Mapping, Sequence
7
7
  from concurrent.futures import ProcessPoolExecutor
8
8
  from dataclasses import dataclass
9
9
  from types import ModuleType
10
- from typing import Any, MutableMapping, TypedDict, cast
10
+ from typing import Any, MutableMapping, cast
11
11
 
12
12
  from .._compat import TypeAlias
13
-
14
-
15
- from ..constants import (
16
- get_aliases,
17
- get_param,
18
- )
13
+ from ..alias import collect_attr, collect_theta_attr, get_attr, set_attr
19
14
  from ..callback_utils import CallbackEvent, callback_manager
15
+ from ..constants import get_param
16
+ from ..constants.aliases import (
17
+ ALIAS_D2VF,
18
+ ALIAS_DNFR,
19
+ ALIAS_DSI,
20
+ ALIAS_DVF,
21
+ ALIAS_DEPI,
22
+ ALIAS_EPI,
23
+ ALIAS_SI,
24
+ ALIAS_VF,
25
+ )
20
26
  from ..glyph_history import append_metric, ensure_history
21
- from ..alias import collect_attr, collect_theta_attr, set_attr
22
- from ..helpers.numeric import clamp01
27
+ from ..utils import clamp01
28
+ from ..observers import (
29
+ DEFAULT_GLYPH_LOAD_SPAN,
30
+ DEFAULT_WBAR_SPAN,
31
+ glyph_load,
32
+ kuramoto_order,
33
+ phase_sync,
34
+ )
35
+ from ..sense import sigma_vector
23
36
  from ..types import (
24
37
  CoherenceMetric,
25
38
  FloatArray,
@@ -27,37 +40,22 @@ from ..types import (
27
40
  GlyphLoadDistribution,
28
41
  HistoryState,
29
42
  NodeId,
43
+ ParallelWijPayload,
30
44
  SigmaVector,
31
45
  TNFRGraph,
32
46
  )
33
- from .common import compute_coherence, min_max_range
34
- from .trig_cache import compute_theta_trig, get_trig_cache
35
- from ..observers import (
36
- DEFAULT_GLYPH_LOAD_SPAN,
37
- DEFAULT_WBAR_SPAN,
38
- glyph_load,
39
- kuramoto_order,
40
- phase_sync,
41
- )
42
- from ..sense import sigma_vector
43
47
  from ..utils import (
44
48
  ensure_node_index_map,
45
49
  get_logger,
46
50
  get_numpy,
47
51
  normalize_weights,
52
+ resolve_chunk_size,
48
53
  )
54
+ from .common import compute_coherence, min_max_range
55
+ from .trig_cache import compute_theta_trig, get_trig_cache
49
56
 
50
57
  logger = get_logger(__name__)
51
58
 
52
- ALIAS_EPI = get_aliases("EPI")
53
- ALIAS_VF = get_aliases("VF")
54
- ALIAS_SI = get_aliases("SI")
55
- ALIAS_DNFR = get_aliases("DNFR")
56
- ALIAS_DEPI = get_aliases("DEPI")
57
- ALIAS_DSI = get_aliases("DSI")
58
- ALIAS_DVF = get_aliases("DVF")
59
- ALIAS_D2VF = get_aliases("D2VF")
60
-
61
59
  GLYPH_LOAD_STABILIZERS_KEY = "glyph_load_stabilizers"
62
60
 
63
61
 
@@ -81,9 +79,9 @@ PhaseSyncWeights: TypeAlias = (
81
79
  )
82
80
 
83
81
  SimilarityComponents = tuple[float, float, float, float]
84
- VectorizedComponents: TypeAlias = (
85
- tuple[FloatMatrix, FloatMatrix, FloatMatrix, FloatMatrix]
86
- )
82
+ VectorizedComponents: TypeAlias = tuple[
83
+ FloatMatrix, FloatMatrix, FloatMatrix, FloatMatrix
84
+ ]
87
85
  ScalarOrArray: TypeAlias = float | FloatArray
88
86
  StabilityChunkArgs = tuple[
89
87
  Sequence[float],
@@ -110,19 +108,6 @@ StabilityChunkResult = tuple[
110
108
  MetricValue: TypeAlias = CoherenceMetric
111
109
  MetricProvider = Callable[[], MetricValue]
112
110
  MetricRecord: TypeAlias = tuple[MetricValue | MetricProvider, str]
113
-
114
-
115
- class ParallelWijPayload(TypedDict):
116
- epi_vals: Sequence[float]
117
- vf_vals: Sequence[float]
118
- si_vals: Sequence[float]
119
- cos_vals: Sequence[float]
120
- sin_vals: Sequence[float]
121
- weights: tuple[float, float, float, float]
122
- epi_range: float
123
- vf_range: float
124
-
125
-
126
111
  def _compute_wij_phase_epi_vf_si_vectorized(
127
112
  epi: FloatArray,
128
113
  vf: FloatArray,
@@ -143,9 +128,7 @@ def _compute_wij_phase_epi_vf_si_vectorized(
143
128
  epi_range = epi_range if epi_range > 0 else 1.0
144
129
  vf_range = vf_range if vf_range > 0 else 1.0
145
130
  s_phase = 0.5 * (
146
- 1.0
147
- + cos_th[:, None] * cos_th[None, :]
148
- + sin_th[:, None] * sin_th[None, :]
131
+ 1.0 + cos_th[:, None] * cos_th[None, :] + sin_th[:, None] * sin_th[None, :]
149
132
  )
150
133
  s_epi = 1.0 - np.abs(epi[:, None] - epi[None, :]) / epi_range
151
134
  s_vf = 1.0 - np.abs(vf[:, None] - vf[None, :]) / vf_range
@@ -369,7 +352,7 @@ def _init_parallel_wij(data: ParallelWijPayload) -> None:
369
352
 
370
353
 
371
354
  def _parallel_wij_worker(
372
- pairs: Sequence[tuple[int, int]]
355
+ pairs: Sequence[tuple[int, int]],
373
356
  ) -> list[tuple[int, int, float]]:
374
357
  """Compute coherence weights for ``pairs`` using shared state."""
375
358
 
@@ -444,10 +427,7 @@ def _wij_loops(
444
427
  inputs.si_vals = si_vals
445
428
  inputs.cos_vals = cos_vals_list
446
429
  inputs.sin_vals = sin_vals_list
447
- wij = [
448
- [1.0 if (self_diag and i == j) else 0.0 for j in range(n)]
449
- for i in range(n)
450
- ]
430
+ wij = [[1.0 if (self_diag and i == j) else 0.0 for j in range(n)] for i in range(n)]
451
431
  epi_range = epi_max - epi_min if epi_max > epi_min else 1.0
452
432
  vf_range = vf_max - vf_min if vf_max > vf_min else 1.0
453
433
  weights = (
@@ -498,7 +478,12 @@ def _wij_loops(
498
478
  wij[i][j] = wij[j][i] = wij_ij
499
479
  return wij
500
480
 
501
- chunk_size = max(1, math.ceil(total_pairs / max_workers))
481
+ approx_chunk = math.ceil(total_pairs / max_workers) if max_workers else None
482
+ chunk_size = resolve_chunk_size(
483
+ approx_chunk,
484
+ total_pairs,
485
+ minimum=1,
486
+ )
502
487
  payload: ParallelWijPayload = {
503
488
  "epi_vals": tuple(epi_vals),
504
489
  "vf_vals": tuple(vf_vals),
@@ -516,7 +501,7 @@ def _wij_loops(
516
501
  with ProcessPoolExecutor(max_workers=max_workers, initializer=_init) as executor:
517
502
  futures = []
518
503
  for start in range(0, total_pairs, chunk_size):
519
- chunk = pair_list[start:start + chunk_size]
504
+ chunk = pair_list[start : start + chunk_size]
520
505
  futures.append(executor.submit(_parallel_wij_worker, chunk))
521
506
  for future in futures:
522
507
  for i, j, value in future.result():
@@ -589,15 +574,12 @@ def _coherence_numpy(
589
574
  W = wij.tolist()
590
575
  else:
591
576
  idx = np.where((wij >= thr) & mask)
592
- W = [
593
- (int(i), int(j), float(wij[i, j]))
594
- for i, j in zip(idx[0], idx[1])
595
- ]
577
+ W = [(int(i), int(j), float(wij[i, j])) for i, j in zip(idx[0], idx[1])]
596
578
  return n, values, row_sum, W
597
579
 
598
580
 
599
581
  def _coherence_python_worker(
600
- args: tuple[Sequence[Sequence[float]], int, str, float]
582
+ args: tuple[Sequence[Sequence[float]], int, str, float],
601
583
  ) -> tuple[int, list[float], list[float], CoherenceMatrixSparse]:
602
584
  rows, start, mode, thr = args
603
585
  values: list[float] = []
@@ -661,11 +643,16 @@ def _coherence_python(
661
643
  row_sum[i] += w
662
644
  return n, values, row_sum, W if mode == "dense" else W_sparse
663
645
 
664
- chunk_size = max(1, math.ceil(n / max_workers))
646
+ approx_chunk = math.ceil(n / max_workers) if max_workers else None
647
+ chunk_size = resolve_chunk_size(
648
+ approx_chunk,
649
+ n,
650
+ minimum=1,
651
+ )
665
652
  tasks = []
666
653
  with ProcessPoolExecutor(max_workers=max_workers) as executor:
667
654
  for start in range(0, n, chunk_size):
668
- rows = wij[start:start + chunk_size]
655
+ rows = wij[start : start + chunk_size]
669
656
  tasks.append(
670
657
  executor.submit(
671
658
  _coherence_python_worker,
@@ -675,7 +662,9 @@ def _coherence_python(
675
662
  results = [task.result() for task in tasks]
676
663
 
677
664
  results.sort(key=lambda item: item[0])
678
- sparse_entries: list[tuple[int, int, float]] | None = [] if mode != "dense" else None
665
+ sparse_entries: list[tuple[int, int, float]] | None = (
666
+ [] if mode != "dense" else None
667
+ )
679
668
  for start, chunk_values, chunk_row_sum, chunk_sparse in results:
680
669
  values.extend(chunk_values)
681
670
  for offset, total in enumerate(chunk_row_sum):
@@ -771,9 +760,7 @@ def coherence_matrix(
771
760
 
772
761
  # NumPy handling for optional vectorized operations
773
762
  np = get_numpy()
774
- use_np = (
775
- np is not None if use_numpy is None else (use_numpy and np is not None)
776
- )
763
+ use_np = np is not None if use_numpy is None else (use_numpy and np is not None)
777
764
 
778
765
  cfg_jobs = cfg.get("n_jobs")
779
766
  parallel_jobs = n_jobs if n_jobs is not None else cfg_jobs
@@ -993,6 +980,8 @@ def _coherence_step(G: TNFRGraph, ctx: dict[str, Any] | None = None) -> None:
993
980
 
994
981
 
995
982
  def register_coherence_callbacks(G: TNFRGraph) -> None:
983
+ """Attach coherence matrix maintenance to the ``AFTER_STEP`` event."""
984
+
996
985
  callback_manager.register_callback(
997
986
  G,
998
987
  event=CallbackEvent.AFTER_STEP.value,
@@ -1011,7 +1000,7 @@ def _record_metrics(
1011
1000
  *pairs: MetricRecord,
1012
1001
  evaluate: bool = False,
1013
1002
  ) -> None:
1014
- """Generic recorder for metric values."""
1003
+ """Record metric values for the trace history."""
1015
1004
 
1016
1005
  metrics = cast(MutableMapping[str, list[Any]], hist)
1017
1006
  for payload, key in pairs:
@@ -1084,9 +1073,7 @@ def _update_sigma(G: TNFRGraph, hist: HistoryState) -> None:
1084
1073
  (disruptors, "glyph_load_disr"),
1085
1074
  )
1086
1075
 
1087
- dist: GlyphLoadDistribution = {
1088
- k: v for k, v in gl.items() if not k.startswith("_")
1089
- }
1076
+ dist: GlyphLoadDistribution = {k: v for k, v in gl.items() if not k.startswith("_")}
1090
1077
  sig: SigmaVector = sigma_vector(dist)
1091
1078
  _record_metrics(
1092
1079
  hist,
@@ -1140,7 +1127,10 @@ def _stability_chunk_worker(args: StabilityChunkArgs) -> StabilityChunkResult:
1140
1127
  B_vals.append(B)
1141
1128
  B_sum += B
1142
1129
 
1143
- if abs(float(dnfr_vals[idx])) <= eps_dnfr and abs(float(depi_vals[idx])) <= eps_depi:
1130
+ if (
1131
+ abs(float(dnfr_vals[idx])) <= eps_dnfr
1132
+ and abs(float(depi_vals[idx])) <= eps_depi
1133
+ ):
1144
1134
  stable += 1
1145
1135
 
1146
1136
  chunk_len = len(si_curr_vals)
@@ -1196,18 +1186,22 @@ def _track_stability(
1196
1186
 
1197
1187
  si_prev_arr = np.asarray(
1198
1188
  [
1199
- float(prev_si_data[idx])
1200
- if prev_si_data[idx] is not None
1201
- else float(si_curr_arr[idx])
1189
+ (
1190
+ float(prev_si_data[idx])
1191
+ if prev_si_data[idx] is not None
1192
+ else float(si_curr_arr[idx])
1193
+ )
1202
1194
  for idx in range(total_nodes)
1203
1195
  ],
1204
1196
  dtype=float,
1205
1197
  )
1206
1198
  vf_prev_arr = np.asarray(
1207
1199
  [
1208
- float(prev_vf_data[idx])
1209
- if prev_vf_data[idx] is not None
1210
- else float(vf_curr_arr[idx])
1200
+ (
1201
+ float(prev_vf_data[idx])
1202
+ if prev_vf_data[idx] is not None
1203
+ else float(vf_curr_arr[idx])
1204
+ )
1211
1205
  for idx in range(total_nodes)
1212
1206
  ],
1213
1207
  dtype=float,
@@ -1220,9 +1214,11 @@ def _track_stability(
1220
1214
 
1221
1215
  dvf_prev_arr = np.asarray(
1222
1216
  [
1223
- float(prev_dvf_data[idx])
1224
- if prev_dvf_data[idx] is not None
1225
- else float(dvf_dt_arr[idx])
1217
+ (
1218
+ float(prev_dvf_data[idx])
1219
+ if prev_dvf_data[idx] is not None
1220
+ else float(dvf_dt_arr[idx])
1221
+ )
1226
1222
  for idx in range(total_nodes)
1227
1223
  ],
1228
1224
  dtype=float,
@@ -1268,8 +1264,18 @@ def _track_stability(
1268
1264
  vf_curr_list = list(vf_curr_vals)
1269
1265
 
1270
1266
  if n_jobs and n_jobs > 1:
1271
- chunk_size = max(1, math.ceil(total_nodes / n_jobs))
1272
- chunk_results: list[tuple[int, tuple[int, int, float, float, list[float], list[float], list[float]]]] = []
1267
+ approx_chunk = math.ceil(total_nodes / n_jobs) if n_jobs else None
1268
+ chunk_size = resolve_chunk_size(
1269
+ approx_chunk,
1270
+ total_nodes,
1271
+ minimum=1,
1272
+ )
1273
+ chunk_results: list[
1274
+ tuple[
1275
+ int,
1276
+ tuple[int, int, float, float, list[float], list[float], list[float]],
1277
+ ]
1278
+ ] = []
1273
1279
  with ProcessPoolExecutor(max_workers=n_jobs) as executor:
1274
1280
  futures: list[tuple[int, Any]] = []
1275
1281
  for start in range(0, total_nodes, chunk_size):
@@ -1286,7 +1292,9 @@ def _track_stability(
1286
1292
  eps_dnfr,
1287
1293
  eps_depi,
1288
1294
  )
1289
- futures.append((start, executor.submit(_stability_chunk_worker, chunk_args)))
1295
+ futures.append(
1296
+ (start, executor.submit(_stability_chunk_worker, chunk_args))
1297
+ )
1290
1298
 
1291
1299
  for start, fut in futures:
1292
1300
  chunk_results.append((start, fut.result()))
@@ -1349,7 +1357,10 @@ def _track_stability(
1349
1357
  B_vals_all.append(B_val)
1350
1358
  B_sum += B_val
1351
1359
 
1352
- if abs(float(dnfr_list[idx])) <= eps_dnfr and abs(float(depi_list[idx])) <= eps_depi:
1360
+ if (
1361
+ abs(float(dnfr_list[idx])) <= eps_dnfr
1362
+ and abs(float(depi_list[idx])) <= eps_depi
1363
+ ):
1353
1364
  stable_total += 1
1354
1365
 
1355
1366
  total = len(delta_vals_all)
@@ -1416,10 +1427,30 @@ def _aggregate_si(
1416
1427
  si_hi = float(thr_sel.get("si_hi", thr_def.get("hi", 0.66)))
1417
1428
  si_lo = float(thr_sel.get("si_lo", thr_def.get("lo", 0.33)))
1418
1429
 
1430
+ node_ids = list(G.nodes)
1431
+ if not node_ids:
1432
+ hist["Si_mean"].append(0.0)
1433
+ hist["Si_hi_frac"].append(0.0)
1434
+ hist["Si_lo_frac"].append(0.0)
1435
+ return
1436
+
1437
+ sis = []
1438
+ for node in node_ids:
1439
+ raw = get_attr(
1440
+ G.nodes[node],
1441
+ ALIAS_SI,
1442
+ None,
1443
+ conv=lambda value: value, # Preserve NaN sentinels
1444
+ )
1445
+ try:
1446
+ sis.append(float(raw) if raw is not None else math.nan)
1447
+ except (TypeError, ValueError):
1448
+ sis.append(math.nan)
1449
+
1419
1450
  np_mod = get_numpy()
1420
1451
  if np_mod is not None:
1421
- sis = collect_attr(G, G.nodes, ALIAS_SI, float("nan"), np=np_mod)
1422
- valid = sis[~np_mod.isnan(sis)]
1452
+ sis_array = np_mod.asarray(sis, dtype=float)
1453
+ valid = sis_array[~np_mod.isnan(sis_array)]
1423
1454
  n = int(valid.size)
1424
1455
  if n:
1425
1456
  hist["Si_mean"].append(float(valid.mean()))
@@ -1433,19 +1464,17 @@ def _aggregate_si(
1433
1464
  hist["Si_lo_frac"].append(0.0)
1434
1465
  return
1435
1466
 
1436
- sis = collect_attr(G, G.nodes, ALIAS_SI, float("nan"))
1437
- if not sis:
1438
- hist["Si_mean"].append(0.0)
1439
- hist["Si_hi_frac"].append(0.0)
1440
- hist["Si_lo_frac"].append(0.0)
1441
- return
1442
-
1443
1467
  if n_jobs is not None and n_jobs > 1:
1444
- chunk_size = max(1, math.ceil(len(sis) / n_jobs))
1468
+ approx_chunk = math.ceil(len(sis) / n_jobs) if n_jobs else None
1469
+ chunk_size = resolve_chunk_size(
1470
+ approx_chunk,
1471
+ len(sis),
1472
+ minimum=1,
1473
+ )
1445
1474
  futures = []
1446
1475
  with ProcessPoolExecutor(max_workers=n_jobs) as executor:
1447
1476
  for idx in range(0, len(sis), chunk_size):
1448
- chunk = sis[idx:idx + chunk_size]
1477
+ chunk = sis[idx : idx + chunk_size]
1449
1478
  futures.append(
1450
1479
  executor.submit(_si_chunk_stats, chunk, si_hi, si_lo)
1451
1480
  )
tnfr/metrics/common.py CHANGED
@@ -6,16 +6,12 @@ from types import MappingProxyType
6
6
  from typing import Any, Iterable, Mapping, Sequence
7
7
 
8
8
  from ..alias import collect_attr, get_attr, multi_recompute_abs_max
9
- from ..constants import DEFAULTS, get_aliases
10
- from ..helpers.numeric import clamp01, kahan_sum_nd
11
- from ..types import GraphLike
9
+ from ..constants import DEFAULTS
10
+ from ..constants.aliases import ALIAS_D2EPI, ALIAS_DEPI, ALIAS_DNFR, ALIAS_VF
11
+ from ..utils import clamp01, kahan_sum_nd, normalize_optional_int
12
+ from ..types import GraphLike, NodeAttrMap
12
13
  from ..utils import edge_version_cache, get_numpy, normalize_weights
13
14
 
14
- ALIAS_DNFR = get_aliases("DNFR")
15
- ALIAS_D2EPI = get_aliases("D2EPI")
16
- ALIAS_DEPI = get_aliases("DEPI")
17
- ALIAS_VF = get_aliases("VF")
18
-
19
15
  __all__ = (
20
16
  "GraphLike",
21
17
  "compute_coherence",
@@ -25,6 +21,7 @@ __all__ = (
25
21
  "merge_graph_weights",
26
22
  "merge_and_normalize_weights",
27
23
  "min_max_range",
24
+ "_coerce_jobs",
28
25
  "_get_vf_dnfr_max",
29
26
  )
30
27
 
@@ -70,7 +67,10 @@ def ensure_neighbors_map(G: GraphLike) -> Mapping[Any, Sequence[Any]]:
70
67
  def merge_graph_weights(G: GraphLike, key: str) -> dict[str, float]:
71
68
  """Merge default weights for ``key`` with any graph overrides."""
72
69
 
73
- return {**DEFAULTS[key], **G.graph.get(key, {})}
70
+ overrides = G.graph.get(key, {})
71
+ if overrides is None or not isinstance(overrides, Mapping):
72
+ overrides = {}
73
+ return {**DEFAULTS[key], **overrides}
74
74
 
75
75
 
76
76
  def merge_and_normalize_weights(
@@ -101,7 +101,7 @@ def compute_dnfr_accel_max(G: GraphLike) -> dict[str, float]:
101
101
  )
102
102
 
103
103
 
104
- def normalize_dnfr(nd: Mapping[str, Any], max_val: float) -> float:
104
+ def normalize_dnfr(nd: NodeAttrMap, max_val: float) -> float:
105
105
  """Normalise ``|ΔNFR|`` using ``max_val``."""
106
106
 
107
107
  if max_val <= 0:
@@ -135,9 +135,7 @@ def _get_vf_dnfr_max(G: GraphLike) -> tuple[float, float]:
135
135
  vfmax = G.graph.get("_vfmax")
136
136
  dnfrmax = G.graph.get("_dnfrmax")
137
137
  if vfmax is None or dnfrmax is None:
138
- maxes = multi_recompute_abs_max(
139
- G, {"_vfmax": ALIAS_VF, "_dnfrmax": ALIAS_DNFR}
140
- )
138
+ maxes = multi_recompute_abs_max(G, {"_vfmax": ALIAS_VF, "_dnfrmax": ALIAS_DNFR})
141
139
  if vfmax is None:
142
140
  vfmax = maxes["_vfmax"]
143
141
  if dnfrmax is None:
@@ -147,3 +145,14 @@ def _get_vf_dnfr_max(G: GraphLike) -> tuple[float, float]:
147
145
  vfmax = 1.0 if vfmax == 0 else vfmax
148
146
  dnfrmax = 1.0 if dnfrmax == 0 else dnfrmax
149
147
  return float(vfmax), float(dnfrmax)
148
+
149
+
150
+ def _coerce_jobs(raw_jobs: Any | None) -> int | None:
151
+ """Normalise parallel job hints shared by metrics modules."""
152
+
153
+ return normalize_optional_int(
154
+ raw_jobs,
155
+ allow_non_positive=False,
156
+ strict=False,
157
+ sentinels=None,
158
+ )
tnfr/metrics/common.pyi CHANGED
@@ -1,15 +1,46 @@
1
- from typing import Any
1
+ from collections.abc import Iterable, Mapping, Sequence
2
+ from typing import Any, Literal, overload
2
3
 
3
- __all__: Any
4
+ from ..types import GraphLike, NodeAttrMap
5
+
6
+ __all__: tuple[str, ...]
4
7
 
5
8
  def __getattr__(name: str) -> Any: ...
6
9
 
7
- GraphLike: Any
8
- _get_vf_dnfr_max: Any
9
- compute_coherence: Any
10
- compute_dnfr_accel_max: Any
11
- ensure_neighbors_map: Any
12
- merge_and_normalize_weights: Any
13
- merge_graph_weights: Any
14
- min_max_range: Any
15
- normalize_dnfr: Any
10
+ @overload
11
+ def compute_coherence(G: GraphLike, *, return_means: Literal[False] = ...) -> float: ...
12
+
13
+
14
+ @overload
15
+ def compute_coherence(
16
+ G: GraphLike, *, return_means: Literal[True]
17
+ ) -> tuple[float, float, float]: ...
18
+
19
+
20
+ def compute_coherence(
21
+ G: GraphLike, *, return_means: bool = ...
22
+ ) -> float | tuple[float, float, float]: ...
23
+
24
+ def ensure_neighbors_map(G: GraphLike) -> Mapping[Any, Sequence[Any]]: ...
25
+
26
+ def merge_graph_weights(G: GraphLike, key: str) -> dict[str, float]: ...
27
+
28
+ def merge_and_normalize_weights(
29
+ G: GraphLike,
30
+ key: str,
31
+ fields: Sequence[str],
32
+ *,
33
+ default: float = ...,
34
+ ) -> dict[str, float]: ...
35
+
36
+ def compute_dnfr_accel_max(G: GraphLike) -> dict[str, float]: ...
37
+
38
+ def normalize_dnfr(nd: NodeAttrMap, max_val: float) -> float: ...
39
+
40
+ def min_max_range(
41
+ values: Iterable[float], *, default: tuple[float, float] = ...
42
+ ) -> tuple[float, float]: ...
43
+
44
+ def _get_vf_dnfr_max(G: GraphLike) -> tuple[float, float]: ...
45
+
46
+ def _coerce_jobs(raw_jobs: Any | None) -> int | None: ...