tnfr 4.5.1__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 (170) hide show
  1. tnfr/__init__.py +270 -90
  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 +631 -0
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +732 -0
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +381 -0
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +89 -0
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +199 -0
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +322 -0
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +34 -0
  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/init.py +36 -0
  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 +228 -0
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +158 -0
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.py +31 -0
  35. tnfr/constants/init.pyi +12 -0
  36. tnfr/constants/metric.py +102 -0
  37. tnfr/constants/metric.pyi +19 -0
  38. tnfr/constants_glyphs.py +16 -0
  39. tnfr/constants_glyphs.pyi +12 -0
  40. tnfr/dynamics/__init__.py +136 -0
  41. tnfr/dynamics/__init__.pyi +83 -0
  42. tnfr/dynamics/adaptation.py +201 -0
  43. tnfr/dynamics/aliases.py +22 -0
  44. tnfr/dynamics/coordination.py +343 -0
  45. tnfr/dynamics/dnfr.py +2315 -0
  46. tnfr/dynamics/dnfr.pyi +33 -0
  47. tnfr/dynamics/integrators.py +561 -0
  48. tnfr/dynamics/integrators.pyi +35 -0
  49. tnfr/dynamics/runtime.py +521 -0
  50. tnfr/dynamics/sampling.py +34 -0
  51. tnfr/dynamics/sampling.pyi +7 -0
  52. tnfr/dynamics/selectors.py +680 -0
  53. tnfr/execution.py +216 -0
  54. tnfr/execution.pyi +65 -0
  55. tnfr/flatten.py +283 -0
  56. tnfr/flatten.pyi +28 -0
  57. tnfr/gamma.py +320 -89
  58. tnfr/gamma.pyi +40 -0
  59. tnfr/glyph_history.py +337 -0
  60. tnfr/glyph_history.pyi +53 -0
  61. tnfr/grammar.py +23 -153
  62. tnfr/grammar.pyi +13 -0
  63. tnfr/helpers/__init__.py +151 -0
  64. tnfr/helpers/__init__.pyi +66 -0
  65. tnfr/helpers/numeric.py +88 -0
  66. tnfr/helpers/numeric.pyi +12 -0
  67. tnfr/immutable.py +214 -0
  68. tnfr/immutable.pyi +37 -0
  69. tnfr/initialization.py +199 -0
  70. tnfr/initialization.pyi +73 -0
  71. tnfr/io.py +311 -0
  72. tnfr/io.pyi +11 -0
  73. tnfr/locking.py +37 -0
  74. tnfr/locking.pyi +7 -0
  75. tnfr/metrics/__init__.py +41 -0
  76. tnfr/metrics/__init__.pyi +20 -0
  77. tnfr/metrics/coherence.py +1469 -0
  78. tnfr/metrics/common.py +149 -0
  79. tnfr/metrics/common.pyi +15 -0
  80. tnfr/metrics/core.py +259 -0
  81. tnfr/metrics/core.pyi +13 -0
  82. tnfr/metrics/diagnosis.py +840 -0
  83. tnfr/metrics/diagnosis.pyi +89 -0
  84. tnfr/metrics/export.py +151 -0
  85. tnfr/metrics/glyph_timing.py +369 -0
  86. tnfr/metrics/reporting.py +152 -0
  87. tnfr/metrics/reporting.pyi +12 -0
  88. tnfr/metrics/sense_index.py +294 -0
  89. tnfr/metrics/sense_index.pyi +9 -0
  90. tnfr/metrics/trig.py +216 -0
  91. tnfr/metrics/trig.pyi +12 -0
  92. tnfr/metrics/trig_cache.py +105 -0
  93. tnfr/metrics/trig_cache.pyi +10 -0
  94. tnfr/node.py +255 -177
  95. tnfr/node.pyi +161 -0
  96. tnfr/observers.py +154 -150
  97. tnfr/observers.pyi +46 -0
  98. tnfr/ontosim.py +135 -134
  99. tnfr/ontosim.pyi +33 -0
  100. tnfr/operators/__init__.py +452 -0
  101. tnfr/operators/__init__.pyi +31 -0
  102. tnfr/operators/definitions.py +181 -0
  103. tnfr/operators/definitions.pyi +92 -0
  104. tnfr/operators/jitter.py +266 -0
  105. tnfr/operators/jitter.pyi +11 -0
  106. tnfr/operators/registry.py +80 -0
  107. tnfr/operators/registry.pyi +15 -0
  108. tnfr/operators/remesh.py +569 -0
  109. tnfr/presets.py +10 -23
  110. tnfr/presets.pyi +7 -0
  111. tnfr/py.typed +0 -0
  112. tnfr/rng.py +440 -0
  113. tnfr/rng.pyi +14 -0
  114. tnfr/selector.py +217 -0
  115. tnfr/selector.pyi +19 -0
  116. tnfr/sense.py +307 -142
  117. tnfr/sense.pyi +30 -0
  118. tnfr/structural.py +69 -164
  119. tnfr/structural.pyi +46 -0
  120. tnfr/telemetry/__init__.py +13 -0
  121. tnfr/telemetry/verbosity.py +37 -0
  122. tnfr/tokens.py +61 -0
  123. tnfr/tokens.pyi +41 -0
  124. tnfr/trace.py +520 -95
  125. tnfr/trace.pyi +68 -0
  126. tnfr/types.py +382 -17
  127. tnfr/types.pyi +145 -0
  128. tnfr/utils/__init__.py +158 -0
  129. tnfr/utils/__init__.pyi +133 -0
  130. tnfr/utils/cache.py +755 -0
  131. tnfr/utils/cache.pyi +156 -0
  132. tnfr/utils/data.py +267 -0
  133. tnfr/utils/data.pyi +73 -0
  134. tnfr/utils/graph.py +87 -0
  135. tnfr/utils/graph.pyi +10 -0
  136. tnfr/utils/init.py +746 -0
  137. tnfr/utils/init.pyi +85 -0
  138. tnfr/utils/io.py +157 -0
  139. tnfr/utils/io.pyi +10 -0
  140. tnfr/utils/validators.py +130 -0
  141. tnfr/utils/validators.pyi +19 -0
  142. tnfr/validation/__init__.py +25 -0
  143. tnfr/validation/__init__.pyi +17 -0
  144. tnfr/validation/compatibility.py +59 -0
  145. tnfr/validation/compatibility.pyi +8 -0
  146. tnfr/validation/grammar.py +149 -0
  147. tnfr/validation/grammar.pyi +11 -0
  148. tnfr/validation/rules.py +194 -0
  149. tnfr/validation/rules.pyi +18 -0
  150. tnfr/validation/syntax.py +151 -0
  151. tnfr/validation/syntax.pyi +7 -0
  152. tnfr-6.0.0.dist-info/METADATA +135 -0
  153. tnfr-6.0.0.dist-info/RECORD +157 -0
  154. tnfr/cli.py +0 -322
  155. tnfr/config.py +0 -41
  156. tnfr/constants.py +0 -277
  157. tnfr/dynamics.py +0 -814
  158. tnfr/helpers.py +0 -264
  159. tnfr/main.py +0 -47
  160. tnfr/metrics.py +0 -597
  161. tnfr/operators.py +0 -525
  162. tnfr/program.py +0 -176
  163. tnfr/scenarios.py +0 -34
  164. tnfr/validators.py +0 -38
  165. tnfr-4.5.1.dist-info/METADATA +0 -221
  166. tnfr-4.5.1.dist-info/RECORD +0 -28
  167. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  168. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  169. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/trace.py CHANGED
