tnfr 4.5.2__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 (195) hide show
  1. tnfr/__init__.py +275 -51
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +117 -31
  8. tnfr/alias.pyi +108 -0
  9. tnfr/cache.py +6 -572
  10. tnfr/cache.pyi +16 -0
  11. tnfr/callback_utils.py +16 -38
  12. tnfr/callback_utils.pyi +79 -0
  13. tnfr/cli/__init__.py +34 -14
  14. tnfr/cli/__init__.pyi +26 -0
  15. tnfr/cli/arguments.py +211 -28
  16. tnfr/cli/arguments.pyi +27 -0
  17. tnfr/cli/execution.py +470 -50
  18. tnfr/cli/execution.pyi +70 -0
  19. tnfr/cli/utils.py +18 -3
  20. tnfr/cli/utils.pyi +8 -0
  21. tnfr/config/__init__.py +13 -0
  22. tnfr/config/__init__.pyi +10 -0
  23. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  24. tnfr/config/constants.pyi +12 -0
  25. tnfr/config/feature_flags.py +83 -0
  26. tnfr/{config.py → config/init.py} +11 -7
  27. tnfr/config/init.pyi +8 -0
  28. tnfr/config/operator_names.py +93 -0
  29. tnfr/config/operator_names.pyi +28 -0
  30. tnfr/config/presets.py +84 -0
  31. tnfr/config/presets.pyi +7 -0
  32. tnfr/constants/__init__.py +80 -29
  33. tnfr/constants/__init__.pyi +92 -0
  34. tnfr/constants/aliases.py +31 -0
  35. tnfr/constants/core.py +4 -4
  36. tnfr/constants/core.pyi +17 -0
  37. tnfr/constants/init.py +1 -1
  38. tnfr/constants/init.pyi +12 -0
  39. tnfr/constants/metric.py +7 -15
  40. tnfr/constants/metric.pyi +19 -0
  41. tnfr/dynamics/__init__.py +165 -633
  42. tnfr/dynamics/__init__.pyi +82 -0
  43. tnfr/dynamics/adaptation.py +267 -0
  44. tnfr/dynamics/aliases.py +23 -0
  45. tnfr/dynamics/coordination.py +385 -0
  46. tnfr/dynamics/dnfr.py +2283 -400
  47. tnfr/dynamics/dnfr.pyi +24 -0
  48. tnfr/dynamics/integrators.py +406 -98
  49. tnfr/dynamics/integrators.pyi +34 -0
  50. tnfr/dynamics/runtime.py +881 -0
  51. tnfr/dynamics/sampling.py +10 -5
  52. tnfr/dynamics/sampling.pyi +7 -0
  53. tnfr/dynamics/selectors.py +719 -0
  54. tnfr/execution.py +70 -48
  55. tnfr/execution.pyi +45 -0
  56. tnfr/flatten.py +13 -9
  57. tnfr/flatten.pyi +21 -0
  58. tnfr/gamma.py +66 -53
  59. tnfr/gamma.pyi +34 -0
  60. tnfr/glyph_history.py +110 -52
  61. tnfr/glyph_history.pyi +35 -0
  62. tnfr/glyph_runtime.py +16 -0
  63. tnfr/glyph_runtime.pyi +9 -0
  64. tnfr/immutable.py +69 -28
  65. tnfr/immutable.pyi +34 -0
  66. tnfr/initialization.py +16 -16
  67. tnfr/initialization.pyi +65 -0
  68. tnfr/io.py +6 -240
  69. tnfr/io.pyi +16 -0
  70. tnfr/locking.pyi +7 -0
  71. tnfr/mathematics/__init__.py +81 -0
  72. tnfr/mathematics/backend.py +426 -0
  73. tnfr/mathematics/dynamics.py +398 -0
  74. tnfr/mathematics/epi.py +254 -0
  75. tnfr/mathematics/generators.py +222 -0
  76. tnfr/mathematics/metrics.py +119 -0
  77. tnfr/mathematics/operators.py +233 -0
  78. tnfr/mathematics/operators_factory.py +71 -0
  79. tnfr/mathematics/projection.py +78 -0
  80. tnfr/mathematics/runtime.py +173 -0
  81. tnfr/mathematics/spaces.py +247 -0
  82. tnfr/mathematics/transforms.py +292 -0
  83. tnfr/metrics/__init__.py +10 -10
  84. tnfr/metrics/__init__.pyi +20 -0
  85. tnfr/metrics/coherence.py +993 -324
  86. tnfr/metrics/common.py +23 -16
  87. tnfr/metrics/common.pyi +46 -0
  88. tnfr/metrics/core.py +251 -35
  89. tnfr/metrics/core.pyi +13 -0
  90. tnfr/metrics/diagnosis.py +708 -111
  91. tnfr/metrics/diagnosis.pyi +85 -0
  92. tnfr/metrics/export.py +27 -15
  93. tnfr/metrics/glyph_timing.py +232 -42
  94. tnfr/metrics/reporting.py +33 -22
  95. tnfr/metrics/reporting.pyi +12 -0
  96. tnfr/metrics/sense_index.py +987 -43
  97. tnfr/metrics/sense_index.pyi +9 -0
  98. tnfr/metrics/trig.py +214 -23
  99. tnfr/metrics/trig.pyi +13 -0
  100. tnfr/metrics/trig_cache.py +115 -22
  101. tnfr/metrics/trig_cache.pyi +10 -0
  102. tnfr/node.py +542 -136
  103. tnfr/node.pyi +178 -0
  104. tnfr/observers.py +152 -35
  105. tnfr/observers.pyi +31 -0
  106. tnfr/ontosim.py +23 -19
  107. tnfr/ontosim.pyi +28 -0
  108. tnfr/operators/__init__.py +601 -82
  109. tnfr/operators/__init__.pyi +45 -0
  110. tnfr/operators/definitions.py +513 -0
  111. tnfr/operators/definitions.pyi +78 -0
  112. tnfr/operators/grammar.py +760 -0
  113. tnfr/operators/jitter.py +107 -38
  114. tnfr/operators/jitter.pyi +11 -0
  115. tnfr/operators/registry.py +75 -0
  116. tnfr/operators/registry.pyi +13 -0
  117. tnfr/operators/remesh.py +149 -88
  118. tnfr/py.typed +0 -0
  119. tnfr/rng.py +46 -143
  120. tnfr/rng.pyi +14 -0
  121. tnfr/schemas/__init__.py +8 -0
  122. tnfr/schemas/grammar.json +94 -0
  123. tnfr/selector.py +25 -19
  124. tnfr/selector.pyi +19 -0
  125. tnfr/sense.py +72 -62
  126. tnfr/sense.pyi +23 -0
  127. tnfr/structural.py +522 -262
  128. tnfr/structural.pyi +69 -0
  129. tnfr/telemetry/__init__.py +35 -0
  130. tnfr/telemetry/cache_metrics.py +226 -0
  131. tnfr/telemetry/nu_f.py +423 -0
  132. tnfr/telemetry/nu_f.pyi +123 -0
  133. tnfr/telemetry/verbosity.py +37 -0
  134. tnfr/tokens.py +1 -3
  135. tnfr/tokens.pyi +36 -0
  136. tnfr/trace.py +270 -113
  137. tnfr/trace.pyi +40 -0
  138. tnfr/types.py +574 -6
  139. tnfr/types.pyi +331 -0
  140. tnfr/units.py +69 -0
  141. tnfr/units.pyi +16 -0
  142. tnfr/utils/__init__.py +217 -0
  143. tnfr/utils/__init__.pyi +202 -0
  144. tnfr/utils/cache.py +2395 -0
  145. tnfr/utils/cache.pyi +468 -0
  146. tnfr/utils/chunks.py +104 -0
  147. tnfr/utils/chunks.pyi +21 -0
  148. tnfr/{collections_utils.py → utils/data.py} +147 -90
  149. tnfr/utils/data.pyi +64 -0
  150. tnfr/utils/graph.py +85 -0
  151. tnfr/utils/graph.pyi +10 -0
  152. tnfr/utils/init.py +770 -0
  153. tnfr/utils/init.pyi +78 -0
  154. tnfr/utils/io.py +456 -0
  155. tnfr/{helpers → utils}/numeric.py +51 -24
  156. tnfr/utils/numeric.pyi +21 -0
  157. tnfr/validation/__init__.py +113 -0
  158. tnfr/validation/__init__.pyi +77 -0
  159. tnfr/validation/compatibility.py +95 -0
  160. tnfr/validation/compatibility.pyi +6 -0
  161. tnfr/validation/grammar.py +71 -0
  162. tnfr/validation/grammar.pyi +40 -0
  163. tnfr/validation/graph.py +138 -0
  164. tnfr/validation/graph.pyi +17 -0
  165. tnfr/validation/rules.py +281 -0
  166. tnfr/validation/rules.pyi +55 -0
  167. tnfr/validation/runtime.py +263 -0
  168. tnfr/validation/runtime.pyi +31 -0
  169. tnfr/validation/soft_filters.py +170 -0
  170. tnfr/validation/soft_filters.pyi +37 -0
  171. tnfr/validation/spectral.py +159 -0
  172. tnfr/validation/spectral.pyi +46 -0
  173. tnfr/validation/syntax.py +40 -0
  174. tnfr/validation/syntax.pyi +10 -0
  175. tnfr/validation/window.py +39 -0
  176. tnfr/validation/window.pyi +1 -0
  177. tnfr/viz/__init__.py +9 -0
  178. tnfr/viz/matplotlib.py +246 -0
  179. tnfr-7.0.0.dist-info/METADATA +179 -0
  180. tnfr-7.0.0.dist-info/RECORD +185 -0
  181. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  182. tnfr/grammar.py +0 -344
  183. tnfr/graph_utils.py +0 -84
  184. tnfr/helpers/__init__.py +0 -71
  185. tnfr/import_utils.py +0 -228
  186. tnfr/json_utils.py +0 -162
  187. tnfr/logging_utils.py +0 -116
  188. tnfr/presets.py +0 -60
  189. tnfr/validators.py +0 -84
  190. tnfr/value_utils.py +0 -59
  191. tnfr-4.5.2.dist-info/METADATA +0 -379
  192. tnfr-4.5.2.dist-info/RECORD +0 -67
  193. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  194. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  195. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/metrics/common.py CHANGED
