tnfr 4.5.1__py3-none-any.whl → 4.5.2__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 (78) hide show
  1. tnfr/__init__.py +91 -90
  2. tnfr/alias.py +546 -0
  3. tnfr/cache.py +578 -0
  4. tnfr/callback_utils.py +388 -0
  5. tnfr/cli/__init__.py +75 -0
  6. tnfr/cli/arguments.py +177 -0
  7. tnfr/cli/execution.py +288 -0
  8. tnfr/cli/utils.py +36 -0
  9. tnfr/collections_utils.py +300 -0
  10. tnfr/config.py +19 -28
  11. tnfr/constants/__init__.py +174 -0
  12. tnfr/constants/core.py +159 -0
  13. tnfr/constants/init.py +31 -0
  14. tnfr/constants/metric.py +110 -0
  15. tnfr/constants_glyphs.py +98 -0
  16. tnfr/dynamics/__init__.py +658 -0
  17. tnfr/dynamics/dnfr.py +733 -0
  18. tnfr/dynamics/integrators.py +267 -0
  19. tnfr/dynamics/sampling.py +31 -0
  20. tnfr/execution.py +201 -0
  21. tnfr/flatten.py +283 -0
  22. tnfr/gamma.py +302 -88
  23. tnfr/glyph_history.py +290 -0
  24. tnfr/grammar.py +285 -96
  25. tnfr/graph_utils.py +84 -0
  26. tnfr/helpers/__init__.py +71 -0
  27. tnfr/helpers/numeric.py +87 -0
  28. tnfr/immutable.py +178 -0
  29. tnfr/import_utils.py +228 -0
  30. tnfr/initialization.py +197 -0
  31. tnfr/io.py +246 -0
  32. tnfr/json_utils.py +162 -0
  33. tnfr/locking.py +37 -0
  34. tnfr/logging_utils.py +116 -0
  35. tnfr/metrics/__init__.py +41 -0
  36. tnfr/metrics/coherence.py +829 -0
  37. tnfr/metrics/common.py +151 -0
  38. tnfr/metrics/core.py +101 -0
  39. tnfr/metrics/diagnosis.py +234 -0
  40. tnfr/metrics/export.py +137 -0
  41. tnfr/metrics/glyph_timing.py +189 -0
  42. tnfr/metrics/reporting.py +148 -0
  43. tnfr/metrics/sense_index.py +120 -0
  44. tnfr/metrics/trig.py +181 -0
  45. tnfr/metrics/trig_cache.py +109 -0
  46. tnfr/node.py +214 -159
  47. tnfr/observers.py +126 -136
  48. tnfr/ontosim.py +134 -134
  49. tnfr/operators/__init__.py +420 -0
  50. tnfr/operators/jitter.py +203 -0
  51. tnfr/operators/remesh.py +485 -0
  52. tnfr/presets.py +46 -14
  53. tnfr/rng.py +254 -0
  54. tnfr/selector.py +210 -0
  55. tnfr/sense.py +284 -131
  56. tnfr/structural.py +207 -79
  57. tnfr/tokens.py +60 -0
  58. tnfr/trace.py +329 -94
  59. tnfr/types.py +43 -17
  60. tnfr/validators.py +70 -24
  61. tnfr/value_utils.py +59 -0
  62. tnfr-4.5.2.dist-info/METADATA +379 -0
  63. tnfr-4.5.2.dist-info/RECORD +67 -0
  64. tnfr/cli.py +0 -322
  65. tnfr/constants.py +0 -277
  66. tnfr/dynamics.py +0 -814
  67. tnfr/helpers.py +0 -264
  68. tnfr/main.py +0 -47
  69. tnfr/metrics.py +0 -597
  70. tnfr/operators.py +0 -525
  71. tnfr/program.py +0 -176
  72. tnfr/scenarios.py +0 -34
  73. tnfr-4.5.1.dist-info/METADATA +0 -221
  74. tnfr-4.5.1.dist-info/RECORD +0 -28
  75. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
  76. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
  77. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
  78. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/top_level.txt +0 -0