@@ -1,134 +1,559 @@
1
+ """Trace logging.
2
+
3
+ Field helpers avoid unnecessary copying by reusing dictionaries stored on
4
+ the graph whenever possible. Callers are expected to treat returned
5
+ structures as immutable snapshots.
6
+ """
7
+
1
8
  from __future__ import annotations
2
- from typing import Any, Dict, List, Optional
3
- from collections import Counter
4
9
 
5
- from .constants import DEFAULTS
6
- from .helpers import register_callback, ensure_history, last_glifo
10
+ import warnings
11
+
12
+ from typing import Any, Callable, Protocol, NamedTuple, TypedDict, cast
13
+ from collections.abc import Iterable, Mapping
14
+ from types import MappingProxyType
15
+
16
+ from .constants import TRACE
17
+ from .glyph_history import ensure_history, count_glyphs, append_metric
18
+ from .utils import cached_import, get_graph_mapping, is_non_string_sequence
19
+ from .metrics.sense_index import _normalise_si_sensitivity_mapping
20
+ from .telemetry.verbosity import (
21
+ TelemetryVerbosity,
22
+ TELEMETRY_VERBOSITY_DEFAULT,
23
+ )
24
+ from .types import (
25
+ SigmaVector,
26
+ TNFRGraph,
27
+ TraceCallback,
28
+ TraceFieldFn,
29
+ TraceFieldMap,
30
+ TraceFieldRegistry,
31
+ )
32
+
33
+
34
+ class _KuramotoFn(Protocol):
35
+ def __call__(self, G: TNFRGraph) -> tuple[float, float]:
36
+ ...
37
+
38
+
39
+ class _SigmaVectorFn(Protocol):
40
+ def __call__(
41
+ self, G: TNFRGraph, weight_mode: str | None = None
42
+ ) -> SigmaVector:
43
+ ...
44
+
45
+
46
+ class CallbackSpec(NamedTuple):
47
+ """Specification for a registered callback."""
48
+
49
+ name: str | None
50
+ func: Callable[..., Any]
51
+
52
+
53
+ class TraceMetadata(TypedDict, total=False):
54
+ """Metadata captured by trace field functions."""
55
+
56
+ gamma: Mapping[str, Any]
57
+ grammar: Mapping[str, Any]
58
+ selector: str | None
59
+ dnfr_weights: Mapping[str, Any]
60
+ si_weights: Mapping[str, Any]
61
+ si_sensitivity: Mapping[str, Any]
62
+ callbacks: Mapping[str, list[str] | None]
63
+ thol_open_nodes: int
64
+ kuramoto: Mapping[str, float]
65
+ sigma: Mapping[str, float]
66
+ glyphs: Mapping[str, int]
67
+
68
+
69
+ class TraceSnapshot(TraceMetadata, total=False):
70
+ """Trace snapshot stored in the history."""
71
+
72
+ t: float
73
+ phase: str
74
+
75
+
76
+ class TraceFieldSpec(NamedTuple):
77
+ """Declarative specification for a trace field producer."""
78
+
79
+ name: str
80
+ phase: str
81
+ producer: TraceFieldFn
82
+ tiers: tuple[TelemetryVerbosity, ...]
83
+
84
+
85
+ TRACE_VERBOSITY_DEFAULT = TELEMETRY_VERBOSITY_DEFAULT
86
+ TRACE_VERBOSITY_PRESETS: dict[str, tuple[str, ...]] = {}
87
+ _TRACE_CAPTURE_ALIASES: Mapping[str, str] = MappingProxyType(
88
+ {
89
+ "glyphs": "glyph_counts",
90
+ }
91
+ )
92
+
93
+
94
+ def _canonical_capture_name(name: str) -> str:
95
+ """Return the canonical capture field name for ``name``."""
96
+
97
+ stripped = name.strip()
98
+ alias = _TRACE_CAPTURE_ALIASES.get(stripped)
99
+ if alias is not None:
100
+ return alias
101
+
102
+ lowered = stripped.lower()
103
+ alias = _TRACE_CAPTURE_ALIASES.get(lowered)
104
+ if alias is not None:
105
+ return alias
106
+
107
+ return stripped
108
+
109
+
110
+ def _normalise_capture_spec(raw: Any) -> set[str]:
111
+ """Coerce custom capture payloads to a ``set`` of field names."""
7
112
 