@@ -6,17 +6,11 @@ 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 ..collections_utils import normalize_weights
10
- from ..constants import DEFAULTS, get_aliases
11
- from ..cache import edge_version_cache
12
- from ..helpers.numeric import clamp01, kahan_sum_nd
13
- from ..import_utils import get_numpy
14
- from ..types import GraphLike
15
-
16
- ALIAS_DNFR = get_aliases("DNFR")
17
- ALIAS_D2EPI = get_aliases("D2EPI")
18
- ALIAS_DEPI = get_aliases("DEPI")
19
- ALIAS_VF = get_aliases("VF")
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
13
+ from ..utils import edge_version_cache, get_numpy, normalize_weights
20
14
 
21
15
  __all__ = (
22
16
  "GraphLike",
@@ -27,6 +21,7 @@ __all__ = (
27
21
  "merge_graph_weights",
28
22
  "merge_and_normalize_weights",
29
23
  "min_max_range",
24
+ "_coerce_jobs",
30
25
  "_get_vf_dnfr_max",
31
26
  )
32
27
 
@@ -72,7 +67,10 @@ def ensure_neighbors_map(G: GraphLike) -> Mapping[Any, Sequence[Any]]:
72
67
  def merge_graph_weights(G: GraphLike, key: str) -> dict[str, float]:
73
68
  """Merge default weights for ``key`` with any graph overrides."""
74
69
 
75
- 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}
76
74
 
