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/core.py CHANGED
@@ -2,9 +2,19 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Mapping
5
+ from collections.abc import Mapping, MutableMapping
6
6
  from typing import Any, NamedTuple, cast
7
7
 
8
+ from ..callback_utils import CallbackEvent, callback_manager
9
+ from ..constants import get_param
10
+ from ..glyph_history import append_metric, ensure_history
11
+ from ..telemetry import ensure_nu_f_telemetry
12
+ from ..units import get_hz_bridge
13
+ from ..telemetry.verbosity import (
14
+ TELEMETRY_VERBOSITY_DEFAULT,
15
+ TELEMETRY_VERBOSITY_LEVELS,
16
+ TelemetryVerbosity,
17
+ )
8
18
  from ..types import (
9
19
  GlyphSelector,
10
20
  NodeId,
@@ -17,32 +27,23 @@ from ..types import (
17
27
  TraceFieldMap,
18
28
  TraceFieldRegistry,
19
29
  )
20
-
21
- from ..callback_utils import CallbackEvent, callback_manager
22
- from ..constants import get_param
23
- from ..glyph_history import append_metric, ensure_history
24
30
  from ..utils import get_logger
25
- from ..telemetry.verbosity import (
26
- TelemetryVerbosity,
27
- TELEMETRY_VERBOSITY_DEFAULT,
28
- TELEMETRY_VERBOSITY_LEVELS,
29
- )
30
31
  from .coherence import (
32
+ GLYPH_LOAD_STABILIZERS_KEY,
31
33
  _aggregate_si,
32
34
  _track_stability,
33
35
  _update_coherence,
34
36
  _update_phase_sync,
35
37
  _update_sigma,
36
38
  register_coherence_callbacks,
37
- GLYPH_LOAD_STABILIZERS_KEY,
38
39
  )
39
40
  from .diagnosis import register_diagnosis_callbacks
40
- from .glyph_timing import _compute_advanced_metrics, GlyphMetricsHistory
41
+ from .glyph_timing import GlyphMetricsHistory, _compute_advanced_metrics
41
42
  from .reporting import (
42
43
  Tg_by_node,
43
44
  Tg_global,
44
- glyphogram_series,
45
45
  glyph_top,
46
+ glyphogram_series,
46
47
  latency_series,
47
48
  )
48
49
 
@@ -79,6 +80,7 @@ class MetricsVerbositySpec(NamedTuple):
79
80
  enable_advanced: bool
80
81
  attach_coherence_hooks: bool
81
82
  attach_diagnosis_hooks: bool
83
+ enable_nu_f: bool
82
84
 
83
85
 
84
86
  METRICS_VERBOSITY_DEFAULT = TELEMETRY_VERBOSITY_DEFAULT
@@ -89,7 +91,8 @@ _METRICS_VERBOSITY_PRESETS: dict[str, MetricsVerbositySpec] = {}
89
91
  def _register_metrics_preset(spec: MetricsVerbositySpec) -> None:
90
92
  if spec.name not in TELEMETRY_VERBOSITY_LEVELS:
91
93
  raise ValueError(
92
- "Unknown metrics verbosity '%s'; use %s" % (
94
+ "Unknown metrics verbosity '%s'; use %s"
95
+ % (
93
96
  spec.name,
94
97
  ", ".join(TELEMETRY_VERBOSITY_LEVELS),
95
98
  )
@@ -106,6 +109,7 @@ _register_metrics_preset(
106
109
  enable_advanced=False,
107
110
  attach_coherence_hooks=False,
108
111
  attach_diagnosis_hooks=False,
112
+ enable_nu_f=False,
109
113
  )
110
114
  )
111
115
 
@@ -117,6 +121,7 @@ _detailed_spec = MetricsVerbositySpec(
117
121
  enable_advanced=False,
118
122
  attach_coherence_hooks=True,
119
123
  attach_diagnosis_hooks=False,
124
+ enable_nu_f=True,
120
125
  )
121
126
  _register_metrics_preset(_detailed_spec)
122
127
  _register_metrics_preset(
@@ -124,6 +129,7 @@ _register_metrics_preset(
124
129
  name=TelemetryVerbosity.DEBUG.value,
125
130
  enable_advanced=True,
126
131
  attach_diagnosis_hooks=True,
132
+ enable_nu_f=True,
127
133
  )
128
134
  )
129
135
 
@@ -139,6 +145,51 @@ _METRICS_SIGMA_HISTORY_KEYS = (
139
145
  "sense_sigma_angle",
140
146
  )
141
147
  _METRICS_SI_HISTORY_KEYS = ("Si_mean", "Si_hi_frac", "Si_lo_frac")
148
+ _METRICS_NU_F_HISTORY_KEYS = (
149
+ "nu_f_rate_hz_str",
150
+ "nu_f_rate_hz",
151
+ "nu_f_ci_lower_hz_str",
152
+ "nu_f_ci_upper_hz_str",
153
+ "nu_f_ci_lower_hz",
154
+ "nu_f_ci_upper_hz",
155
+ )
156
+
157
+
158
+ def _update_nu_f_snapshot(
159
+ G: TNFRGraph,
160
+ hist: MutableMapping[str, Any],
161
+ *,
162
+ record_history: bool,
163
+ ) -> None:
164
+ """Refresh νf telemetry snapshot and optionally persist it in history."""
165
+
166
+ accumulator = ensure_nu_f_telemetry(G, confidence_level=None)
167
+ snapshot = accumulator.snapshot(graph=G)
168
+ payload = snapshot.as_payload()
169
+ bridge: float | None
170
+ try:
171
+ bridge = float(get_hz_bridge(G))
172
+ except (TypeError, ValueError, KeyError):
173
+ bridge = None
174
+ else:
175
+ payload["hz_bridge"] = bridge
176
+
177
+ telemetry = G.graph.setdefault("telemetry", {})
178
+ if not isinstance(telemetry, MutableMapping):
179
+ telemetry = {}
180
+ G.graph["telemetry"] = telemetry
181
+ telemetry["nu_f_snapshot"] = payload
182
+ telemetry["nu_f_bridge"] = bridge
183
+
184
+ if record_history:
185
+ append_metric(hist, "nu_f_rate_hz_str", snapshot.rate_hz_str)
186
+ append_metric(hist, "nu_f_rate_hz", snapshot.rate_hz)
187
+ append_metric(hist, "nu_f_ci_lower_hz_str", snapshot.ci_lower_hz_str)
188
+ append_metric(hist, "nu_f_ci_upper_hz_str", snapshot.ci_upper_hz_str)
189
+ append_metric(hist, "nu_f_ci_lower_hz", snapshot.ci_lower_hz)
190
+ append_metric(hist, "nu_f_ci_upper_hz", snapshot.ci_upper_hz)
191
+
192
+ G.graph["_nu_f_snapshot_payload"] = payload
142
193
 
143
194
 
144
195
  def _resolve_metrics_verbosity(cfg: Mapping[str, Any]) -> MetricsVerbositySpec:
@@ -187,6 +238,9 @@ def _metrics_step(G: TNFRGraph, ctx: dict[str, Any] | None = None) -> None:
187
238
  if spec.enable_aggregate_si:
188
239
  for key in _METRICS_SI_HISTORY_KEYS:
189
240
  hist.setdefault(key, [])
241
+ if spec.enable_nu_f:
242
+ for key in _METRICS_NU_F_HISTORY_KEYS:
243
+ hist.setdefault(key, [])
190
244
  G.graph[metrics_sentinel_key] = history_id
191
245
 
192
246
  dt = float(get_param(G, "DT"))
@@ -233,6 +287,8 @@ def _metrics_step(G: TNFRGraph, ctx: dict[str, Any] | None = None) -> None:
233
287
  if spec.enable_aggregate_si:
234
288
  _aggregate_si(G, hist, n_jobs=metrics_jobs)
235
289
 
290
+ _update_nu_f_snapshot(G, hist, record_history=spec.enable_nu_f)
291
+
236
292
  if spec.enable_advanced:
237
293
  _compute_advanced_metrics(
238
294
  G,
@@ -245,6 +301,8 @@ def _metrics_step(G: TNFRGraph, ctx: dict[str, Any] | None = None) -> None:
245
301
 
246
302
 
247
303
  def register_metrics_callbacks(G: TNFRGraph) -> None:
304
+ """Attach canonical metrics callbacks according to graph configuration."""
305
+
248
306
  cfg = cast(Mapping[str, Any], get_param(G, "METRICS"))
249
307
  spec = _resolve_metrics_verbosity(cfg)
250
308
  callback_manager.register_callback(
tnfr/metrics/diagnosis.py CHANGED
@@ -3,28 +3,28 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import math
6
+ from collections import deque
7
+ from collections.abc import Mapping, MutableMapping, Sequence
6
8
  from concurrent.futures import ProcessPoolExecutor
7
9
  from dataclasses import dataclass
8
10
  from functools import partial
9
11
  from operator import ge, le
10
12
  from statistics import StatisticsError, fmean
11
13
  from typing import Any, Callable, Iterable, cast
12
- from collections import deque
13
- from collections.abc import Mapping, MutableMapping, Sequence
14
14
 
15
+ from ..alias import get_attr
16
+ from ..callback_utils import CallbackEvent, callback_manager
15
17
  from ..constants import (
16
18
  STATE_DISSONANT,
17
19
  STATE_STABLE,
18
20
  STATE_TRANSITION,
19
21
  VF_KEY,
20
- get_aliases,
21
22
  get_param,
22
23
  normalise_state_token,
23
24
  )
24
- from ..callback_utils import CallbackEvent, callback_manager
25
+ from ..constants.aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_VF
25
26
  from ..glyph_history import append_metric, ensure_history
26
- from ..alias import get_attr
27
- from ..helpers.numeric import clamp01, similarity_abs
27
+ from ..utils import clamp01, resolve_chunk_size, similarity_abs
28
28
  from ..types import (
29
29
  DiagnosisNodeData,
30
30
  DiagnosisPayload,
@@ -36,31 +36,17 @@ from ..types import (
36
36
  TNFRGraph,
37
37
  )
38
38
  from ..utils import get_numpy
39
- from .common import compute_dnfr_accel_max, min_max_range, normalize_dnfr
40
39
  from .coherence import CoherenceMatrixPayload, coherence_matrix, local_phase_sync
40
+ from .common import (
41
+ _coerce_jobs,
42
+ compute_dnfr_accel_max,
43
+ min_max_range,
44
+ normalize_dnfr,
45
+ )
41
46
  from .trig_cache import compute_theta_trig, get_trig_cache
42
47
 
43
- ALIAS_EPI = get_aliases("EPI")
44
- ALIAS_VF = get_aliases("VF")
45
- ALIAS_SI = get_aliases("SI")
46
- ALIAS_DNFR = get_aliases("DNFR")
47
-
48
48
  CoherenceSeries = Sequence[CoherenceMatrixPayload | None]
49
49
  CoherenceHistory = Mapping[str, CoherenceSeries]
50
-
51
-
52
- def _coerce_jobs(raw_jobs: Any | None) -> int | None:
53
- """Normalise ``n_jobs`` values coming from user configuration."""
54
-
55
- try:
56
- jobs = None if raw_jobs is None else int(raw_jobs)
57
- except (TypeError, ValueError):
58
- return None
59
- if jobs is not None and jobs <= 0:
60
- return None
61
- return jobs
62
-
63
-
64
50
  def _coherence_matrix_to_numpy(
65
51
  weight_matrix: Any,
66
52
  size: int,
@@ -376,7 +362,9 @@ def _node_diagnostics(
376
362
 
377
363
  if compute_symmetry:
378
364
  epi_bar = node_data.get("neighbor_epi_mean")
379
- symm = 1.0 if epi_bar is None else similarity_abs(EPI, epi_bar, epi_min, epi_max)
365
+ symm = (
366
+ 1.0 if epi_bar is None else similarity_abs(EPI, epi_bar, epi_min, epi_max)
367
+ )
380
368
  else:
381
369
  symm = None
382
370
 
@@ -505,10 +493,7 @@ def _diagnosis_step(
505
493
 
506
494
  if supports_vector:
507
495
  epi_arr = np_mod.fromiter(
508
- (
509
- cast(float, get_attr(nd, ALIAS_EPI, 0.0))
510
- for _, nd in nodes_data
511
- ),
496
+ (cast(float, get_attr(nd, ALIAS_EPI, 0.0)) for _, nd in nodes_data),
512
497
  dtype=float,
513
498
  count=len(nodes_data),
514
499
  )
@@ -518,10 +503,7 @@ def _diagnosis_step(
518
503
 
519
504
  si_arr = np_mod.clip(
520
505
  np_mod.fromiter(
521
- (
522
- cast(float, get_attr(nd, ALIAS_SI, 0.0))
523
- for _, nd in nodes_data
524
- ),
506
+ (cast(float, get_attr(nd, ALIAS_SI, 0.0)) for _, nd in nodes_data),
525
507
  dtype=float,
526
508
  count=len(nodes_data),
527
509
  ),
@@ -531,10 +513,7 @@ def _diagnosis_step(
531
513
  si_vals = si_arr.tolist()
532
514
 
533
515
  vf_arr = np_mod.fromiter(
534
- (
535
- cast(float, get_attr(nd, ALIAS_VF, 0.0))
536
- for _, nd in nodes_data
537
- ),
516
+ (cast(float, get_attr(nd, ALIAS_VF, 0.0)) for _, nd in nodes_data),
538
517
  dtype=float,
539
518
  count=len(nodes_data),
540
519
  )
@@ -595,9 +574,7 @@ def _diagnosis_step(
595
574
  if supports_vector:
596
575
  size = len(coherence_nodes)
597
576
  matrix_np = (
598
- _coherence_matrix_to_numpy(weight_matrix, size, np_mod)
599
- if size
600
- else None
577
+ _coherence_matrix_to_numpy(weight_matrix, size, np_mod) if size else None
601
578
  )
602
579
  if matrix_np is not None and size:
603
580
  cos_weight = np_mod.fromiter(
@@ -617,8 +594,7 @@ def _diagnosis_step(
617
594
  np_mod,
618
595
  )
619
596
  rloc_map = {
620
- coherence_nodes[idx]: float(weighted_sync[idx])
621
- for idx in range(size)
597
+ coherence_nodes[idx]: float(weighted_sync[idx]) for idx in range(size)
622
598
  }
623
599
  else:
624
600
  rloc_map = {}
@@ -647,14 +623,19 @@ def _diagnosis_step(
647
623
  rloc_values = [rloc_map.get(node, 0.0) for node in nodes]
648
624
  else:
649
625
  if n_jobs and n_jobs > 1 and len(nodes) > 1:
650
- chunk_size = max(1, math.ceil(len(nodes) / n_jobs))
626
+ approx_chunk = math.ceil(len(nodes) / n_jobs) if n_jobs else None
627
+ chunk_size = resolve_chunk_size(
628
+ approx_chunk,
629
+ len(nodes),
630
+ minimum=1,
631
+ )
651
632
  rloc_values = []
652
633
  with ProcessPoolExecutor(max_workers=n_jobs) as executor:
653
634
  futures = [
654
635
  executor.submit(
655
636
  _rlocal_worker,
656
637
  RLocalWorkerArgs(
657
- chunk=nodes[idx:idx + chunk_size],
638
+ chunk=nodes[idx : idx + chunk_size],
658
639
  coherence_nodes=coherence_nodes,
659
640
  weight_matrix=weight_matrix,
660
641
  weight_index=weight_index,
@@ -681,7 +662,9 @@ def _diagnosis_step(
681
662
  )
682
663
 
683
664
  if isinstance(Wi_last, (list, tuple)) and Wi_last:
684
- wi_values = [Wi_last[i] if i < len(Wi_last) else None for i in range(len(nodes))]
665
+ wi_values = [
666
+ Wi_last[i] if i < len(Wi_last) else None for i in range(len(nodes))
667
+ ]
685
668
  else:
686
669
  wi_values = [None] * len(nodes)
687
670
 
@@ -697,7 +680,12 @@ def _diagnosis_step(
697
680
  np_mod,
698
681
  )
699
682
  elif n_jobs and n_jobs > 1 and len(nodes) > 1:
700
- chunk_size = max(1, math.ceil(len(nodes) / n_jobs))
683
+ approx_chunk = math.ceil(len(nodes) / n_jobs) if n_jobs else None
684
+ chunk_size = resolve_chunk_size(
685
+ approx_chunk,
686
+ len(nodes),
687
+ minimum=1,
688
+ )
701
689
  neighbor_means = cast(list[float | None], [])
702
690
  with ProcessPoolExecutor(max_workers=n_jobs) as executor:
703
691
  submit = cast(Callable[..., Any], executor.submit)
@@ -708,7 +696,7 @@ def _diagnosis_step(
708
696
  _neighbor_mean_worker,
709
697
  ),
710
698
  NeighborMeanWorkerArgs(
711
- chunk=nodes[idx:idx + chunk_size],
699
+ chunk=nodes[idx : idx + chunk_size],
712
700
  neighbors_map=neighbors_map,
713
701
  epi_map=epi_map,
714
702
  ),
@@ -716,9 +704,7 @@ def _diagnosis_step(
716
704
  for idx in range(0, len(nodes), chunk_size)
717
705
  ]
718
706
  for fut in futures:
719
- neighbor_means.extend(
720
- cast(list[float | None], fut.result())
721
- )
707
+ neighbor_means.extend(cast(list[float | None], fut.result()))
722
708
  else:
723
709
  neighbor_means = _neighbor_mean_worker(
724
710
  NeighborMeanWorkerArgs(
@@ -754,7 +740,12 @@ def _diagnosis_step(
754
740
  }
755
741
 
756
742
  if n_jobs and n_jobs > 1 and len(node_payload) > 1:
757
- chunk_size = max(1, math.ceil(len(node_payload) / n_jobs))
743
+ approx_chunk = math.ceil(len(node_payload) / n_jobs) if n_jobs else None
744
+ chunk_size = resolve_chunk_size(
745
+ approx_chunk,
746
+ len(node_payload),
747
+ minimum=1,
748
+ )
758
749
  diag_pairs: DiagnosisResultList = []
759
750
  with ProcessPoolExecutor(max_workers=n_jobs) as executor:
760
751
  submit = cast(Callable[..., Any], executor.submit)
@@ -767,7 +758,7 @@ def _diagnosis_step(
767
758
  ],
768
759
  _diagnosis_worker_chunk,
769
760
  ),
770
- node_payload[idx:idx + chunk_size],
761
+ node_payload[idx : idx + chunk_size],
771
762
  shared,
772
763
  )
773
764
  for idx in range(0, len(node_payload), chunk_size)
@@ -785,9 +776,7 @@ def _diagnosis_step(
785
776
  append_metric(hist, key, diag)
786
777
 
787
778
 
788
- def dissonance_events(
789
- G: TNFRGraph, ctx: DiagnosisSharedState | None = None
790
- ) -> None:
779
+ def dissonance_events(G: TNFRGraph, ctx: DiagnosisSharedState | None = None) -> None:
791
780
  """Emit per-node structural dissonance start/end events.
792
781
 
793
782
  Events are recorded as ``"dissonance_start"`` and ``"dissonance_end"``.
@@ -823,6 +812,8 @@ def dissonance_events(
823
812
 
824
813
 
825
814
  def register_diagnosis_callbacks(G: TNFRGraph) -> None:
815
+ """Attach diagnosis observers (Si/dissonance tracking) to ``G``."""
816
+
826
817
  raw_jobs = G.graph.get("DIAGNOSIS_N_JOBS")
827
818
  n_jobs = _coerce_jobs(raw_jobs)
828
819
 
@@ -61,7 +61,6 @@ class RLocalWorkerArgs:
61
61
  sin_map: Mapping[Any, float],
62
62
  ) -> None: ...
63
63
 
64
-
65
64
  class NeighborMeanWorkerArgs:
66
65
  chunk: Sequence[Any]
67
66
  neighbors_map: Mapping[Any, tuple[Any, ...]]
@@ -74,15 +73,12 @@ class NeighborMeanWorkerArgs:
74
73
  epi_map: Mapping[Any, float],
75
74
  ) -> None: ...
76
75
 
77
-
78
76
  def _rlocal_worker(args: RLocalWorkerArgs) -> list[float]: ...
79
-
80
77
  def _neighbor_mean_worker(args: NeighborMeanWorkerArgs) -> list[float | None]: ...
81
-
82
- def _state_from_thresholds(Rloc: float, dnfr_n: float, cfg: Mapping[str, Any]) -> str: ...
83
-
78
+ def _state_from_thresholds(
79
+ Rloc: float, dnfr_n: float, cfg: Mapping[str, Any]
80
+ ) -> str: ...
84
81
  def _recommendation(state: str, cfg: Mapping[str, Any]) -> list[Any]: ...
85
-
86
82
  def _get_last_weights(
87
83
  G: TNFRGraph,
88
84
  hist: Mapping[str, Sequence[CoherenceMatrixPayload | None]],
tnfr/metrics/export.py CHANGED
@@ -5,16 +5,14 @@ from __future__ import annotations
5
5
  import csv
6
6
  import math
7
7
  from collections.abc import Iterable, Iterator, Sequence
8
- from itertools import zip_longest, tee
8
+ from itertools import tee, zip_longest
9
9
  from typing import Mapping, TextIO
10
10
 
11
11
  from ..config.constants import GLYPHS_CANONICAL
12
12
  from ..glyph_history import ensure_history
13
- from ..io import safe_write
14
- from ..utils import json_dumps
15
- from ..types import Graph
13
+ from ..utils import json_dumps, safe_write
14
+ from ..types import Graph, SigmaTrace
16
15
  from .core import glyphogram_series
17
- from .glyph_timing import SigmaTrace
18
16
 
19
17
 
20
18
  def _write_csv(
@@ -2,20 +2,38 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import math
5
6
  from collections import Counter, defaultdict
6
7
  from concurrent.futures import ProcessPoolExecutor
7
8
  from dataclasses import dataclass
8
- import math
9
9
  from types import ModuleType
10
- from typing import Any, Callable, Mapping, MutableMapping, MutableSequence, Sequence, TypedDict, cast
10
+ from typing import (
11
+ Any,
12
+ Callable,
13
+ Mapping,
14
+ MutableMapping,
15
+ Sequence,
16
+ cast,
17
+ )
11
18
 
12
19
  from ..alias import get_attr
13
- from ..constants import get_aliases, get_param
14
20
  from ..config.constants import GLYPH_GROUPS, GLYPHS_CANONICAL
15
- from ..glyph_history import append_metric, last_glyph
16
- from ..types import GraphLike
17
-
18
- ALIAS_EPI = get_aliases("EPI")
21
+ from ..constants import get_param
22
+ from ..constants.aliases import ALIAS_EPI
23
+ from ..glyph_history import append_metric
24
+ from ..glyph_runtime import last_glyph
25
+ from ..utils import resolve_chunk_size
26
+ from ..types import (
27
+ GlyphCounts,
28
+ GlyphMetricsHistory,
29
+ GlyphMetricsHistoryValue,
30
+ GlyphTimingByNode,
31
+ GlyphTimingTotals,
32
+ GlyphogramRow,
33
+ GraphLike,
34
+ MetricsListHistory,
35
+ SigmaTrace,
36
+ )
19
37
 
20
38
  LATENT_GLYPH: str = "SHA"
21
39
  DEFAULT_EPI_SUPPORT_LIMIT = 0.05
@@ -36,26 +54,6 @@ def _has_numpy_support(np_obj: object) -> bool:
36
54
  and hasattr(np_obj, "fromiter")
37
55
  and hasattr(np_obj, "bincount")
38
56
  )
39
-
40
-
41
- class SigmaTrace(TypedDict):
42
- """Time-aligned σ(t) trace exported alongside glyphograms."""
43
-
44
- t: list[float]
45
- sigma_x: list[float]
46
- sigma_y: list[float]
47
- mag: list[float]
48
- angle: list[float]
49
-
50
-
51
- GlyphogramRow = MutableMapping[str, float]
52
- GlyphTimingTotals = MutableMapping[str, float]
53
- GlyphTimingByNode = MutableMapping[Any, MutableMapping[str, MutableSequence[float]]]
54
- GlyphCounts = Mapping[str, int]
55
- GlyphMetricsHistoryValue = MutableMapping[Any, Any] | MutableSequence[Any]
56
- GlyphMetricsHistory = MutableMapping[str, GlyphMetricsHistoryValue]
57
- MetricsListHistory = MutableMapping[str, list[Any]]
58
-
59
57
  _GLYPH_TO_INDEX = {glyph: idx for idx, glyph in enumerate(GLYPHS_CANONICAL)}
60
58
 
61
59
 
@@ -70,6 +68,8 @@ def _coerce_float(value: Any) -> float:
70
68
 
71
69
  @dataclass
72
70
  class GlyphTiming:
71
+ """Mutable accumulator tracking the active glyph and its dwell time."""
72
+
73
73
  curr: str | None = None
74
74
  run: float = 0.0
75
75
 
@@ -219,11 +219,16 @@ def _update_tg(
219
219
  }
220
220
  )
221
221
  elif n_jobs is not None and n_jobs > 1 and len(glyph_sequence) > 1:
222
- chunk_size = max(1, math.ceil(len(glyph_sequence) / n_jobs))
222
+ approx_chunk = math.ceil(len(glyph_sequence) / n_jobs) if n_jobs else None
223
+ chunk_size = resolve_chunk_size(
224
+ approx_chunk,
225
+ len(glyph_sequence),
226
+ minimum=1,
227
+ )
223
228
  futures = []
224
229
  with ProcessPoolExecutor(max_workers=n_jobs) as executor:
225
230
  for start in range(0, len(glyph_sequence), chunk_size):
226
- chunk = glyph_sequence[start:start + chunk_size]
231
+ chunk = glyph_sequence[start : start + chunk_size]
227
232
  futures.append(executor.submit(_count_glyphs_chunk, chunk))
228
233
  for future in futures:
229
234
  counts.update(future.result())
@@ -299,12 +304,17 @@ def _update_epi_support(
299
304
  abs(_coerce_float(get_attr(nd, ALIAS_EPI, 0.0)))
300
305
  for _, nd in G.nodes(data=True)
301
306
  ]
302
- chunk_size = max(1, math.ceil(len(values) / n_jobs))
307
+ approx_chunk = math.ceil(len(values) / n_jobs) if n_jobs else None
308
+ chunk_size = resolve_chunk_size(
309
+ approx_chunk,
310
+ len(values),
311
+ minimum=1,
312
+ )
303
313
  totals: list[tuple[float, int]] = []
304
314
  with ProcessPoolExecutor(max_workers=n_jobs) as executor:
305
315
  futures = []
306
316
  for start in range(0, len(values), chunk_size):
307
- chunk = values[start:start + chunk_size]
317
+ chunk = values[start : start + chunk_size]
308
318
  futures.append(executor.submit(_epi_support_chunk, chunk, threshold))
309
319
  for future in futures:
310
320
  totals.append(future.result())
tnfr/metrics/reporting.py CHANGED
@@ -3,14 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Sequence
6
- from typing import Any
7
-
8
6
  from heapq import nlargest
9
- from statistics import mean, fmean, StatisticsError
7
+ from statistics import StatisticsError, fmean, mean
8
+ from typing import Any
10
9
 
11
10
  from ..glyph_history import ensure_history
12
- from ..types import NodeId, TNFRGraph
13
11
  from ..sense import sigma_rose
12
+ from ..types import NodeId, TNFRGraph
14
13
  from .glyph_timing import for_each_glyph
15
14
 
16
15
  __all__ = [
@@ -70,6 +69,8 @@ def Tg_by_node(
70
69
 
71
70
 
72
71
  def latency_series(G: TNFRGraph) -> dict[str, list[float]]:
72
+ """Return latency samples as ``{"t": [...], "value": [...]}``."""
73
+
73
74
  hist = ensure_history(G)
74
75
  xs = hist.get("latency_index", [])
75
76
  return {
@@ -79,11 +80,15 @@ def latency_series(G: TNFRGraph) -> dict[str, list[float]]:
79
80
 
80
81
 
81
82
  def glyphogram_series(G: TNFRGraph) -> dict[str, list[float]]:
83
+ """Return glyphogram time series keyed by glyph label."""
84
+
82
85
  hist = ensure_history(G)
83
86
  xs = hist.get("glyphogram", [])
84
87
  if not xs:
85
88
  return {"t": []}
86
- out: dict[str, list[float]] = {"t": [float(x.get("t", i)) for i, x in enumerate(xs)]}
89
+ out: dict[str, list[float]] = {
90
+ "t": [float(x.get("t", i)) for i, x in enumerate(xs)]
91
+ }
87
92
 
88
93
  def add(g: str) -> None:
89
94
  out[g] = [float(x.get(g, 0.0)) for x in xs]
@@ -104,7 +109,9 @@ def glyph_top(G: TNFRGraph, k: int = 3) -> list[tuple[str, float]]:
104
109
 
105
110
  def build_metrics_summary(
106
111
  G: TNFRGraph, *, series_limit: int | None = None
107
- ) -> tuple[dict[str, float | dict[str, float] | dict[str, list[float]] | dict[str, int]], bool]:
112
+ ) -> tuple[
113
+ dict[str, float | dict[str, float] | dict[str, list[float]] | dict[str, int]], bool
114
+ ]:
108
115
  """Collect a compact metrics summary for CLI reporting.
109
116
 
110
117
  Parameters