tnfr/trace.py CHANGED
@@ -1,134 +1,369 @@
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
+ from functools import partial
11
+ from typing import Any, Callable, Optional, Protocol, NamedTuple, TypedDict, cast
12
+ from collections.abc import Iterable, Mapping
13
+
14
+ from .constants import TRACE
15
+ 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
19
+
20
+
21
+ class _KuramotoFn(Protocol):
22
+ def __call__(self, G: Any) -> tuple[float, float]: ...
23
+
24
+
25
+ class _SigmaVectorFn(Protocol):
26
+ def __call__(
27
+ self, G: Any, weight_mode: str | None = None
28
+ ) -> dict[str, float]: ...
29
+
30
+
31
+ class CallbackSpec(NamedTuple):
32
+ """Specification for a registered callback."""
33
+
34
+ name: str | None
35
+ func: Callable[..., Any]
36
+
37
+
38
+ class TraceMetadata(TypedDict, total=False):
39
+ """Metadata captured by trace field functions."""
40
+
41
+ gamma: Mapping[str, Any]
42
+ grammar: Mapping[str, Any]
43
+ selector: str | None
44
+ dnfr_weights: Mapping[str, Any]
45
+ si_weights: Mapping[str, Any]
46
+ si_sensitivity: Mapping[str, Any]
47
+ callbacks: Mapping[str, list[str] | None]
48
+ thol_open_nodes: int
49
+ kuramoto: Mapping[str, float]
50
+ sigma: Mapping[str, float]
51
+ glyphs: Mapping[str, int]
52
+
53
+
54
+ class TraceSnapshot(TraceMetadata, total=False):
55
+ """Trace snapshot stored in the history."""
56
+
57
+ t: float
58
+ phase: str
59
+
60
+
61
+ def _kuramoto_fallback(G: Any) -> tuple[float, float]:
62
+ return 0.0, 0.0
63
+
64
+
65
+ kuramoto_R_psi: _KuramotoFn = cast(
66
+ _KuramotoFn,
67
+ cached_import("tnfr.gamma", "kuramoto_R_psi", fallback=_kuramoto_fallback),
68
+ )
69
+
70
+
71
+ def _sigma_fallback(
72
+ G: Any, _weight_mode: str | None = None
73
+ ) -> dict[str, float]:
74
+ """Return a null sigma vector regardless of ``_weight_mode``."""
75
+
76
+ return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0, "n": 0}
7
77
 
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
13
78
 
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}
79
+ # Public exports for this module
80
+ __all__ = (
81
+ "CallbackSpec",
82
+ "TraceMetadata",
83
+ "TraceSnapshot",
84
+ "register_trace",
85
+ "register_trace_field",
86
+ "_callback_names",
87
+ "gamma_field",
88
+ "grammar_field",
89
+ )
19
90
 
20
91
  # -------------------------
21
- # Defaults
92
+ # Helpers
22
93
  # -------------------------
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
- })
94
+
95
+
96
+ def _trace_setup(
97
+ G,
98
+ ) -> tuple[
99
+ Optional[dict[str, Any]], set[str], Optional[dict[str, Any]], Optional[str]
100
+ ]:
101
+ """Common configuration for trace snapshots.
102
+
103
+ Returns the active configuration, capture set, history and key under
104
+ which metadata will be stored. If tracing is disabled returns
105
+ ``(None, set(), None, None)``.
106
+ """
107
+
108
+ cfg = G.graph.get("TRACE", TRACE)
109
+ if not cfg.get("enabled", True):
110
+ return None, set(), None, None
111
+
112
+ capture: set[str] = set(cfg.get("capture", []))
113
+ hist = ensure_history(G)
114
+ key = cfg.get("history_key", "trace_meta")
115
+ return cfg, capture, hist, key
116
+
117
+
118
+ def _callback_names(
119
+ callbacks: Mapping[str, CallbackSpec] | Iterable[CallbackSpec],
120
+ ) -> list[str]:
121
+ """Return callback names from ``callbacks``."""
122
+ if isinstance(callbacks, Mapping):
123
+ callbacks = callbacks.values()
124
+ return [
125
+ cb.name
126
+ if cb.name is not None
127
+ else str(getattr(cb.func, "__name__", "fn"))
128
+ for cb in callbacks
129
+ ]
130
+
131
+
132
+ def mapping_field(G: Any, graph_key: str, out_key: str) -> TraceMetadata:
133
+ """Helper to copy mappings from ``G.graph`` into trace output."""
134
+ mapping = get_graph_mapping(
135
+ G, graph_key, f"G.graph[{graph_key!r}] no es un mapeo; se ignora"
136
+ )
137
+ if mapping is None:
138
+ return {}
139
+ return cast(TraceMetadata, {out_key: mapping})
140
+
28
141
 