8
- try:
9
- from .gamma import kuramoto_R_psi
10
- except Exception: # pragma: no cover
11
- def kuramoto_R_psi(G):
12
- return 0.0, 0.0
113
+ if raw is None:
114
+ return set()
115
+ if isinstance(raw, Mapping):
116
+ return {_canonical_capture_name(str(name)) for name in raw.keys()}
117
+ if isinstance(raw, str):
118
+ return {_canonical_capture_name(raw)}
119
+ if isinstance(raw, Iterable):
120
+ return {_canonical_capture_name(str(name)) for name in raw}
121
+ return {_canonical_capture_name(str(raw))}
13
122
 
14
- try:
15
- from .sense import sigma_vector_global
16
- except Exception: # pragma: no cover
17
- def sigma_vector_global(G, *args, **kwargs):
18
- return {"x": 1.0, "y": 0.0, "mag": 1.0, "angle": 0.0, "n": 0}
123
+
124
+ def _resolve_trace_capture(cfg: Mapping[str, Any]) -> set[str]:
125
+ """Return the capture set declared by ``cfg`` respecting verbosity."""
126
+
127
+ if "capture" in cfg:
128
+ return _normalise_capture_spec(cfg.get("capture"))
129
+
130
+ raw_verbosity = cfg.get("verbosity", TRACE_VERBOSITY_DEFAULT)
131
+ verbosity = str(raw_verbosity).lower()
132
+ fields = TRACE_VERBOSITY_PRESETS.get(verbosity)
133
+ if fields is None:
134
+ warnings.warn(
135
+ (
136
+ "Unknown TRACE verbosity %r; falling back to %s"
137
+ % (raw_verbosity, TRACE_VERBOSITY_DEFAULT)
138
+ ),
139
+ UserWarning,
140
+ stacklevel=3,
141
+ )
142
+ fields = TRACE_VERBOSITY_PRESETS[TRACE_VERBOSITY_DEFAULT]
143
+ return set(fields)
144
+
145
+
146
+ def _kuramoto_fallback(G: TNFRGraph) -> tuple[float, float]:
147
+ return 0.0, 0.0
148
+
149
+
150
+ kuramoto_R_psi: _KuramotoFn = cast(
151
+ _KuramotoFn,
152
+ cached_import("tnfr.gamma", "kuramoto_R_psi", fallback=_kuramoto_fallback),
153
+ )
154
+
155
+
156
+ def _sigma_fallback(
157
+ G: TNFRGraph, _weight_mode: str | None = None
158
+ ) -> SigmaVector:
159
+ """Return a null sigma vector regardless of ``_weight_mode``."""
160
+
161
+ return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0, "n": 0}
162
+
163
+
164
+ # Public exports for this module
165
+ __all__ = (
166
+ "CallbackSpec",
167
+ "TraceFieldSpec",
168
+ "TraceMetadata",
169
+ "TraceSnapshot",
170
+ "register_trace",
171
+ "register_trace_field",
172
+ "_callback_names",
173
+ "gamma_field",
174
+ "grammar_field",
175
+ )
19
176
 