77
75
 
78
76
  def merge_and_normalize_weights(
@@ -103,7 +101,7 @@ def compute_dnfr_accel_max(G: GraphLike) -> dict[str, float]:
103
101
  )
104
102
 
105
103
 
106
- def normalize_dnfr(nd: Mapping[str, Any], max_val: float) -> float:
104
+ def normalize_dnfr(nd: NodeAttrMap, max_val: float) -> float:
107
105
  """Normalise ``|ΔNFR|`` using ``max_val``."""
108
106
 
109
107
  if max_val <= 0:
@@ -137,9 +135,7 @@ def _get_vf_dnfr_max(G: GraphLike) -> tuple[float, float]:
137
135
  vfmax = G.graph.get("_vfmax")
138
136
  dnfrmax = G.graph.get("_dnfrmax")
139
137
  if vfmax is None or dnfrmax is None:
140
- maxes = multi_recompute_abs_max(
141
- G, {"_vfmax": ALIAS_VF, "_dnfrmax": ALIAS_DNFR}
142
- )
138
+ maxes = multi_recompute_abs_max(G, {"_vfmax": ALIAS_VF, "_dnfrmax": ALIAS_DNFR})
143
139
  if vfmax is None:
144
140
  vfmax = maxes["_vfmax"]
145
141
  if dnfrmax is None:
@@ -149,3 +145,14 @@ def _get_vf_dnfr_max(G: GraphLike) -> tuple[float, float]:
149
145
  vfmax = 1.0 if vfmax == 0 else vfmax
150
146
  dnfrmax = 1.0 if dnfrmax == 0 else dnfrmax
151
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
+ )
@@ -0,0 +1,46 @@
1
+ from collections.abc import Iterable, Mapping, Sequence
2
+ from typing import Any, Literal, overload
3
+
4
+ from ..types import GraphLike, NodeAttrMap
5
+
6
+ __all__: tuple[str, ...]
7
+
8
+ def __getattr__(name: str) -> Any: ...
9
+
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: ...
tnfr/metrics/core.py CHANGED
@@ -2,13 +2,34 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any
5
+ from collections.abc import Mapping, MutableMapping
6
+ from typing import Any, NamedTuple, cast
6
7
 
7
8
  from ..callback_utils import CallbackEvent, callback_manager
8
9
  from ..constants import get_param
9
10
  from ..glyph_history import append_metric, ensure_history
10
- from ..logging_utils import get_logger
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
+ )
18
+ from ..types import (
19
+ GlyphSelector,
20
+ NodeId,
21
+ SelectorPreselectionChoices,
22
+ SelectorPreselectionMetrics,
23
+ SelectorPreselectionPayload,
24
+ TNFRGraph,
25
+ TraceCallback,
26
+ TraceFieldFn,
27
+ TraceFieldMap,
28
+ TraceFieldRegistry,
29
+ )
30
+ from ..utils import get_logger
11
31
  from .coherence import (
32
+ GLYPH_LOAD_STABILIZERS_KEY,
12
33
  _aggregate_si,
13
34
  _track_stability,
14
35
  _update_coherence,
@@ -17,18 +38,28 @@ from .coherence import (
17
38
  register_coherence_callbacks,
18
39
  )
19
40
  from .diagnosis import register_diagnosis_callbacks
20
- from .glyph_timing import _compute_advanced_metrics
41
+ from .glyph_timing import GlyphMetricsHistory, _compute_advanced_metrics
21
42
  from .reporting import (
22
43
  Tg_by_node,
23
44
  Tg_global,
24
- glyphogram_series,
25
45
  glyph_top,
46
+ glyphogram_series,
26
47
  latency_series,
27
48
  )
28
49
 
29
50
  logger = get_logger(__name__)
30
51
 
31
52
  __all__ = [
53
+ "TNFRGraph",
54
+ "NodeId",
55
+ "GlyphSelector",
56
+ "SelectorPreselectionMetrics",
57
+ "SelectorPreselectionChoices",
58
+ "SelectorPreselectionPayload",
59
+ "TraceCallback",
60
+ "TraceFieldFn",
61
+ "TraceFieldMap",
62
+ "TraceFieldRegistry",
32
63
  "_metrics_step",
33
64
  "register_metrics_callbacks",
34
65
  "Tg_global",
@@ -39,32 +70,177 @@ __all__ = [
39
70
  ]
40
71
 
41
72
 
42
- def _metrics_step(G, ctx: dict[str, Any] | None = None):
73
+ class MetricsVerbositySpec(NamedTuple):
74
+ """Runtime configuration for metrics verbosity tiers."""
75
+
76
+ name: str
77
+ enable_phase_sync: bool
78
+ enable_sigma: bool
79
+ enable_aggregate_si: bool
80
+ enable_advanced: bool
81
+ attach_coherence_hooks: bool
82
+ attach_diagnosis_hooks: bool
83
+ enable_nu_f: bool
84
+
85
+
86
+ METRICS_VERBOSITY_DEFAULT = TELEMETRY_VERBOSITY_DEFAULT
87
+
88
+ _METRICS_VERBOSITY_PRESETS: dict[str, MetricsVerbositySpec] = {}
89
+
90
+
91
+ def _register_metrics_preset(spec: MetricsVerbositySpec) -> None:
92
+ if spec.name not in TELEMETRY_VERBOSITY_LEVELS:
93
+ raise ValueError(
94
+ "Unknown metrics verbosity '%s'; use %s"
95
+ % (
96
+ spec.name,
97
+ ", ".join(TELEMETRY_VERBOSITY_LEVELS),
98
+ )
99
+ )
100
+ _METRICS_VERBOSITY_PRESETS[spec.name] = spec
101
+
102
+
103
+ _register_metrics_preset(
104
+ MetricsVerbositySpec(
105
+ name=TelemetryVerbosity.BASIC.value,
106
+ enable_phase_sync=False,
107
+ enable_sigma=False,
108
+ enable_aggregate_si=False,
109
+ enable_advanced=False,
110
+ attach_coherence_hooks=False,
111
+ attach_diagnosis_hooks=False,
112
+ enable_nu_f=False,
113
+ )
114
+ )
115
+
116
+ _detailed_spec = MetricsVerbositySpec(
117
+ name=TelemetryVerbosity.DETAILED.value,
118
+ enable_phase_sync=True,
119
+ enable_sigma=True,
120
+ enable_aggregate_si=True,
121
+ enable_advanced=False,
122
+ attach_coherence_hooks=True,
123
+ attach_diagnosis_hooks=False,
124
+ enable_nu_f=True,
125
+ )
126
+ _register_metrics_preset(_detailed_spec)
127
+ _register_metrics_preset(
128
+ _detailed_spec._replace(
129
+ name=TelemetryVerbosity.DEBUG.value,
130
+ enable_advanced=True,
131
+ attach_diagnosis_hooks=True,
132
+ enable_nu_f=True,
133
+ )
134
+ )
135
+
136
+
137
+ _METRICS_BASE_HISTORY_KEYS = ("C_steps", "stable_frac", "delta_Si", "B")
138
+ _METRICS_PHASE_HISTORY_KEYS = ("phase_sync", "kuramoto_R")
139
+ _METRICS_SIGMA_HISTORY_KEYS = (
140
+ GLYPH_LOAD_STABILIZERS_KEY,
141
+ "glyph_load_disr",
142
+ "sense_sigma_x",
143
+ "sense_sigma_y",
144
+ "sense_sigma_mag",
145
+ "sense_sigma_angle",
146
+ )
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
193
+
194
+
195
+ def _resolve_metrics_verbosity(cfg: Mapping[str, Any]) -> MetricsVerbositySpec:
196
+ """Return the preset matching ``cfg['verbosity']``."""
197
+
198
+ raw_value = cfg.get("verbosity", METRICS_VERBOSITY_DEFAULT)
199
+ key = str(raw_value).lower()
200
+ spec = _METRICS_VERBOSITY_PRESETS.get(key)
201
+ if spec is not None:
202
+ return spec
203
+ logger.warning(
204
+ "Unknown METRICS verbosity '%s'; falling back to '%s'",
205
+ raw_value,
206
+ METRICS_VERBOSITY_DEFAULT,
207
+ )
208
+ return _METRICS_VERBOSITY_PRESETS[METRICS_VERBOSITY_DEFAULT]
209
+
210
+
211
+ def _metrics_step(G: TNFRGraph, ctx: dict[str, Any] | None = None) -> None:
43
212
  """Update operational TNFR metrics per step."""
44
213
 
45
214
  del ctx
46
215
 
47
- cfg = get_param(G, "METRICS")
216
+ cfg = cast(Mapping[str, Any], get_param(G, "METRICS"))
48
217
  if not cfg.get("enabled", True):
49
218
  return
50
219
 
220
+ spec = _resolve_metrics_verbosity(cfg)
51
221
  hist = ensure_history(G)
222
+ if "glyph_load_estab" in hist:
223
+ raise ValueError(
224
+ "History payloads using 'glyph_load_estab' are no longer supported. "
225
+ "Rename the series to 'glyph_load_stabilizers' before loading the graph."
226
+ )
52
227
  metrics_sentinel_key = "_metrics_history_id"
53
228
  history_id = id(hist)
54
229
  if G.graph.get(metrics_sentinel_key) != history_id:
55
- for k in (
56
- "C_steps",
57
- "stable_frac",
58
- "phase_sync",
59
- "glyph_load_estab",
60
- "glyph_load_disr",
61
- "Si_mean",
62
- "Si_hi_frac",
63
- "Si_lo_frac",
64
- "delta_Si",
65
- "B",
66
- ):
67
- hist.setdefault(k, [])
230
+ for key in _METRICS_BASE_HISTORY_KEYS:
231
+ hist.setdefault(key, [])
232
+ if spec.enable_phase_sync:
233
+ for key in _METRICS_PHASE_HISTORY_KEYS:
234
+ hist.setdefault(key, [])
235
+ if spec.enable_sigma:
236
+ for key in _METRICS_SIGMA_HISTORY_KEYS:
237
+ hist.setdefault(key, [])
238
+ if spec.enable_aggregate_si:
239
+ for key in _METRICS_SI_HISTORY_KEYS:
240
+ hist.setdefault(key, [])
241
+ if spec.enable_nu_f:
242
+ for key in _METRICS_NU_F_HISTORY_KEYS:
243
+ hist.setdefault(key, [])
68
244
  G.graph[metrics_sentinel_key] = history_id
69
245
 
70
246
  dt = float(get_param(G, "DT"))
@@ -73,29 +249,69 @@ def _metrics_step(G, ctx: dict[str, Any] | None = None):
73
249
  t = float(G.graph.get("_t", 0.0))
74
250
 
75
251
  _update_coherence(G, hist)
76
- _track_stability(G, hist, dt, eps_dnfr, eps_depi)
252
+
253
+ raw_jobs = cfg.get("n_jobs")
254
+ metrics_jobs: int | None
77
255
  try:
78
- _update_phase_sync(G, hist)
79
- _update_sigma(G, hist)
80
- if hist.get("C_steps") and hist.get("stable_frac"):
81
- append_metric(
82
- hist,
83
- "iota",
84
- hist["C_steps"][-1] * hist["stable_frac"][-1],
85
- )
86
- except (KeyError, AttributeError, TypeError) as exc:
87
- logger.debug("observer update failed: %s", exc)
256
+ metrics_jobs = None if raw_jobs is None else int(raw_jobs)
257
+ except (TypeError, ValueError):
258
+ metrics_jobs = None
259
+ else:
260
+ if metrics_jobs <= 0:
261
+ metrics_jobs = None
262
+
263
+ _track_stability(
264
+ G,
265
+ hist,
266
+ dt,
267
+ eps_dnfr,
268
+ eps_depi,
269
+ n_jobs=metrics_jobs,
270
+ )
271
+ if spec.enable_phase_sync or spec.enable_sigma:
272
+ try:
273
+ if spec.enable_phase_sync:
274
+ _update_phase_sync(G, hist)
275
+ if spec.enable_sigma:
276
+ _update_sigma(G, hist)
277
+ except (KeyError, AttributeError, TypeError) as exc:
278
+ logger.debug("observer update failed: %s", exc)
279
+
280
+ if hist.get("C_steps") and hist.get("stable_frac"):
281
+ append_metric(
282
+ hist,
283
+ "iota",
284
+ hist["C_steps"][-1] * hist["stable_frac"][-1],
285
+ )
286
+
287
+ if spec.enable_aggregate_si:
288
+ _aggregate_si(G, hist, n_jobs=metrics_jobs)
289
+
290
+ _update_nu_f_snapshot(G, hist, record_history=spec.enable_nu_f)
291
+
292
+ if spec.enable_advanced:
293
+ _compute_advanced_metrics(
294
+ G,
295
+ cast(GlyphMetricsHistory, hist),
296
+ t,
297
+ dt,
298
+ cfg,
299
+ n_jobs=metrics_jobs,
300
+ )
88
301
 
89
- _aggregate_si(G, hist)
90
- _compute_advanced_metrics(G, hist, t, dt, cfg)
91
302
 
303
+ def register_metrics_callbacks(G: TNFRGraph) -> None:
304
+ """Attach canonical metrics callbacks according to graph configuration."""
92
305
 
93
- def register_metrics_callbacks(G) -> None:
306
+ cfg = cast(Mapping[str, Any], get_param(G, "METRICS"))
307
+ spec = _resolve_metrics_verbosity(cfg)
94
308
  callback_manager.register_callback(
95
309
  G,
96
310
  event=CallbackEvent.AFTER_STEP.value,
97
311
  func=_metrics_step,
98
312
  name="metrics_step",
99
313
  )
100
- register_coherence_callbacks(G)
101
- register_diagnosis_callbacks(G)
314
+ if spec.attach_coherence_hooks:
315
+ register_coherence_callbacks(G)
316
+ if spec.attach_diagnosis_hooks:
317
+ register_diagnosis_callbacks(G)
tnfr/metrics/core.pyi ADDED
@@ -0,0 +1,13 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ Tg_by_node: Any
8
+ Tg_global: Any
9
+ _metrics_step: Any
10
+ glyph_top: Any
11
+ glyphogram_series: Any
12
+ latency_series: Any
13
+ register_metrics_callbacks: Any