29
142
  # -------------------------
30
- # Helpers
143
+ # Builders
31
144
  # -------------------------
32
145
 
146
+
147
+ def _new_trace_meta(
148
+ G, phase: str
149
+ ) -> Optional[
150
+ tuple[TraceSnapshot, set[str], Optional[dict[str, Any]], Optional[str]]
151
+ ]:
152
+ """Initialise trace metadata for a ``phase``.
153
+
154
+ Wraps :func:`_trace_setup` and creates the base structure with timestamp
155
+ and current phase. Returns ``None`` if tracing is disabled.
156
+ """
157
+
158
+ cfg, capture, hist, key = _trace_setup(G)
159
+ if not cfg:
160
+ return None
161
+
162
+ meta: TraceSnapshot = {"t": float(G.graph.get("_t", 0.0)), "phase": phase}
163
+ return meta, capture, hist, key
164
+
165
+
33
166
  # -------------------------
34
167
  # Snapshots
35
168
  # -------------------------
36
169
 
37
- def _trace_before(G, *args, **kwargs):
38
- if not G.graph.get("TRACE", DEFAULTS["TRACE"]).get("enabled", True):
170
+
171
+ def _trace_capture(
172
+ G, phase: str, fields: Mapping[str, Callable[[Any], TraceMetadata]]
173
+ ) -> None:
174
+ """Capture ``fields`` for ``phase`` and store the snapshot.
175
+
176
+ A :class:`TraceSnapshot` is appended to the configured history when
177
+ tracing is active. If there is no active history or storage key the
178
+ capture is silently ignored.
179
+ """
180
+
181
+ res = _new_trace_meta(G, phase)
182
+ if not res:
39
183
  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
184
 
45
- meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "before"}
185
+ meta, capture, hist, key = res
186
+ if not capture:
187
+ return
188
+ for name, getter in fields.items():
189
+ if name in capture:
190
+ meta.update(cast(TraceSnapshot, getter(G)))
191
+ if hist is None or key is None:
192
+ return
193
+ append_metric(hist, key, meta)
46
194
 
47
- if "gamma" in capture:
48
- meta["gamma"] = dict(G.graph.get("GAMMA", {}))
49
195
 
50
- if "grammar" in capture:
51
- meta["grammar"] = dict(G.graph.get("GRAMMAR_CANON", {}))
196
+ # -------------------------
197
+ # Registry
198
+ # -------------------------
52
199
 
53
- if "selector" in capture:
54
- sel = G.graph.get("glyph_selector")
55
- meta["selector"] = getattr(sel, "__name__", str(sel)) if sel else None
56
200
 
57
- if "dnfr_weights" in capture:
58
- mix = G.graph.get("DNFR_WEIGHTS")
59
- if isinstance(mix, dict):
60
- meta["dnfr_weights"] = dict(mix)
201
+ TRACE_FIELDS: dict[str, dict[str, Callable[[Any], TraceMetadata]]] = {}
61
202
 
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", {}))
65
203
 
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
204
+ def register_trace_field(
205
+ phase: str, name: str, func: Callable[[Any], TraceMetadata]
206
+ ) -> None:
207
+ """Register ``func`` to populate trace field ``name`` during ``phase``."""
72
208
 
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
209
+ TRACE_FIELDS.setdefault(phase, {})[name] = func
81
210
 
82
- hist.setdefault(key, []).append(meta)
83
211
 
212
+ gamma_field = partial(mapping_field, graph_key="GAMMA", out_key="gamma")
84
213
 
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")
92
214
 