20
177
  # -------------------------
21
- # Defaults
178
+ # Helpers
22
179
  # -------------------------
23
- DEFAULTS.setdefault("TRACE", {
24
- "enabled": True,
25
- "capture": ["gamma", "grammar", "selector", "dnfr_weights", "si_weights", "callbacks", "thol_state", "sigma", "kuramoto", "glifo_counts"],
26
- "history_key": "trace_meta",
27
- })
180
+
181
+
182
+ def _trace_setup(
183
+ G: TNFRGraph,
184
+ ) -> tuple[
185
+ Mapping[str, Any] | None,
186
+ set[str],
187
+ dict[str, Any] | None,
188
+ str | None,
189
+ ]:
190
+ """Common configuration for trace snapshots.
191
+
192
+ Returns the active configuration, capture set, history and key under
193
+ which metadata will be stored. If tracing is disabled returns
194
+ ``(None, set(), None, None)``.
195
+ """
196
+
197
+ cfg_raw = G.graph.get("TRACE", TRACE)
198
+ cfg = cfg_raw if isinstance(cfg_raw, Mapping) else TRACE
199
+ if not cfg.get("enabled", True):
200
+ return None, set(), None, None
201
+
202
+ capture = _resolve_trace_capture(cfg)
203
+ hist = ensure_history(G)
204
+ key = cast(str | None, cfg.get("history_key", "trace_meta"))
205
+ return cfg, capture, hist, key
206
+
207
+
208
+ def _callback_names(
209
+ callbacks: Mapping[str, CallbackSpec] | Iterable[CallbackSpec],
210
+ ) -> list[str]:
211
+ """Return callback names from ``callbacks``."""
212
+ if isinstance(callbacks, Mapping):
213
+ callbacks = callbacks.values()
214
+ return [
215
+ cb.name
216
+ if cb.name is not None
217
+ else str(getattr(cb.func, "__name__", "fn"))
218
+ for cb in callbacks
219
+ ]
220
+
221
+
222
+ EMPTY_MAPPING: Mapping[str, Any] = MappingProxyType({})
223
+
224
+
225
+ def mapping_field(G: TNFRGraph, graph_key: str, out_key: str) -> TraceMetadata:
226
+ """Helper to copy mappings from ``G.graph`` into trace output."""
227
+ mapping = get_graph_mapping(
228
+ G, graph_key, f"G.graph[{graph_key!r}] is not a mapping; ignoring"
229
+ )
230
+ if mapping is None:
231
+ return {}
232
+ return {out_key: mapping}
233
+
28
234
 
