tnfr 4.5.2__py3-none-any.whl → 6.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. tnfr/__init__.py +228 -49
  2. tnfr/__init__.pyi +40 -0
  3. tnfr/_compat.py +11 -0
  4. tnfr/_version.py +7 -0
  5. tnfr/_version.pyi +7 -0
  6. tnfr/alias.py +106 -21
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +666 -512
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +2 -9
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +21 -7
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +42 -20
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +54 -20
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +0 -2
  19. tnfr/cli/utils.pyi +8 -0
  20. tnfr/config/__init__.py +12 -0
  21. tnfr/config/__init__.pyi +8 -0
  22. tnfr/config/constants.py +104 -0
  23. tnfr/config/constants.pyi +12 -0
  24. tnfr/{config.py → config/init.py} +11 -7
  25. tnfr/config/init.pyi +8 -0
  26. tnfr/config/operator_names.py +106 -0
  27. tnfr/config/operator_names.pyi +28 -0
  28. tnfr/config/presets.py +104 -0
  29. tnfr/config/presets.pyi +7 -0
  30. tnfr/constants/__init__.py +78 -24
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +1 -2
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.pyi +12 -0
  35. tnfr/constants/metric.py +4 -12
  36. tnfr/constants/metric.pyi +19 -0
  37. tnfr/constants_glyphs.py +9 -91
  38. tnfr/constants_glyphs.pyi +12 -0
  39. tnfr/dynamics/__init__.py +112 -634
  40. tnfr/dynamics/__init__.pyi +83 -0
  41. tnfr/dynamics/adaptation.py +201 -0
  42. tnfr/dynamics/aliases.py +22 -0
  43. tnfr/dynamics/coordination.py +343 -0
  44. tnfr/dynamics/dnfr.py +1936 -354
  45. tnfr/dynamics/dnfr.pyi +33 -0
  46. tnfr/dynamics/integrators.py +369 -75
  47. tnfr/dynamics/integrators.pyi +35 -0
  48. tnfr/dynamics/runtime.py +521 -0
  49. tnfr/dynamics/sampling.py +8 -5
  50. tnfr/dynamics/sampling.pyi +7 -0
  51. tnfr/dynamics/selectors.py +680 -0
  52. tnfr/execution.py +56 -41
  53. tnfr/execution.pyi +65 -0
  54. tnfr/flatten.py +7 -7
  55. tnfr/flatten.pyi +28 -0
  56. tnfr/gamma.py +54 -37
  57. tnfr/gamma.pyi +40 -0
  58. tnfr/glyph_history.py +85 -38
  59. tnfr/glyph_history.pyi +53 -0
  60. tnfr/grammar.py +19 -338
  61. tnfr/grammar.pyi +13 -0
  62. tnfr/helpers/__init__.py +110 -30
  63. tnfr/helpers/__init__.pyi +66 -0
  64. tnfr/helpers/numeric.py +1 -0
  65. tnfr/helpers/numeric.pyi +12 -0
  66. tnfr/immutable.py +55 -19
  67. tnfr/immutable.pyi +37 -0
  68. tnfr/initialization.py +12 -10
  69. tnfr/initialization.pyi +73 -0
  70. tnfr/io.py +99 -34
  71. tnfr/io.pyi +11 -0
  72. tnfr/locking.pyi +7 -0
  73. tnfr/metrics/__init__.pyi +20 -0
  74. tnfr/metrics/coherence.py +934 -294
  75. tnfr/metrics/common.py +1 -3
  76. tnfr/metrics/common.pyi +15 -0
  77. tnfr/metrics/core.py +192 -34
  78. tnfr/metrics/core.pyi +13 -0
  79. tnfr/metrics/diagnosis.py +707 -101
  80. tnfr/metrics/diagnosis.pyi +89 -0
  81. tnfr/metrics/export.py +27 -13
  82. tnfr/metrics/glyph_timing.py +218 -38
  83. tnfr/metrics/reporting.py +22 -18
  84. tnfr/metrics/reporting.pyi +12 -0
  85. tnfr/metrics/sense_index.py +199 -25
  86. tnfr/metrics/sense_index.pyi +9 -0
  87. tnfr/metrics/trig.py +53 -18
  88. tnfr/metrics/trig.pyi +12 -0
  89. tnfr/metrics/trig_cache.py +3 -7
  90. tnfr/metrics/trig_cache.pyi +10 -0
  91. tnfr/node.py +148 -125
  92. tnfr/node.pyi +161 -0
  93. tnfr/observers.py +44 -30
  94. tnfr/observers.pyi +46 -0
  95. tnfr/ontosim.py +14 -13
  96. tnfr/ontosim.pyi +33 -0
  97. tnfr/operators/__init__.py +84 -52
  98. tnfr/operators/__init__.pyi +31 -0
  99. tnfr/operators/definitions.py +181 -0
  100. tnfr/operators/definitions.pyi +92 -0
  101. tnfr/operators/jitter.py +86 -23
  102. tnfr/operators/jitter.pyi +11 -0
  103. tnfr/operators/registry.py +80 -0
  104. tnfr/operators/registry.pyi +15 -0
  105. tnfr/operators/remesh.py +141 -57
  106. tnfr/presets.py +9 -54
  107. tnfr/presets.pyi +7 -0
  108. tnfr/py.typed +0 -0
  109. tnfr/rng.py +259 -73
  110. tnfr/rng.pyi +14 -0
  111. tnfr/selector.py +24 -17
  112. tnfr/selector.pyi +19 -0
  113. tnfr/sense.py +55 -43
  114. tnfr/sense.pyi +30 -0
  115. tnfr/structural.py +44 -267
  116. tnfr/structural.pyi +46 -0
  117. tnfr/telemetry/__init__.py +13 -0
  118. tnfr/telemetry/verbosity.py +37 -0
  119. tnfr/tokens.py +3 -2
  120. tnfr/tokens.pyi +41 -0
  121. tnfr/trace.py +272 -82
  122. tnfr/trace.pyi +68 -0
  123. tnfr/types.py +345 -6
  124. tnfr/types.pyi +145 -0
  125. tnfr/utils/__init__.py +158 -0
  126. tnfr/utils/__init__.pyi +133 -0
  127. tnfr/utils/cache.py +755 -0
  128. tnfr/utils/cache.pyi +156 -0
  129. tnfr/{collections_utils.py → utils/data.py} +57 -90
  130. tnfr/utils/data.pyi +73 -0
  131. tnfr/utils/graph.py +87 -0
  132. tnfr/utils/graph.pyi +10 -0
  133. tnfr/utils/init.py +746 -0
  134. tnfr/utils/init.pyi +85 -0
  135. tnfr/{json_utils.py → utils/io.py} +13 -18
  136. tnfr/utils/io.pyi +10 -0
  137. tnfr/utils/validators.py +130 -0
  138. tnfr/utils/validators.pyi +19 -0
  139. tnfr/validation/__init__.py +25 -0
  140. tnfr/validation/__init__.pyi +17 -0
  141. tnfr/validation/compatibility.py +59 -0
  142. tnfr/validation/compatibility.pyi +8 -0
  143. tnfr/validation/grammar.py +149 -0
  144. tnfr/validation/grammar.pyi +11 -0
  145. tnfr/validation/rules.py +194 -0
  146. tnfr/validation/rules.pyi +18 -0
  147. tnfr/validation/syntax.py +151 -0
  148. tnfr/validation/syntax.pyi +7 -0
  149. tnfr-6.0.0.dist-info/METADATA +135 -0
  150. tnfr-6.0.0.dist-info/RECORD +157 -0
  151. tnfr/graph_utils.py +0 -84
  152. tnfr/import_utils.py +0 -228
  153. tnfr/logging_utils.py +0 -116
  154. tnfr/validators.py +0 -84
  155. tnfr/value_utils.py +0 -59
  156. tnfr-4.5.2.dist-info/METADATA +0 -379
  157. tnfr-4.5.2.dist-info/RECORD +0 -67
  158. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  159. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  160. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  161. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/structural.pyi ADDED