93
- meta: Dict[str, Any] = {"t": float(G.graph.get("_t", 0.0)), "phase": "after"}
215
+ grammar_field = partial(mapping_field, graph_key="GRAMMAR_CANON", out_key="grammar")
216
+
217
+
218
+ dnfr_weights_field = partial(
219
+ mapping_field, graph_key="DNFR_WEIGHTS", out_key="dnfr_weights"
220
+ )
221
+
222
+
223
+ def selector_field(G: Any) -> TraceMetadata:
224
+ sel = G.graph.get("glyph_selector")
225
+ return cast(TraceMetadata, {"selector": getattr(sel, "__name__", str(sel)) if sel else None})
226
+
227
+
228
+ _si_weights_field = partial(mapping_field, graph_key="_Si_weights", out_key="si_weights")
229
+
230
+
231
+ _si_sensitivity_field = partial(
232
+ mapping_field, graph_key="_Si_sensitivity", out_key="si_sensitivity"
233
+ )
234
+
235
+
236
+ def si_weights_field(G: Any) -> TraceMetadata:
237
+ """Return sense-plane weights and sensitivity."""
238
+
239
+ return cast(
240
+ TraceMetadata,
241
+ {
242
+ **(_si_weights_field(G) or {"si_weights": {}}),
243
+ **(_si_sensitivity_field(G) or {"si_sensitivity": {}}),
244
+ },
245
+ )
94
246
 
95
- if "kuramoto" in capture:
96
- R, psi = kuramoto_R_psi(G)
97
- meta["kuramoto"] = {"R": float(R), "psi": float(psi)}
98
247
 
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))}
248
+ def callbacks_field(G: Any) -> TraceMetadata:
249
+ cb = G.graph.get("callbacks")
250
+ if not isinstance(cb, Mapping):
251
+ return {}
252
+ out: dict[str, list[str] | None] = {}
253
+ for phase, cb_map in cb.items():
254
+ if isinstance(cb_map, Mapping) or is_non_string_sequence(cb_map):
255
+ out[phase] = _callback_names(cb_map)
256
+ else:
257
+ out[phase] = None
258
+ return cast(TraceMetadata, {"callbacks": out})
102
259
 
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)
110
260
 
111
- hist.setdefault(key, []).append(meta)
261
+ def thol_state_field(G: Any) -> TraceMetadata:
262
+ th_open = 0
263
+ for _, nd in G.nodes(data=True):
264
+ st = nd.get("_GRAM", {})
265
+ if st.get("thol_open", False):
266
+ th_open += 1
267
+ return cast(TraceMetadata, {"thol_open_nodes": th_open})
268
+
269
+
270
+ def kuramoto_field(G: Any) -> TraceMetadata:
271
+ R, psi = kuramoto_R_psi(G)
272
+ return cast(TraceMetadata, {"kuramoto": {"R": float(R), "psi": float(psi)}})
273
+
274
+
275
+ def sigma_field(G: Any) -> TraceMetadata:
276
+ sigma_vector_from_graph: _SigmaVectorFn = cast(
277
+ _SigmaVectorFn,
278
+ cached_import(
279
+ "tnfr.sense",
280
+ "sigma_vector_from_graph",
281
+ fallback=_sigma_fallback,
282
+ ),
283
+ )
284
+ 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
+ )
296
+
297
+
298
+ def glyph_counts_field(G: Any) -> TraceMetadata:
299
+ """Return glyph count snapshot.
300
+
301
+ ``count_glyphs`` already produces a fresh mapping so no additional copy
302
+ is taken. Treat the returned mapping as read-only.
303
+ """
304
+
305
+ cnt = count_glyphs(G, window=1)
306
+ return cast(TraceMetadata, {"glyphs": cnt})
307
+
308
+
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)
317
+
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)
112
321
 
113
322
 
114
323
  # -------------------------
115
324
  # API
116
325
  # -------------------------
117
326
 
327
+
118
328
  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