29
235
  # -------------------------
30
- # Helpers
236
+ # Builders
31
237
  # -------------------------
32
238
 
239
+
240
+ def _new_trace_meta(
241
+ G: TNFRGraph, phase: str
242
+ ) -> tuple[TraceSnapshot, set[str], dict[str, Any] | None, str | None] | None:
243
+ """Initialise trace metadata for a ``phase``.
244
+
245
+ Wraps :func:`_trace_setup` and creates the base structure with timestamp
246
+ and current phase. Returns ``None`` if tracing is disabled.
247
+ """
248
+
249
+ cfg, capture, hist, key = _trace_setup(G)
250
+ if not cfg:
251
+ return None
252
+
253
+ meta: TraceSnapshot = {"t": float(G.graph.get("_t", 0.0)), "phase": phase}
254
+ return meta, capture, hist, key
255
+
256
+
33
257
  # -------------------------
34
258
  # Snapshots
35
259
  # -------------------------
36
260
 
37
- def _trace_before(G, *args, **kwargs):
38
- if not G.graph.get("TRACE", DEFAULTS["TRACE"]).get("enabled", True):
261
+
262
+ def _trace_capture(
263
+ G: TNFRGraph, phase: str, fields: TraceFieldMap
264
+ ) -> None:
265
+ """Capture ``fields`` for ``phase`` and store the snapshot.
266
+
267
+ A :class:`TraceSnapshot` is appended to the configured history when
268
+ tracing is active. If there is no active history or storage key the
269
+ capture is silently ignored.
270
+ """
271
+
272
+ res = _new_trace_meta(G, phase)
273
+ if not res:
39
274
  return
40
- cfg = G.graph.get("TRACE", DEFAULTS["TRACE"])
41
- capture: List[str] = list(cfg.get("capture", []))
42
- hist = ensure_history(G)
43
- key = cfg.get("history_key", "trace_meta")
44
275
 
45
- meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "before"}
276
+ meta, capture, hist, key = res
277
+ if not capture:
278
+ return
279
+ for name, getter in fields.items():
280
+ if name in capture:
281
+ meta.update(getter(G))
282
+ if hist is None or key is None:
283
+ return
284
+ append_metric(hist, key, meta)
285
+
46
286
 
47
- if "gamma" in capture:
48
- meta["gamma"] = dict(G.graph.get("GAMMA", {}))
287
+ # -------------------------
288
+ # Registry
289
+ # -------------------------
49
290
 
50
- if "grammar" in capture:
51
- meta["grammar"] = dict(G.graph.get("GRAMMAR_CANON", {}))
52
291
 
53
- if "selector" in capture:
54
- sel = G.graph.get("glyph_selector")
55
- meta["selector"] = getattr(sel, "__name__", str(sel)) if sel else None
292
+ TRACE_FIELDS: TraceFieldRegistry = {}
56
293
 
57
- if "dnfr_weights" in capture:
58
- mix = G.graph.get("DNFR_WEIGHTS")
59
- if isinstance(mix, dict):
60
- meta["dnfr_weights"] = dict(mix)
61
294
 
62
- if "si_weights" in capture:
63
- meta["si_weights"] = dict(G.graph.get("_Si_weights", {}))
64
- meta["si_sensitivity"] = dict(G.graph.get("_Si_sensitivity", {}))
295
+ def register_trace_field(
296
+ phase: str, name: str, func: TraceFieldFn
297
+ ) -> None:
298
+ """Register ``func`` to populate trace field ``name`` during ``phase``."""
65
299
 
66
- if "callbacks" in capture:
67
- # si el motor guarda los callbacks, exponer nombres por fase
68
- cb = G.graph.get("_callbacks")
69
- if isinstance(cb, dict):
70
- out = {k: [getattr(f, "__name__", "fn") for (_, f, *_rest) in v] if isinstance(v, list) else None for k, v in cb.items()}
71
- meta["callbacks"] = out
300
+ TRACE_FIELDS.setdefault(phase, {})[name] = func
72
301
 