@@ -0,0 +1,46 @@
1
+ from typing import TYPE_CHECKING, Any, Callable, Hashable, Iterable
2
+
3
+ from .operators.definitions import (
4
+ Operator,
5
+ Emission,
6
+ Reception,
7
+ Coherence,
8
+ Dissonance,
9
+ Coupling,
10
+ Resonance,
11
+ Silence,
12
+ Expansion,
13
+ Contraction,
14
+ SelfOrganization,
15
+ Mutation,
16
+ Transition,
17
+ Recursivity,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ import networkx as nx
22
+
23
+ __all__: tuple[str, ...]
24
+
25
+
26
+ def __getattr__(name: str) -> Any: ...
27
+
28
+
29
+ def create_nfr(
30
+ name: str,
31
+ *,
32
+ epi: float = ...,
33
+ vf: float = ...,
34
+ theta: float = ...,
35
+ graph: "nx.Graph" | None = ...,
36
+ dnfr_hook: Callable[..., None] = ...,
37
+ ) -> tuple["nx.Graph", str]: ...
38
+
39
+
40
+ OPERATORS: dict[str, Operator]
41
+
42
+
43
+ def validate_sequence(names: Iterable[str]) -> tuple[bool, str]: ...
44
+
45
+
46
+ def run_sequence(G: "nx.Graph", node: Hashable, ops: Iterable[Operator]) -> None: ...
@@ -0,0 +1,13 @@
1
+ """Telemetry helpers for shared observability settings."""
2
+
3
+ from .verbosity import (
4
+ TelemetryVerbosity,
5
+ TELEMETRY_VERBOSITY_DEFAULT,
6
+ TELEMETRY_VERBOSITY_LEVELS,
7
+ )
8
+
9
+ __all__ = [
10
+ "TelemetryVerbosity",
11
+ "TELEMETRY_VERBOSITY_DEFAULT",
12
+ "TELEMETRY_VERBOSITY_LEVELS",
13
+ ]
@@ -0,0 +1,37 @@
1
+ """Canonical telemetry verbosity presets for TNFR structures.
2
+
3
+ Each level expresses how much structural context is exported in traces and
4
+ metrics:
5
+
6
+ * ``basic`` preserves lightweight coherence checks for quick health probes.
7
+ * ``detailed`` adds phase alignment and coupling diagnostics to map resonance.
8
+ * ``debug`` captures the full glyph narrative for deep structural forensics.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from enum import Enum
14
+
15
+
16
+ class TelemetryVerbosity(str, Enum):
17
+ """Enumerated verbosity tiers shared by trace and metrics pipelines."""
18
+
19
+ BASIC = "basic"
20
+ DETAILED = "detailed"
21
+ DEBUG = "debug"
22
+
23
+
24
+ TELEMETRY_VERBOSITY_LEVELS: tuple[str, ...] = tuple(
25
+ level.value for level in TelemetryVerbosity
26
+ )
27
+ """Ordered tuple of canonical telemetry verbosity identifiers."""
28
+
29
+ TELEMETRY_VERBOSITY_DEFAULT: str = TelemetryVerbosity.DEBUG.value
30
+ """Default telemetry verbosity preserving complete structural capture."""
31
+
32
+
33
+ __all__ = [
34
+ "TelemetryVerbosity",
35
+ "TELEMETRY_VERBOSITY_LEVELS",
36
+ "TELEMETRY_VERBOSITY_DEFAULT",
37
+ ]
tnfr/tokens.py CHANGED
@@ -6,9 +6,10 @@ from dataclasses import dataclass
6
6
  from enum import Enum, auto
7
7
  from typing import Any, Iterable, Optional, Sequence, Union
8
8
 
9
- from .types import Glyph
9
+ from .types import Glyph, NodeId
10
10
 
11
- Node = Any
11
+ Node = NodeId
12
+ #: Alias maintained for backwards compatibility with historical token helpers.
12
13
 
13
14
 
14
15
  @dataclass(slots=True)
tnfr/tokens.pyi ADDED
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Iterable, Optional, Sequence
6
+
7
+ from .types import Glyph, NodeId
8
+ from ._compat import TypeAlias
9
+
10
+ __all__: tuple[str, ...]
11
+
12
+ Node: TypeAlias = NodeId
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class WAIT:
17
+ steps: int = 1
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class TARGET:
22
+ nodes: Optional[Iterable[Node]] = None
23
+
24
+
25
+ @dataclass(slots=True)
26
+ class THOL:
27
+ body: Sequence["Token"]
28
+ repeat: int = 1
29
+ force_close: Optional[Glyph] = None
30
+
31
+
32
+ Token: TypeAlias = Glyph | WAIT | TARGET | THOL | str
33
+
34
+ THOL_SENTINEL: object
35
+
36
+
37
+ class OpTag(Enum):
38
+ TARGET = ...
39
+ WAIT = ...
40
+ GLYPH = ...
41
+ THOL = ...
tnfr/trace.py CHANGED
@@ -7,25 +7,40 @@ structures as immutable snapshots.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from functools import partial
11
- from typing import Any, Callable, Optional, Protocol, NamedTuple, TypedDict, cast
10
+ import warnings
11
+
12
+ from typing import Any, Callable, Protocol, NamedTuple, TypedDict, cast
12
13
  from collections.abc import Iterable, Mapping
14
+ from types import MappingProxyType
13
15
 
14
16
  from .constants import TRACE
15
17
  from .glyph_history import ensure_history, count_glyphs, append_metric
16
- from .import_utils import cached_import
17
- from .graph_utils import get_graph_mapping
18
- from .collections_utils import is_non_string_sequence
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
+ )
19
32
 
20
33
 
21
34
  class _KuramotoFn(Protocol):
22
- def __call__(self, G: Any) -> tuple[float, float]: ...
35
+ def __call__(self, G: TNFRGraph) -> tuple[float, float]:
36
+ ...
23
37
 
24
38
 
25
39
  class _SigmaVectorFn(Protocol):
26
40
  def __call__(
27
- self, G: Any, weight_mode: str | None = None
28
- ) -> dict[str, float]: ...
41
+ self, G: TNFRGraph, weight_mode: str | None = None
42
+ ) -> SigmaVector:
43
+ ...
29
44
 
30
45
 
31
46
  class CallbackSpec(NamedTuple):
@@ -58,7 +73,77 @@ class TraceSnapshot(TraceMetadata, total=False):
58
73
  phase: str
59
74
 
60
75
 
61
- def _kuramoto_fallback(G: Any) -> tuple[float, float]:
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."""
112
+
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))}
122
+
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]:
62
147
  return 0.0, 0.0