329
+ """Enable before/after-step snapshots and dump operational metadata
330
+ to history.
331
+
332
+ Trace snapshots are stored as :class:`TraceSnapshot` entries in
333
+ ``G.graph['history'][TRACE.history_key]`` with:
334
+ - gamma: active Γi(R) specification
335
+ - grammar: canonical grammar configuration
336
+ - selector: glyph selector name
337
+ - dnfr_weights: ΔNFR mix declared in the engine
338
+ - si_weights: α/β/γ weights and Si sensitivity
339
+ - callbacks: callbacks registered per phase (if in
340
+ ``G.graph['callbacks']``)
341
+ - thol_open_nodes: how many nodes have an open THOL block
342
+ - kuramoto: network ``(R, ψ)``
343
+ - sigma: global sense-plane vector
344
+ - glyphs: glyph counts after the step
345
+
346
+ Field helpers reuse graph dictionaries and expect them to be treated as
347
+ immutable snapshots by consumers.
132
348
  """
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")
349
+ if G.graph.get("_trace_registered"):
350
+ return
351
+
352
+ from .callback_utils import callback_manager
353
+
354
+ for phase in TRACE_FIELDS.keys():
355
+ event = f"{phase}_step"
356
+
357
+ def _make_cb(ph):
358
+ def _cb(G, ctx: dict[str, Any] | None = None):
359
+ del ctx
360
+
361
+ _trace_capture(G, ph, TRACE_FIELDS.get(ph, {}))
362
+
363
+ return _cb
364
+
365
+ callback_manager.register_callback(
366
+ G, event=event, func=_make_cb(phase), name=f"trace_{phase}"
367
+ )
368
+
369
+ G.graph["_trace_registered"] = True
tnfr/types.py CHANGED
@@ -1,18 +1,44 @@
1
+ """Type definitions and protocols shared across the engine."""
2
+
1
3
  from __future__ import annotations