73
- if "thol_state" in capture:
74
- # cuántos nodos tienen bloque T’HOL abierto
75
- th_open = 0
76
- for n in G.nodes():
77
- st = G.nodes[n].get("_GRAM", {})
78
- if st.get("thol_open", False):
79
- th_open += 1
80
- meta["thol_open_nodes"] = th_open
81
302
 
82
- hist.setdefault(key, []).append(meta)
303
+ def gamma_field(G: TNFRGraph) -> TraceMetadata:
304
+ return mapping_field(G, "GAMMA", "gamma")
83
305
 
84
306
 
85
- def _trace_after(G, *args, **kwargs):
86
- if not G.graph.get("TRACE", DEFAULTS["TRACE"]).get("enabled", True):
87
- return
88
- cfg = G.graph.get("TRACE", DEFAULTS["TRACE"])
89
- capture: List[str] = list(cfg.get("capture", []))
90
- hist = ensure_history(G)
91
- key = cfg.get("history_key", "trace_meta")
307
+ def grammar_field(G: TNFRGraph) -> TraceMetadata:
308
+ return mapping_field(G, "GRAMMAR_CANON", "grammar")
309
+
310
+
311
+ def dnfr_weights_field(G: TNFRGraph) -> TraceMetadata:
312
+ return mapping_field(G, "DNFR_WEIGHTS", "dnfr_weights")
313
+
314
+
315
+ def selector_field(G: TNFRGraph) -> TraceMetadata:
316
+ sel = G.graph.get("glyph_selector")
317
+ selector_name = getattr(sel, "__name__", str(sel)) if sel else None
318
+ return {"selector": selector_name}
319
+
320
+
321
+ def _si_weights_field(G: TNFRGraph) -> TraceMetadata:
322
+ weights = mapping_field(G, "_Si_weights", "si_weights")
323
+ if weights:
324
+ return weights
325
+ return {"si_weights": EMPTY_MAPPING}
326
+
327
+
328
+ def _si_sensitivity_field(G: TNFRGraph) -> TraceMetadata:
329
+ mapping = get_graph_mapping(
330
+ G,
331
+ "_Si_sensitivity",
332
+ "G.graph['_Si_sensitivity'] is not a mapping; ignoring",
333
+ )
334
+ if mapping is None:
335
+ return {"si_sensitivity": EMPTY_MAPPING}
336
+
337
+ normalised = _normalise_si_sensitivity_mapping(mapping, warn=True)
92
338
 
93
- meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "after"}
339
+ if normalised != mapping:
340
+ G.graph["_Si_sensitivity"] = normalised
94
341
 
95
- if "kuramoto" in capture:
96
- R, psi = kuramoto_R_psi(G)
97
- meta["kuramoto"] = {"R": float(R), "psi": float(psi)}
342
+ return {"si_sensitivity": MappingProxyType(normalised)}
98
343
 
99
- if "sigma" in capture:
100
- sv = sigma_vector_global(G)
101
- meta["sigma"] = {"x": float(sv.get("x", 1.0)), "y": float(sv.get("y", 0.0)), "mag": float(sv.get("mag", 1.0)), "angle": float(sv.get("angle", 0.0))}
102
344
 
103
- if "glifo_counts" in capture:
104
- cnt = Counter()
105
- for n in G.nodes():
106
- g = last_glifo(G.nodes[n])
107
- if g:
108
- cnt[g] += 1
109
- meta["glifos"] = dict(cnt)
345
+ def si_weights_field(G: TNFRGraph) -> TraceMetadata:
346
+ """Return sense-plane weights and sensitivity."""
110
347
 
