tnfr 4.5.0__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.
- tnfr/__init__.py +91 -89
- tnfr/alias.py +546 -0
- tnfr/cache.py +578 -0
- tnfr/callback_utils.py +388 -0
- tnfr/cli/__init__.py +75 -0
- tnfr/cli/arguments.py +177 -0
- tnfr/cli/execution.py +288 -0
- tnfr/cli/utils.py +36 -0
- tnfr/collections_utils.py +300 -0
- tnfr/config.py +19 -28
- tnfr/constants/__init__.py +174 -0
- tnfr/constants/core.py +159 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/metric.py +110 -0
- tnfr/constants_glyphs.py +98 -0
- tnfr/dynamics/__init__.py +658 -0
- tnfr/dynamics/dnfr.py +733 -0
- tnfr/dynamics/integrators.py +267 -0
- tnfr/dynamics/sampling.py +31 -0
- tnfr/execution.py +201 -0
- tnfr/flatten.py +283 -0
- tnfr/gamma.py +302 -88
- tnfr/glyph_history.py +290 -0
- tnfr/grammar.py +285 -96
- tnfr/graph_utils.py +84 -0
- tnfr/helpers/__init__.py +71 -0
- tnfr/helpers/numeric.py +87 -0
- tnfr/immutable.py +178 -0
- tnfr/import_utils.py +228 -0
- tnfr/initialization.py +197 -0
- tnfr/io.py +246 -0
- tnfr/json_utils.py +162 -0
- tnfr/locking.py +37 -0
- tnfr/logging_utils.py +116 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/coherence.py +829 -0
- tnfr/metrics/common.py +151 -0
- tnfr/metrics/core.py +101 -0
- tnfr/metrics/diagnosis.py +234 -0
- tnfr/metrics/export.py +137 -0
- tnfr/metrics/glyph_timing.py +189 -0
- tnfr/metrics/reporting.py +148 -0
- tnfr/metrics/sense_index.py +120 -0
- tnfr/metrics/trig.py +181 -0
- tnfr/metrics/trig_cache.py +109 -0
- tnfr/node.py +214 -159
- tnfr/observers.py +126 -128
- tnfr/ontosim.py +134 -134
- tnfr/operators/__init__.py +420 -0
- tnfr/operators/jitter.py +203 -0
- tnfr/operators/remesh.py +485 -0
- tnfr/presets.py +46 -14
- tnfr/rng.py +254 -0
- tnfr/selector.py +210 -0
- tnfr/sense.py +284 -131
- tnfr/structural.py +207 -79
- tnfr/tokens.py +60 -0
- tnfr/trace.py +329 -94
- tnfr/types.py +43 -17
- tnfr/validators.py +70 -24
- tnfr/value_utils.py +59 -0
- tnfr-4.5.2.dist-info/METADATA +379 -0
- tnfr-4.5.2.dist-info/RECORD +67 -0
- tnfr/cli.py +0 -322
- tnfr/constants.py +0 -277
- tnfr/dynamics.py +0 -814
- tnfr/helpers.py +0 -264
- tnfr/main.py +0 -47
- tnfr/metrics.py +0 -597
- tnfr/operators.py +0 -525
- tnfr/program.py +0 -176
- tnfr/scenarios.py +0 -34
- tnfr-4.5.0.dist-info/METADATA +0 -109
- tnfr-4.5.0.dist-info/RECORD +0 -28
- {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
- {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.0.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
|
|
6
|
-
from
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
#
|
|
92
|
+
# Helpers
|
|
22
93
|
# -------------------------
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
"""
|
|
1
|
+
"""Validation utilities."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import Iterable
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
23
|
-
if sv.get("mag", 0.0) > 1.0 +
|
|
24
|
-
raise ValueError("
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
"""
|
|
36
|
-
|
|
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)
|