63
148
 
64
149
 
@@ -69,8 +154,8 @@ kuramoto_R_psi: _KuramotoFn = cast(
69
154
 
70
155
 
71
156
  def _sigma_fallback(
72
- G: Any, _weight_mode: str | None = None
73
- ) -> dict[str, float]:
157
+ G: TNFRGraph, _weight_mode: str | None = None
158
+ ) -> SigmaVector:
74
159
  """Return a null sigma vector regardless of ``_weight_mode``."""
75
160
 
76
161
  return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0, "n": 0}
@@ -79,6 +164,7 @@ def _sigma_fallback(
79
164
  # Public exports for this module
80
165
  __all__ = (
81
166
  "CallbackSpec",
167
+ "TraceFieldSpec",
82
168
  "TraceMetadata",
83
169
  "TraceSnapshot",
84
170
  "register_trace",
@@ -94,9 +180,12 @@ __all__ = (
94
180
 
95
181
 
96
182
  def _trace_setup(
97
- G,
183
+ G: TNFRGraph,
98
184
  ) -> tuple[
99
- Optional[dict[str, Any]], set[str], Optional[dict[str, Any]], Optional[str]
185
+ Mapping[str, Any] | None,
186
+ set[str],
187
+ dict[str, Any] | None,
188
+ str | None,
100
189
  ]:
101
190
  """Common configuration for trace snapshots.
102
191
 
@@ -105,13 +194,14 @@ def _trace_setup(
105
194
  ``(None, set(), None, None)``.
106
195
  """
107
196
 
108
- cfg = G.graph.get("TRACE", TRACE)
197
+ cfg_raw = G.graph.get("TRACE", TRACE)
198
+ cfg = cfg_raw if isinstance(cfg_raw, Mapping) else TRACE
109
199
  if not cfg.get("enabled", True):
110
200
  return None, set(), None, None
111
201
 
112
- capture: set[str] = set(cfg.get("capture", []))
202
+ capture = _resolve_trace_capture(cfg)
113
203
  hist = ensure_history(G)
114
- key = cfg.get("history_key", "trace_meta")
204
+ key = cast(str | None, cfg.get("history_key", "trace_meta"))
115
205
  return cfg, capture, hist, key
116
206
 
117
207
 
@@ -129,14 +219,17 @@ def _callback_names(
129
219
  ]
130
220
 
131
221
 
132
- def mapping_field(G: Any, graph_key: str, out_key: str) -> TraceMetadata:
222
+ EMPTY_MAPPING: Mapping[str, Any] = MappingProxyType({})
223
+
224
+
225
+ def mapping_field(G: TNFRGraph, graph_key: str, out_key: str) -> TraceMetadata:
133
226
  """Helper to copy mappings from ``G.graph`` into trace output."""
134
227
  mapping = get_graph_mapping(
135
- G, graph_key, f"G.graph[{graph_key!r}] no es un mapeo; se ignora"
228
+ G, graph_key, f"G.graph[{graph_key!r}] is not a mapping; ignoring"
136
229
  )
137
230
  if mapping is None:
138
231
  return {}
139
- return cast(TraceMetadata, {out_key: mapping})
232
+ return {out_key: mapping}
140
233
 
141
234
 
142
235
  # -------------------------
@@ -145,10 +238,8 @@ def mapping_field(G: Any, graph_key: str, out_key: str) -> TraceMetadata:
145
238
 
146
239
 
147
240
  def _new_trace_meta(
148
- G, phase: str
149
- ) -> Optional[
150
- tuple[TraceSnapshot, set[str], Optional[dict[str, Any]], Optional[str]]
151
- ]:
241
+ G: TNFRGraph, phase: str
242
+ ) -> tuple[TraceSnapshot, set[str], dict[str, Any] | None, str | None] | None:
152
243
  """Initialise trace metadata for a ``phase``.
153
244
 
154
245
  Wraps :func:`_trace_setup` and creates the base structure with timestamp
@@ -169,7 +260,7 @@ def _new_trace_meta(
169
260
 
170
261
 
171
262
  def _trace_capture(
172
- G, phase: str, fields: Mapping[str, Callable[[Any], TraceMetadata]]
263
+ G: TNFRGraph, phase: str, fields: TraceFieldMap
173
264
  ) -> None:
174
265
  """Capture ``fields`` for ``phase`` and store the snapshot.
175
266
 
@@ -187,7 +278,7 @@ def _trace_capture(
187
278
  return
188
279
  for name, getter in fields.items():
189
280
  if name in capture:
190
- meta.update(cast(TraceSnapshot, getter(G)))
281
+ meta.update(getter(G))
191
282
  if hist is None or key is None:
192
283
  return
193
284
  append_metric(hist, key, meta)
@@ -198,54 +289,68 @@ def _trace_capture(
198
289
  # -------------------------
199
290
 
200
291
 
201
- TRACE_FIELDS: dict[str, dict[str, Callable[[Any], TraceMetadata]]] = {}
292
+ TRACE_FIELDS: TraceFieldRegistry = {}
202
293
 
203
294
 
204
295
  def register_trace_field(
205
- phase: str, name: str, func: Callable[[Any], TraceMetadata]
296
+ phase: str, name: str, func: TraceFieldFn
206
297
  ) -> None:
207
298
  """Register ``func`` to populate trace field ``name`` during ``phase``."""
208
299
 
209
300
  TRACE_FIELDS.setdefault(phase, {})[name] = func
210
301
 
211
302
 
212
- gamma_field = partial(mapping_field, graph_key="GAMMA", out_key="gamma")
303
+ def gamma_field(G: TNFRGraph) -> TraceMetadata:
304
+ return mapping_field(G, "GAMMA", "gamma")
213
305
 
214
306
 
215
- grammar_field = partial(mapping_field, graph_key="GRAMMAR_CANON", out_key="grammar")
307
+ def grammar_field(G: TNFRGraph) -> TraceMetadata:
308
+ return mapping_field(G, "GRAMMAR_CANON", "grammar")
216
309
 
217
310
 
218
- dnfr_weights_field = partial(
219
- mapping_field, graph_key="DNFR_WEIGHTS", out_key="dnfr_weights"
220
- )
311
+ def dnfr_weights_field(G: TNFRGraph) -> TraceMetadata:
312
+ return mapping_field(G, "DNFR_WEIGHTS", "dnfr_weights")
221
313
 
222
314
 
223
- def selector_field(G: Any) -> TraceMetadata:
315
+ def selector_field(G: TNFRGraph) -> TraceMetadata:
224
316
  sel = G.graph.get("glyph_selector")
225
- return cast(TraceMetadata, {"selector": getattr(sel, "__name__", str(sel)) if sel else None})
317
+ selector_name = getattr(sel, "__name__", str(sel)) if sel else None
318
+ return {"selector": selector_name}
226
319
 
227
320
 
228
- _si_weights_field = partial(mapping_field, graph_key="_Si_weights", out_key="si_weights")
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}
229
326
 
230
327
 
231
- _si_sensitivity_field = partial(
232
- mapping_field, graph_key="_Si_sensitivity", out_key="si_sensitivity"
233
- )
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)
234
338
 
339
+ if normalised != mapping:
340
+ G.graph["_Si_sensitivity"] = normalised
235
341
 
236
- def si_weights_field(G: Any) -> TraceMetadata:
342
+ return {"si_sensitivity": MappingProxyType(normalised)}
343
+
344
+
345
+ def si_weights_field(G: TNFRGraph) -> TraceMetadata:
237
346
  """Return sense-plane weights and sensitivity."""
238
347
 
239
- return cast(
240
- TraceMetadata,
241
- {
242
- **(_si_weights_field(G) or {"si_weights": {}}),
243
- **(_si_sensitivity_field(G) or {"si_sensitivity": {}}),
244
- },
245
- )
348
+ weights = _si_weights_field(G)
349
+ sensitivity = _si_sensitivity_field(G)
350
+ return {**weights, **sensitivity}
246
351
 
247
352
 
248
- def callbacks_field(G: Any) -> TraceMetadata:
353
+ def callbacks_field(G: TNFRGraph) -> TraceMetadata:
249
354
  cb = G.graph.get("callbacks")
250
355
  if not isinstance(cb, Mapping):
251
356
  return {}
@@ -255,24 +360,24 @@ def callbacks_field(G: Any) -> TraceMetadata:
255
360
  out[phase] = _callback_names(cb_map)
256
361
  else:
257
362
  out[phase] = None
258
- return cast(TraceMetadata, {"callbacks": out})
363
+ return {"callbacks": out}
259
364
 
260
365
 
261
- def thol_state_field(G: Any) -> TraceMetadata:
366
+ def thol_state_field(G: TNFRGraph) -> TraceMetadata:
262
367
  th_open = 0
263
368
  for _, nd in G.nodes(data=True):
264
369
  st = nd.get("_GRAM", {})
265
370
  if st.get("thol_open", False):
266
371
  th_open += 1
267
- return cast(TraceMetadata, {"thol_open_nodes": th_open})
372
+ return {"thol_open_nodes": th_open}
268
373
 
269
374
 
270
- def kuramoto_field(G: Any) -> TraceMetadata:
375
+ def kuramoto_field(G: TNFRGraph) -> TraceMetadata:
271
376
  R, psi = kuramoto_R_psi(G)
272
- return cast(TraceMetadata, {"kuramoto": {"R": float(R), "psi": float(psi)}})
377
+ return {"kuramoto": {"R": float(R), "psi": float(psi)}}
273
378
 
274
379
 
275
- def sigma_field(G: Any) -> TraceMetadata:
380
+ def sigma_field(G: TNFRGraph) -> TraceMetadata:
276
381
  sigma_vector_from_graph: _SigmaVectorFn = cast(
277
382
  _SigmaVectorFn,
278
383
  cached_import(
@@ -282,20 +387,17 @@ def sigma_field(G: Any) -> TraceMetadata:
282
387
  ),
283
388
  )
284
389
  sv = sigma_vector_from_graph(G)
285
- return cast(
286
- TraceMetadata,
287
- {
288
- "sigma": {
289
- "x": float(sv.get("x", 0.0)),
290
- "y": float(sv.get("y", 0.0)),
291
- "mag": float(sv.get("mag", 0.0)),
292
- "angle": float(sv.get("angle", 0.0)),
293
- }
294
- },
295
- )
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
+ }
296
398
 
297
399
 
298
- def glyph_counts_field(G: Any) -> TraceMetadata:
400
+ def glyph_counts_field(G: TNFRGraph) -> TraceMetadata:
299
401
  """Return glyph count snapshot.
300
402
 
301
403
  ``count_glyphs`` already produces a fresh mapping so no additional copy
@@ -303,21 +405,109 @@ def glyph_counts_field(G: Any) -> TraceMetadata:
303
405
  """
304
406
 
305
407
  cnt = count_glyphs(G, window=1)
306
- return cast(TraceMetadata, {"glyphs": cnt})
307
-
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
+ )
308
501
 