111
- hist.setdefault(key, []).append(meta)
348
+ weights = _si_weights_field(G)
349
+ sensitivity = _si_sensitivity_field(G)
350
+ return {**weights, **sensitivity}
351
+
352
+
353
+ def callbacks_field(G: TNFRGraph) -> TraceMetadata:
354
+ cb = G.graph.get("callbacks")
355
+ if not isinstance(cb, Mapping):
356
+ return {}
357
+ out: dict[str, list[str] | None] = {}
358
+ for phase, cb_map in cb.items():
359
+ if isinstance(cb_map, Mapping) or is_non_string_sequence(cb_map):
360
+ out[phase] = _callback_names(cb_map)
361
+ else:
362
+ out[phase] = None
363
+ return {"callbacks": out}
364
+
365
+
366
+ def thol_state_field(G: TNFRGraph) -> TraceMetadata:
367
+ th_open = 0
368
+ for _, nd in G.nodes(data=True):
369
+ st = nd.get("_GRAM", {})
370
+ if st.get("thol_open", False):
371
+ th_open += 1
372
+ return {"thol_open_nodes": th_open}
373
+
374
+
375
+ def kuramoto_field(G: TNFRGraph) -> TraceMetadata:
376
+ R, psi = kuramoto_R_psi(G)
377
+ return {"kuramoto": {"R": float(R), "psi": float(psi)}}
378
+
379
+
380
+ def sigma_field(G: TNFRGraph) -> TraceMetadata:
381
+ sigma_vector_from_graph: _SigmaVectorFn = cast(
382
+ _SigmaVectorFn,
383
+ cached_import(
384
+ "tnfr.sense",
385
+ "sigma_vector_from_graph",
386
+ fallback=_sigma_fallback,
387
+ ),
388
+ )
389
+ sv = sigma_vector_from_graph(G)
390
+ return {
391
+ "sigma": {
392
+ "x": float(sv.get("x", 0.0)),
393
+ "y": float(sv.get("y", 0.0)),
394
+ "mag": float(sv.get("mag", 0.0)),
395
+ "angle": float(sv.get("angle", 0.0)),
396
+ }
397
+ }
398
+
399
+
400
+ def glyph_counts_field(G: TNFRGraph) -> TraceMetadata:
401
+ """Return glyph count snapshot.
402
+
403
+ ``count_glyphs`` already produces a fresh mapping so no additional copy
404
+ is taken. Treat the returned mapping as read-only.
405
+ """
406
+
407
+ cnt = count_glyphs(G, window=1)
408
+ return {"glyphs": cnt}
409
+
410
+
411
+ TRACE_FIELD_SPECS: tuple[TraceFieldSpec, ...] = (
412
+ TraceFieldSpec(
413
+ name="gamma",
414
+ phase="before",
415
+ producer=gamma_field,
416
+ tiers=(
417
+ TelemetryVerbosity.BASIC,
418
+ TelemetryVerbosity.DETAILED,
419
+ TelemetryVerbosity.DEBUG,
420
+ ),
421
+ ),
422
+ TraceFieldSpec(
423
+ name="grammar",
424
+ phase="before",
425
+ producer=grammar_field,
426
+ tiers=(
427
+ TelemetryVerbosity.BASIC,
428
+ TelemetryVerbosity.DETAILED,
429
+ TelemetryVerbosity.DEBUG,
430
+ ),
431
+ ),
432
+ TraceFieldSpec(
433
+ name="selector",
434
+ phase="before",
435
+ producer=selector_field,
436
+ tiers=(
437
+ TelemetryVerbosity.BASIC,
438
+ TelemetryVerbosity.DETAILED,
439
+ TelemetryVerbosity.DEBUG,
440
+ ),
441
+ ),
442
+ TraceFieldSpec(
443
+ name="dnfr_weights",
444
+ phase="before",
445
+ producer=dnfr_weights_field,
446
+ tiers=(
447
+ TelemetryVerbosity.BASIC,
448
+ TelemetryVerbosity.DETAILED,
449
+ TelemetryVerbosity.DEBUG,
450
+ ),
451
+ ),
452
+ TraceFieldSpec(
453
+ name="si_weights",
454
+ phase="before",
455
+ producer=si_weights_field,
456
+ tiers=(
457
+ TelemetryVerbosity.BASIC,
458
+ TelemetryVerbosity.DETAILED,
459
+ TelemetryVerbosity.DEBUG,
460
+ ),
461
+ ),
462
+ TraceFieldSpec(
463
+ name="callbacks",
464
+ phase="before",
465
+ producer=callbacks_field,
466
+ tiers=(
467
+ TelemetryVerbosity.BASIC,
468
+ TelemetryVerbosity.DETAILED,
469
+ TelemetryVerbosity.DEBUG,
470
+ ),
471
+ ),
472
+ TraceFieldSpec(
473
+ name="thol_open_nodes",
474
+ phase="before",
475
+ producer=thol_state_field,
476
+ tiers=(
477
+ TelemetryVerbosity.BASIC,
478
+ TelemetryVerbosity.DETAILED,
479
+ TelemetryVerbosity.DEBUG,
480
+ ),
481
+ ),
482
+ TraceFieldSpec(
483
+ name="kuramoto",
484
+ phase="after",
485
+ producer=kuramoto_field,
486
+ tiers=(TelemetryVerbosity.DETAILED, TelemetryVerbosity.DEBUG),
487
+ ),
488
+ TraceFieldSpec(
489
+ name="sigma",
490
+ phase="after",
491
+ producer=sigma_field,
492
+ tiers=(TelemetryVerbosity.DETAILED, TelemetryVerbosity.DEBUG),
493
+ ),
494
+ TraceFieldSpec(
495
+ name="glyph_counts",
496
+ phase="after",
497
+ producer=glyph_counts_field,
498
+ tiers=(TelemetryVerbosity.DEBUG,),
499
+ ),
500
+ )
501
+
502
+ TRACE_VERBOSITY_PRESETS = {
503
+ level.value: tuple(
504
+ spec.name for spec in TRACE_FIELD_SPECS if level in spec.tiers
505
+ )
506
+ for level in TelemetryVerbosity
507
+ }
508
+
509
+ for spec in TRACE_FIELD_SPECS:
510
+ register_trace_field(spec.phase, spec.name, spec.producer)
112
511
 