2
- from dataclasses import dataclass, field
3
- from typing import Dict, Any
4
-
5
-
6
- @dataclass
7
- class NodeState:
8
- EPI: float = 0.0
9
- vf: float = 1.0 # νf
10
- theta: float = 0.0 # θ
11
- Si: float = 0.5
12
- epi_kind: str = ""
13
- extra: Dict[str, Any] = field(default_factory=dict)
14
-
15
- def to_attrs(self) -> Dict[str, Any]:
16
- d = {"EPI": self.EPI, "νf": self.vf, "θ": self.theta, "Si": self.Si, "EPI_kind": self.epi_kind}
17
- d.update(self.extra)
18
- return d
4
+
5
+ from enum import Enum
6
+ from typing import Any, Iterable, Protocol
7
+
8
+ __all__ = ("GraphLike", "Glyph")
9
+
10
+
11
+ class GraphLike(Protocol):
12
+ """Protocol for graph objects used throughout TNFR metrics.
13
+
14
+ The metrics helpers assume a single coherent graph interface so that
15
+ coherence, resonance and derived indicators read/write data through the
16
+ same structural access points.
17
+ """
18
+
19
+ graph: dict[str, Any]
20
+
21
+ def nodes(self, data: bool = ...) -> Iterable[Any]: ...
22
+
23
+ def number_of_nodes(self) -> int: ...
24
+
25
+ def neighbors(self, n: Any) -> Iterable[Any]: ...
26
+
27
+ def __iter__(self) -> Iterable[Any]: ...
28
+
29
+ class Glyph(str, Enum):
30
+ """Canonical TNFR glyphs."""
31
+
32
+ AL = "AL"
33
+ EN = "EN"
34
+ IL = "IL"
35
+ OZ = "OZ"
36
+ UM = "UM"
37
+ RA = "RA"
38
+ SHA = "SHA"
39
+ VAL = "VAL"
40
+ NUL = "NUL"
41
+ THOL = "THOL"
42
+ ZHIR = "ZHIR"
43
+ NAV = "NAV"
44
+ REMESH = "REMESH"
tnfr/validators.py CHANGED
@@ -1,38 +1,84 @@
1
- """Validadores de invariantes TNFR."""
1
+ """Validation utilities."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Iterable
5
4
 
6
- from .constants import ALIAS_EPI, DEFAULTS
7
- from .helpers import _get_attr
8
- from .sense import sigma_vector_global, GLYPHS_CANONICAL
9
- from .helpers import last_glifo
5
+ import numbers
6
+ import sys
10
7
 
8
+ from .constants import get_aliases, get_param
9
+ from .alias import get_attr
10
+ from .sense import sigma_vector_from_graph
11
+ from .helpers.numeric import within_range
12
+ from .constants_glyphs import GLYPHS_CANONICAL_SET
11
13
 
12
- def _validate_epi(G) -> None:
13
- emin = float(G.graph.get("EPI_MIN", DEFAULTS.get("EPI_MIN", -1.0)))
14
- emax = float(G.graph.get("EPI_MAX", DEFAULTS.get("EPI_MAX", 1.0)))
15
- for n in G.nodes():
16
- x = float(_get_attr(G.nodes[n], ALIAS_EPI, 0.0))
17
- if not (emin - 1e-9 <= x <= emax + 1e-9):
18
- raise ValueError(f"EPI fuera de rango en nodo {n}: {x}")
14
+ ALIAS_EPI = get_aliases("EPI")
15
+ ALIAS_VF = get_aliases("VF")
16
+
17
+ __all__ = ("validate_window", "run_validators")
18
+
19
+
20
+ def validate_window(window: int, *, positive: bool = False) -> int:
21
+ """Validate ``window`` as an ``int`` and return it.
22
+
23
+ Non-integer values raise :class:`TypeError`. When ``positive`` is ``True``
24
+ the value must be strictly greater than zero; otherwise it may be zero.
25
+ Negative values always raise :class:`ValueError`.
26
+ """
27
+
28
+ if isinstance(window, bool) or not isinstance(window, numbers.Integral):
29
+ raise TypeError("'window' must be an integer")
30
+ if window < 0 or (positive and window == 0):
31
+ kind = "positive" if positive else "non-negative"
32
+ raise ValueError(f"'window'={window} must be {kind}")
33
+ return int(window)
34
+
35
+
36
+ def _require_attr(data, alias, node, name):
37
+ """Return attribute value or raise if missing."""
38
+ val = get_attr(data, alias, None)
39
+ if val is None:
40
+ raise ValueError(f"Missing {name} attribute in node {node}")
41
+ return val
19
42
 
20
43
 
21
44
  def _validate_sigma(G) -> None:
22
- sv = sigma_vector_global(G)
23
- if sv.get("mag", 0.0) > 1.0 + 1e-9:
24
- raise ValueError("Norma de σ excede 1")
45
+ sv = sigma_vector_from_graph(G)
46
+ if sv.get("mag", 0.0) > 1.0 + sys.float_info.epsilon:
47
+ raise ValueError("σ norm exceeds 1")
48
+
25
49
 
50
+ def _check_epi_vf(epi, vf, epi_min, epi_max, vf_min, vf_max, n):
51
+ _check_range(epi, epi_min, epi_max, "EPI", n)
52
+ _check_range(vf, vf_min, vf_max, "VF", n)
26
53
 
27
- def _validate_glifos(G) -> None:
28
- for n in G.nodes():
29
- g = last_glifo(G.nodes[n])
30
- if g and g not in GLYPHS_CANONICAL:
31
- raise ValueError(f"Glifo inválido {g} en nodo {n}")
54
+
55
+ def _out_of_range_msg(name, node, val):
56
+ return f"{name} out of range in node {node}: {val}"
57
+
58
+
59
+ def _check_range(val, lower, upper, name, node, tol: float = 1e-9):
60
+ if not within_range(val, lower, upper, tol):
61
+ raise ValueError(_out_of_range_msg(name, node, val))
62
+
63
+
64
+ def _check_glyph(g, n):
65
+ if g and g not in GLYPHS_CANONICAL_SET:
66
+ raise KeyError(f"Invalid glyph {g} in node {n}")
32
67
 
33
68
 
34
69
  def run_validators(G) -> None:
35
- """Ejecuta todos los validadores de invariantes sobre ``G``."""
36
- _validate_epi(G)
70
+ """Run all invariant validators on ``G`` with a single node pass."""
71
+ from .glyph_history import last_glyph
72
+
73
+ epi_min = float(get_param(G, "EPI_MIN"))
74
+ epi_max = float(get_param(G, "EPI_MAX"))
75
+ vf_min = float(get_param(G, "VF_MIN"))
76
+ vf_max = float(get_param(G, "VF_MAX"))
77
+
78
+ for n, data in G.nodes(data=True):
79
+ epi = _require_attr(data, ALIAS_EPI, n, "EPI")
80
+ vf = _require_attr(data, ALIAS_VF, n, "VF")
81
+ _check_epi_vf(epi, vf, epi_min, epi_max, vf_min, vf_max, n)
82
+ _check_glyph(last_glyph(data), n)
83
+
37
84
  _validate_sigma(G)
38
- _validate_glifos(G)