309
- # Pre-register default fields
310
- register_trace_field("before", "gamma", gamma_field)
311
- register_trace_field("before", "grammar", grammar_field)
312
- register_trace_field("before", "selector", selector_field)
313
- register_trace_field("before", "dnfr_weights", dnfr_weights_field)
314
- register_trace_field("before", "si_weights", si_weights_field)
315
- register_trace_field("before", "callbacks", callbacks_field)
316
- register_trace_field("before", "thol_open_nodes", thol_state_field)
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
+ }
317
508
 
318
- register_trace_field("after", "kuramoto", kuramoto_field)
319
- register_trace_field("after", "sigma", sigma_field)
320
- register_trace_field("after", "glyph_counts", glyph_counts_field)
509
+ for spec in TRACE_FIELD_SPECS:
510
+ register_trace_field(spec.phase, spec.name, spec.producer)
321
511
 
322
512
 
323
513
  # -------------------------
@@ -325,7 +515,7 @@ register_trace_field("after", "glyph_counts", glyph_counts_field)
325
515
  # -------------------------
326
516
 
327
517
 
328
- def register_trace(G) -> None:
518
+ def register_trace(G: TNFRGraph) -> None:
329
519
  """Enable before/after-step snapshots and dump operational metadata
330
520
  to history.
331
521
 
@@ -354,11 +544,11 @@ def register_trace(G) -> None:
354
544
  for phase in TRACE_FIELDS.keys():
355
545
  event = f"{phase}_step"
356
546
 
357
- def _make_cb(ph):
358
- def _cb(G, ctx: dict[str, Any] | None = None):
547
+ def _make_cb(ph: str) -> TraceCallback:
548
+ def _cb(graph: TNFRGraph, ctx: dict[str, Any]) -> None:
359
549
  del ctx
360
550
 
361
- _trace_capture(G, ph, TRACE_FIELDS.get(ph, {}))
551
+ _trace_capture(graph, ph, TRACE_FIELDS.get(ph, {}))
362
552
 
363
553
  return _cb
364
554