113
512
 
114
513
  # -------------------------
115
514
  # API
116
515
  # -------------------------
117
516
 
118
- def register_trace(G) -> None:
119
- """Activa snapshots before/after step y vuelca metadatos operativos en history.
120
-
121
- Guarda en G.graph['history'][TRACE.history_key] una lista de entradas {'phase': 'before'|'after', ...} con:
122
- - gamma: especificación activa de Γi(R)
123
- - grammar: configuración de gramática canónica
124
- - selector: nombre del selector glífico
125
- - dnfr_weights: mezcla ΔNFR declarada en el motor
126
- - si_weights: pesos α/β/γ y sensibilidad de Si
127
- - callbacks: callbacks registrados por fase (si están en G.graph['_callbacks'])
128
- - thol_open_nodes: cuántos nodos tienen bloque T’HOL abierto
129
- - kuramoto: (R, ψ) de la red
130
- - sigma: vector global del plano del sentido
131
- - glifos: conteos por glifo tras el paso
517
+
518
+ def register_trace(G: TNFRGraph) -> None:
519
+ """Enable before/after-step snapshots and dump operational metadata
520
+ to history.
521
+
522
+ Trace snapshots are stored as :class:`TraceSnapshot` entries in
523
+ ``G.graph['history'][TRACE.history_key]`` with:
524
+ - gamma: active Γi(R) specification
525
+ - grammar: canonical grammar configuration
526
+ - selector: glyph selector name
527
+ - dnfr_weights: ΔNFR mix declared in the engine
528
+ - si_weights: α/β/γ weights and Si sensitivity
529
+ - callbacks: callbacks registered per phase (if in
530
+ ``G.graph['callbacks']``)
531
+ - thol_open_nodes: how many nodes have an open THOL block
532
+ - kuramoto: network ``(R, ψ)``
533
+ - sigma: global sense-plane vector
534
+ - glyphs: glyph counts after the step
535
+
536
+ Field helpers reuse graph dictionaries and expect them to be treated as
537
+ immutable snapshots by consumers.
132
538
  """
133
- register_callback(G, when="before_step", func=_trace_before, name="trace_before")
134
- register_callback(G, when="after_step", func=_trace_after, name="trace_after")
539
+ if G.graph.get("_trace_registered"):
540
+ return
541
+
542
+ from .callback_utils import callback_manager
543
+
544
+ for phase in TRACE_FIELDS.keys():
545
+ event = f"{phase}_step"
546
+
547
+ def _make_cb(ph: str) -> TraceCallback:
548
+ def _cb(graph: TNFRGraph, ctx: dict[str, Any]) -> None:
549
+ del ctx
550
+
551
+ _trace_capture(graph, ph, TRACE_FIELDS.get(ph, {}))
552
+
553
+ return _cb
554
+
555
+ callback_manager.register_callback(
556
+ G, event=event, func=_make_cb(phase), name=f"trace_{phase}"
557
+ )
558
+
559
+ G.graph["_trace_registered"] = True