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.
- tnfr/__init__.py +228 -49
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +106 -21
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +666 -512
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +2 -9
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +21 -7
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +42 -20
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +54 -20
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +0 -2
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +12 -0
- tnfr/config/__init__.pyi +8 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/{config.py → config/init.py} +11 -7
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +106 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +104 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +78 -24
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +1 -2
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +4 -12
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +9 -91
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +112 -634
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +201 -0
- tnfr/dynamics/aliases.py +22 -0
- tnfr/dynamics/coordination.py +343 -0
- tnfr/dynamics/dnfr.py +1936 -354
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +369 -75
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +8 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +56 -41
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +7 -7
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +54 -37
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +85 -38
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +19 -338
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +110 -30
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +1 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +55 -19
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +12 -10
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +99 -34
- tnfr/io.pyi +11 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +934 -294
- tnfr/metrics/common.py +1 -3
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +192 -34
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +707 -101
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +27 -13
- tnfr/metrics/glyph_timing.py +218 -38
- tnfr/metrics/reporting.py +22 -18
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +199 -25
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +53 -18
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +3 -7
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +148 -125
- tnfr/node.pyi +161 -0
- tnfr/observers.py +44 -30
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +14 -13
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +84 -52
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +86 -23
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +141 -57
- tnfr/presets.py +9 -54
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +259 -73
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +24 -17
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +55 -43
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +44 -267
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +3 -2
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +272 -82
- tnfr/trace.pyi +68 -0
- tnfr/types.py +345 -6
- tnfr/types.pyi +145 -0
- tnfr/utils/__init__.py +158 -0
- tnfr/utils/__init__.pyi +133 -0
- tnfr/utils/cache.py +755 -0
- tnfr/utils/cache.pyi +156 -0
- tnfr/{collections_utils.py → utils/data.py} +57 -90
- tnfr/utils/data.pyi +73 -0
- tnfr/utils/graph.py +87 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +746 -0
- tnfr/utils/init.pyi +85 -0
- tnfr/{json_utils.py → utils/io.py} +13 -18
- tnfr/utils/io.pyi +10 -0
- tnfr/utils/validators.py +130 -0
- tnfr/utils/validators.pyi +19 -0
- tnfr/validation/__init__.py +25 -0
- tnfr/validation/__init__.pyi +17 -0
- tnfr/validation/compatibility.py +59 -0
- tnfr/validation/compatibility.pyi +8 -0
- tnfr/validation/grammar.py +149 -0
- tnfr/validation/grammar.pyi +11 -0
- tnfr/validation/rules.py +194 -0
- tnfr/validation/rules.pyi +18 -0
- tnfr/validation/syntax.py +151 -0
- tnfr/validation/syntax.pyi +7 -0
- tnfr-6.0.0.dist-info/METADATA +135 -0
- tnfr-6.0.0.dist-info/RECORD +157 -0
- tnfr/graph_utils.py +0 -84
- tnfr/import_utils.py +0 -228
- tnfr/logging_utils.py +0 -116
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/sense.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
"""Sense calculations."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import TypeVar
|
|
5
|
-
from collections.abc import Iterable, Mapping
|
|
4
|
+
from typing import Any, Callable, TypeVar
|
|
5
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
6
6
|
import math
|
|
7
7
|
from collections import Counter
|
|
8
8
|
from itertools import tee
|
|
9
9
|
|
|
10
|
-
import networkx as nx
|
|
10
|
+
import networkx as nx
|
|
11
11
|
|
|
12
12
|
from .constants import get_aliases, get_graph_param
|
|
13
13
|
from .alias import get_attr
|
|
14
14
|
from .helpers.numeric import clamp01, kahan_sum_nd
|
|
15
|
-
from .
|
|
15
|
+
from .utils import get_numpy
|
|
16
16
|
from .callback_utils import CallbackEvent, callback_manager
|
|
17
17
|
from .glyph_history import (
|
|
18
18
|
ensure_history,
|
|
@@ -20,12 +20,13 @@ from .glyph_history import (
|
|
|
20
20
|
count_glyphs,
|
|
21
21
|
append_metric,
|
|
22
22
|
)
|
|
23
|
-
from .
|
|
23
|
+
from .config.constants import (
|
|
24
24
|
ANGLE_MAP,
|
|
25
25
|
GLYPHS_CANONICAL,
|
|
26
26
|
)
|
|
27
|
+
from .types import NodeId, SigmaVector, TNFRGraph
|
|
27
28
|
# -------------------------
|
|
28
|
-
# Canon:
|
|
29
|
+
# Canon: circular glyph order and angles
|
|
29
30
|
# -------------------------
|
|
30
31
|
|
|
31
32
|
GLYPH_UNITS: dict[str, complex] = {
|
|
@@ -45,7 +46,7 @@ __all__ = (
|
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
# -------------------------
|
|
48
|
-
#
|
|
49
|
+
# Basic utilities
|
|
49
50
|
# -------------------------
|
|
50
51
|
|
|
51
52
|
|
|
@@ -58,7 +59,7 @@ def _resolve_glyph(g: str, mapping: Mapping[str, T]) -> T:
|
|
|
58
59
|
try:
|
|
59
60
|
return mapping[g]
|
|
60
61
|
except KeyError as e: # pragma: no cover - small helper
|
|
61
|
-
raise KeyError(f"
|
|
62
|
+
raise KeyError(f"Unknown glyph: {g}") from e
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
def glyph_angle(g: str) -> float:
|
|
@@ -76,17 +77,19 @@ def glyph_unit(g: str) -> complex:
|
|
|
76
77
|
ALIAS_SI = get_aliases("SI")
|
|
77
78
|
ALIAS_EPI = get_aliases("EPI")
|
|
78
79
|
|
|
79
|
-
MODE_FUNCS = {
|
|
80
|
+
MODE_FUNCS: dict[str, Callable[[Mapping[str, Any]], float]] = {
|
|
80
81
|
"Si": lambda nd: clamp01(get_attr(nd, ALIAS_SI, 0.5)),
|
|
81
82
|
"EPI": lambda nd: max(0.0, get_attr(nd, ALIAS_EPI, 0.0)),
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
def _weight(nd, mode: str) -> float:
|
|
86
|
+
def _weight(nd: Mapping[str, Any], mode: str) -> float:
|
|
86
87
|
return MODE_FUNCS.get(mode, lambda _: 1.0)(nd)
|
|
87
88
|
|
|
88
89
|
|
|
89
|
-
def _node_weight(
|
|
90
|
+
def _node_weight(
|
|
91
|
+
nd: Mapping[str, Any], weight_mode: str
|
|
92
|
+
) -> tuple[str, float, complex] | None:
|
|
90
93
|
"""Return ``(glyph, weight, weighted_unit)`` or ``None`` if no glyph."""
|
|
91
94
|
g = last_glyph(nd)
|
|
92
95
|
if not g:
|
|
@@ -96,7 +99,7 @@ def _node_weight(nd, weight_mode: str) -> tuple[str, float, complex] | None:
|
|
|
96
99
|
return g, w, z
|
|
97
100
|
|
|
98
101
|
|
|
99
|
-
def _sigma_cfg(G):
|
|
102
|
+
def _sigma_cfg(G: TNFRGraph) -> dict[str, Any]:
|
|
100
103
|
return get_graph_param(G, "SIGMA", dict)
|
|
101
104
|
|
|
102
105
|
|
|
@@ -110,7 +113,7 @@ def _to_complex(val: complex | float | int) -> complex:
|
|
|
110
113
|
raise TypeError("values must be an iterable of real or complex numbers")
|
|
111
114
|
|
|
112
115
|
|
|
113
|
-
def _empty_sigma(fallback_angle: float) ->
|
|
116
|
+
def _empty_sigma(fallback_angle: float) -> SigmaVector:
|
|
114
117
|
"""Return an empty σ-vector with ``fallback_angle``.
|
|
115
118
|
|
|
116
119
|
Helps centralise the default structure returned when no values are
|
|
@@ -127,14 +130,14 @@ def _empty_sigma(fallback_angle: float) -> dict[str, float]:
|
|
|
127
130
|
|
|
128
131
|
|
|
129
132
|
# -------------------------
|
|
130
|
-
# σ
|
|
133
|
+
# σ per node and global σ
|
|
131
134
|
# -------------------------
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
def _sigma_from_iterable(
|
|
135
138
|
values: Iterable[complex | float | int] | complex | float | int,
|
|
136
139
|
fallback_angle: float = 0.0,
|
|
137
|
-
) ->
|
|
140
|
+
) -> SigmaVector:
|
|
138
141
|
"""Normalise vectors in the σ-plane.
|
|
139
142
|
|
|
140
143
|
``values`` may contain complex or real numbers; real inputs are promoted to
|
|
@@ -159,15 +162,15 @@ def _sigma_from_iterable(
|
|
|
159
162
|
mag = float(np.hypot(x, y))
|
|
160
163
|
ang = float(np.arctan2(y, x)) if mag > 0 else float(fallback_angle)
|
|
161
164
|
return {
|
|
162
|
-
"x": x,
|
|
163
|
-
"y": y,
|
|
164
|
-
"mag": mag,
|
|
165
|
-
"angle": ang,
|
|
166
|
-
"n": cnt,
|
|
165
|
+
"x": float(x),
|
|
166
|
+
"y": float(y),
|
|
167
|
+
"mag": float(mag),
|
|
168
|
+
"angle": float(ang),
|
|
169
|
+
"n": int(cnt),
|
|
167
170
|
}
|
|
168
171
|
cnt = 0
|
|
169
172
|
|
|
170
|
-
def pair_iter():
|
|
173
|
+
def pair_iter() -> Iterator[tuple[float, float]]:
|
|
171
174
|
nonlocal cnt
|
|
172
175
|
for val in iterator:
|
|
173
176
|
z = _to_complex(val)
|
|
@@ -188,24 +191,32 @@ def _sigma_from_iterable(
|
|
|
188
191
|
"y": float(y),
|
|
189
192
|
"mag": float(mag),
|
|
190
193
|
"angle": float(ang),
|
|
191
|
-
"n": cnt,
|
|
194
|
+
"n": int(cnt),
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
|
|
195
198
|
def _ema_update(
|
|
196
|
-
prev:
|
|
197
|
-
) ->
|
|
199
|
+
prev: SigmaVector, current: SigmaVector, alpha: float
|
|
200
|
+
) -> SigmaVector:
|
|
198
201
|
"""Exponential moving average update for σ vectors."""
|
|
199
202
|
x = (1 - alpha) * prev["x"] + alpha * current["x"]
|
|
200
203
|
y = (1 - alpha) * prev["y"] + alpha * current["y"]
|
|
201
204
|
mag = math.hypot(x, y)
|
|
202
205
|
ang = math.atan2(y, x)
|
|
203
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
"x": float(x),
|
|
208
|
+
"y": float(y),
|
|
209
|
+
"mag": float(mag),
|
|
210
|
+
"angle": float(ang),
|
|
211
|
+
"n": int(current["n"]),
|
|
212
|
+
}
|
|
204
213
|
|
|
205
214
|
|
|
206
215
|
def _sigma_from_nodes(
|
|
207
|
-
nodes: Iterable[
|
|
208
|
-
|
|
216
|
+
nodes: Iterable[Mapping[str, Any]],
|
|
217
|
+
weight_mode: str,
|
|
218
|
+
fallback_angle: float = 0.0,
|
|
219
|
+
) -> tuple[SigmaVector, list[tuple[str, float, complex]]]:
|
|
209
220
|
"""Aggregate weighted glyph vectors for ``nodes``.
|
|
210
221
|
|
|
211
222
|
Returns the aggregated σ vector and the list of ``(glyph, weight, vector)``
|
|
@@ -218,8 +229,8 @@ def _sigma_from_nodes(
|
|
|
218
229
|
|
|
219
230
|
|
|
220
231
|
def sigma_vector_node(
|
|
221
|
-
G, n, weight_mode: str | None = None
|
|
222
|
-
) ->
|
|
232
|
+
G: TNFRGraph, n: NodeId, weight_mode: str | None = None
|
|
233
|
+
) -> SigmaVector | None:
|
|
223
234
|
cfg = _sigma_cfg(G)
|
|
224
235
|
nd = G.nodes[n]
|
|
225
236
|
weight_mode = weight_mode or cfg.get("weight", "Si")
|
|
@@ -229,11 +240,12 @@ def sigma_vector_node(
|
|
|
229
240
|
g, w, _ = nws[0]
|
|
230
241
|
if sv["mag"] == 0:
|
|
231
242
|
sv["angle"] = glyph_angle(g)
|
|
232
|
-
sv
|
|
243
|
+
sv["glyph"] = g
|
|
244
|
+
sv["w"] = float(w)
|
|
233
245
|
return sv
|
|
234
246
|
|
|
235
247
|
|
|
236
|
-
def sigma_vector(dist:
|
|
248
|
+
def sigma_vector(dist: Mapping[str, float]) -> SigmaVector:
|
|
237
249
|
"""Compute Σ⃗ from a glyph distribution.
|
|
238
250
|
|
|
239
251
|
``dist`` may contain raw counts or proportions. All ``(glyph, weight)``
|
|
@@ -246,8 +258,8 @@ def sigma_vector(dist: dict[str, float]) -> dict[str, float]:
|
|
|
246
258
|
|
|
247
259
|
|
|
248
260
|
def sigma_vector_from_graph(
|
|
249
|
-
G:
|
|
250
|
-
) ->
|
|
261
|
+
G: TNFRGraph, weight_mode: str | None = None
|
|
262
|
+
) -> SigmaVector:
|
|
251
263
|
"""Global vector in the σ sense plane for a graph.
|
|
252
264
|
|
|
253
265
|
Parameters
|
|
@@ -264,7 +276,7 @@ def sigma_vector_from_graph(
|
|
|
264
276
|
"""
|
|
265
277
|
|
|
266
278
|
if not isinstance(G, nx.Graph):
|
|
267
|
-
raise TypeError("sigma_vector_from_graph
|
|
279
|
+
raise TypeError("sigma_vector_from_graph requires a networkx.Graph")
|
|
268
280
|
|
|
269
281
|
cfg = _sigma_cfg(G)
|
|
270
282
|
weight_mode = weight_mode or cfg.get("weight", "Si")
|
|
@@ -275,23 +287,23 @@ def sigma_vector_from_graph(
|
|
|
275
287
|
|
|
276
288
|
|
|
277
289
|
# -------------------------
|
|
278
|
-
#
|
|
290
|
+
# History / series
|
|
279
291
|
# -------------------------
|
|
280
292
|
|
|
281
293
|
|
|
282
|
-
def push_sigma_snapshot(G, t: float | None = None) -> None:
|
|
294
|
+
def push_sigma_snapshot(G: TNFRGraph, t: float | None = None) -> None:
|
|
283
295
|
cfg = _sigma_cfg(G)
|
|
284
296
|
if not cfg.get("enabled", True):
|
|
285
297
|
return
|
|
286
298
|
|
|
287
|
-
#
|
|
299
|
+
# Local history cache to avoid repeated lookups
|
|
288
300
|
hist = ensure_history(G)
|
|
289
301
|
key = cfg.get("history_key", "sigma_global")
|
|
290
302
|
|
|
291
303
|
weight_mode = cfg.get("weight", "Si")
|
|
292
304
|
sv = sigma_vector_from_graph(G, weight_mode)
|
|
293
305
|
|
|
294
|
-
#
|
|
306
|
+
# Optional exponential smoothing (EMA)
|
|
295
307
|
alpha = float(cfg.get("smooth", 0.0))
|
|
296
308
|
if alpha > 0 and hist.get(key):
|
|
297
309
|
sv = _ema_update(hist[key][-1], sv, alpha)
|
|
@@ -301,11 +313,11 @@ def push_sigma_snapshot(G, t: float | None = None) -> None:
|
|
|
301
313
|
|
|
302
314
|
append_metric(hist, key, sv)
|
|
303
315
|
|
|
304
|
-
#
|
|
316
|
+
# Glyph count per step (useful for the glyph rose)
|
|
305
317
|
counts = count_glyphs(G, last_only=True)
|
|
306
318
|
append_metric(hist, "sigma_counts", {"t": current_t, **counts})
|
|
307
319
|
|
|
308
|
-
#
|
|
320
|
+
# Optional per-node trajectory
|
|
309
321
|
if cfg.get("per_node", False):
|
|
310
322
|
per = hist.setdefault("sigma_per_node", {})
|
|
311
323
|
for n, nd in G.nodes(data=True):
|
|
@@ -317,11 +329,11 @@ def push_sigma_snapshot(G, t: float | None = None) -> None:
|
|
|
317
329
|
|
|
318
330
|
|
|
319
331
|
# -------------------------
|
|
320
|
-
#
|
|
332
|
+
# Register as an automatic callback (after_step)
|
|
321
333
|
# -------------------------
|
|
322
334
|
|
|
323
335
|
|
|
324
|
-
def register_sigma_callback(G) -> None:
|
|
336
|
+
def register_sigma_callback(G: TNFRGraph) -> None:
|
|
325
337
|
callback_manager.register_callback(
|
|
326
338
|
G,
|
|
327
339
|
event=CallbackEvent.AFTER_STEP.value,
|
|
@@ -330,7 +342,7 @@ def register_sigma_callback(G) -> None:
|
|
|
330
342
|
)
|
|
331
343
|
|
|
332
344
|
|
|
333
|
-
def sigma_rose(G, steps: int | None = None) -> dict[str, int]:
|
|
345
|
+
def sigma_rose(G: TNFRGraph, steps: int | None = None) -> dict[str, int]:
|
|
334
346
|
"""Histogram of glyphs in the last ``steps`` steps (or all)."""
|
|
335
347
|
hist = ensure_history(G)
|
|
336
348
|
counts = hist.get("sigma_counts", [])
|
tnfr/sense.pyi
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from .types import NodeId, SigmaVector, TNFRGraph
|
|
7
|
+
|
|
8
|
+
__all__: tuple[str, ...]
|
|
9
|
+
|
|
10
|
+
GLYPH_UNITS: dict[str, complex]
|
|
11
|
+
|
|
12
|
+
def glyph_angle(g: str) -> float: ...
|
|
13
|
+
|
|
14
|
+
def glyph_unit(g: str) -> complex: ...
|
|
15
|
+
|
|
16
|
+
def push_sigma_snapshot(G: TNFRGraph, t: Optional[float] = None) -> None: ...
|
|
17
|
+
|
|
18
|
+
def register_sigma_callback(G: TNFRGraph) -> None: ...
|
|
19
|
+
|
|
20
|
+
def sigma_rose(G: TNFRGraph, steps: Optional[int] = None) -> dict[str, int]: ...
|
|
21
|
+
|
|
22
|
+
def sigma_vector(dist: Mapping[str, float]) -> SigmaVector: ...
|
|
23
|
+
|
|
24
|
+
def sigma_vector_from_graph(
|
|
25
|
+
G: TNFRGraph, weight_mode: Optional[str] = None
|
|
26
|
+
) -> SigmaVector: ...
|
|
27
|
+
|
|
28
|
+
def sigma_vector_node(
|
|
29
|
+
G: TNFRGraph, n: NodeId, weight_mode: Optional[str] = None
|
|
30
|
+
) -> Optional[SigmaVector]: ...
|
tnfr/structural.py
CHANGED
|
@@ -1,20 +1,39 @@
|
|
|
1
1
|
"""Structural analysis."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
from typing import Iterable
|
|
5
|
-
import networkx as nx # type: ignore[import-untyped]
|
|
6
6
|
|
|
7
|
+
import networkx as nx
|
|
8
|
+
|
|
9
|
+
from .constants import EPI_PRIMARY, VF_PRIMARY, THETA_PRIMARY
|
|
7
10
|
from .dynamics import (
|
|
8
11
|
set_delta_nfr_hook,
|
|
9
12
|
dnfr_epi_vf_mixed,
|
|
10
13
|
)
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
13
|
-
|
|
14
|
+
from .types import DeltaNFRHook, NodeId, TNFRGraph
|
|
15
|
+
from .operators.definitions import (
|
|
16
|
+
Operator,
|
|
17
|
+
Emission,
|
|
18
|
+
Reception,
|
|
19
|
+
Coherence,
|
|
20
|
+
Dissonance,
|
|
21
|
+
Coupling,
|
|
22
|
+
Resonance,
|
|
23
|
+
Silence,
|
|
24
|
+
Expansion,
|
|
25
|
+
Contraction,
|
|
26
|
+
SelfOrganization,
|
|
27
|
+
Mutation,
|
|
28
|
+
Transition,
|
|
29
|
+
Recursivity,
|
|
30
|
+
)
|
|
31
|
+
from .operators.registry import OPERATORS
|
|
32
|
+
from .validation import validate_sequence
|
|
14
33
|
|
|
15
34
|
|
|
16
35
|
# ---------------------------------------------------------------------------
|
|
17
|
-
# 1)
|
|
36
|
+
# 1) NFR factory
|
|
18
37
|
# ---------------------------------------------------------------------------
|
|
19
38
|
|
|
20
39
|
|
|
@@ -24,9 +43,9 @@ def create_nfr(
|
|
|
24
43
|
epi: float = 0.0,
|
|
25
44
|
vf: float = 1.0,
|
|
26
45
|
theta: float = 0.0,
|
|
27
|
-
graph:
|
|
28
|
-
dnfr_hook=dnfr_epi_vf_mixed,
|
|
29
|
-
) -> tuple[
|
|
46
|
+
graph: TNFRGraph | None = None,
|
|
47
|
+
dnfr_hook: DeltaNFRHook = dnfr_epi_vf_mixed,
|
|
48
|
+
) -> tuple[TNFRGraph, str]:
|
|
30
49
|
"""Create a graph with an initialised NFR node.
|
|
31
50
|
|
|
32
51
|
Returns the tuple ``(G, name)`` for convenience.
|
|
@@ -44,278 +63,36 @@ def create_nfr(
|
|
|
44
63
|
return G, name
|
|
45
64
|
|
|
46
65
|
|
|
47
|
-
# ---------------------------------------------------------------------------
|
|
48
|
-
# 2) Operadores estructurales como API de primer orden
|
|
49
|
-
# ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class Operador:
|
|
53
|
-
"""Base class for TNFR operators.
|
|
54
|
-
|
|
55
|
-
Each operator defines ``name`` (ASCII identifier) and ``glyph``
|
|
56
|
-
(símbolo TNFR canónico). Calling an instance applies the corresponding
|
|
57
|
-
symbol to the node.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
name = "operador"
|
|
61
|
-
glyph = None # tipo: str
|
|
62
|
-
|
|
63
|
-
def __call__(self, G: nx.Graph, node, **kw) -> None:
|
|
64
|
-
if self.glyph is None:
|
|
65
|
-
raise NotImplementedError("Operador sin glyph asignado")
|
|
66
|
-
apply_glyph_with_grammar(G, [node], self.glyph, kw.get("window"))
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class Emision(Operador):
|
|
70
|
-
"""Aplicación del operador de emisión (símbolo ``AL``)."""
|
|
71
|
-
|
|
72
|
-
__slots__ = ()
|
|
73
|
-
name = "emision"
|
|
74
|
-
glyph = Glyph.AL.value
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class Recepcion(Operador):
|
|
78
|
-
"""Operador de recepción (símbolo ``EN``)."""
|
|
79
|
-
|
|
80
|
-
__slots__ = ()
|
|
81
|
-
name = "recepcion"
|
|
82
|
-
glyph = Glyph.EN.value
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class Coherencia(Operador):
|
|
86
|
-
"""Operador de coherencia (símbolo ``IL``)."""
|
|
87
|
-
|
|
88
|
-
__slots__ = ()
|
|
89
|
-
name = "coherencia"
|
|
90
|
-
glyph = Glyph.IL.value
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class Disonancia(Operador):
|
|
94
|
-
"""Operador de disonancia (símbolo ``OZ``)."""
|
|
95
|
-
|
|
96
|
-
__slots__ = ()
|
|
97
|
-
name = "disonancia"
|
|
98
|
-
glyph = Glyph.OZ.value
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class Acoplamiento(Operador):
|
|
102
|
-
"""Operador de acoplamiento (símbolo ``UM``)."""
|
|
103
|
-
|
|
104
|
-
__slots__ = ()
|
|
105
|
-
name = "acoplamiento"
|
|
106
|
-
glyph = Glyph.UM.value
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class Resonancia(Operador):
|
|
110
|
-
"""Operador de resonancia (símbolo ``RA``)."""
|
|
111
|
-
|
|
112
|
-
__slots__ = ()
|
|
113
|
-
name = "resonancia"
|
|
114
|
-
glyph = Glyph.RA.value
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class Silencio(Operador):
|
|
118
|
-
"""Operador de silencio (símbolo ``SHA``)."""
|
|
119
|
-
|
|
120
|
-
__slots__ = ()
|
|
121
|
-
name = "silencio"
|
|
122
|
-
glyph = Glyph.SHA.value
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
class Expansion(Operador):
|
|
126
|
-
"""Operador de expansión (símbolo ``VAL``)."""
|
|
127
|
-
|
|
128
|
-
__slots__ = ()
|
|
129
|
-
name = "expansion"
|
|
130
|
-
glyph = Glyph.VAL.value
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class Contraccion(Operador):
|
|
134
|
-
"""Operador de contracción (símbolo ``NUL``)."""
|
|
135
|
-
|
|
136
|
-
__slots__ = ()
|
|
137
|
-
name = "contraccion"
|
|
138
|
-
glyph = Glyph.NUL.value
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class Autoorganizacion(Operador):
|
|
142
|
-
"""Operador de autoorganización (símbolo ``THOL``)."""
|
|
143
|
-
|
|
144
|
-
__slots__ = ()
|
|
145
|
-
name = "autoorganizacion"
|
|
146
|
-
glyph = Glyph.THOL.value
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class Mutacion(Operador):
|
|
150
|
-
"""Operador de mutación (símbolo ``ZHIR``)."""
|
|
151
|
-
|
|
152
|
-
__slots__ = ()
|
|
153
|
-
name = "mutacion"
|
|
154
|
-
glyph = Glyph.ZHIR.value
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
class Transicion(Operador):
|
|
158
|
-
"""Operador de transición (símbolo ``NAV``)."""
|
|
159
|
-
|
|
160
|
-
__slots__ = ()
|
|
161
|
-
name = "transicion"
|
|
162
|
-
glyph = Glyph.NAV.value
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
class Recursividad(Operador):
|
|
166
|
-
"""Operador de recursividad (símbolo ``REMESH``)."""
|
|
167
|
-
|
|
168
|
-
__slots__ = ()
|
|
169
|
-
name = "recursividad"
|
|
170
|
-
glyph = Glyph.REMESH.value
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
OPERADORES: dict[str, type[Operador]] = {
|
|
174
|
-
Emision.name: Emision,
|
|
175
|
-
Recepcion.name: Recepcion,
|
|
176
|
-
Coherencia.name: Coherencia,
|
|
177
|
-
Disonancia.name: Disonancia,
|
|
178
|
-
Acoplamiento.name: Acoplamiento,
|
|
179
|
-
Resonancia.name: Resonancia,
|
|
180
|
-
Silencio.name: Silencio,
|
|
181
|
-
Expansion.name: Expansion,
|
|
182
|
-
Contraccion.name: Contraccion,
|
|
183
|
-
Autoorganizacion.name: Autoorganizacion,
|
|
184
|
-
Mutacion.name: Mutacion,
|
|
185
|
-
Transicion.name: Transicion,
|
|
186
|
-
Recursividad.name: Recursividad,
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
66
|
__all__ = (
|
|
191
67
|
"create_nfr",
|
|
192
|
-
"
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
68
|
+
"Operator",
|
|
69
|
+
"Emission",
|
|
70
|
+
"Reception",
|
|
71
|
+
"Coherence",
|
|
72
|
+
"Dissonance",
|
|
73
|
+
"Coupling",
|
|
74
|
+
"Resonance",
|
|
75
|
+
"Silence",
|
|
200
76
|
"Expansion",
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
77
|
+
"Contraction",
|
|
78
|
+
"SelfOrganization",
|
|
79
|
+
"Mutation",
|
|
80
|
+
"Transition",
|
|
81
|
+
"Recursivity",
|
|
82
|
+
"OPERATORS",
|
|
207
83
|
"validate_sequence",
|
|
208
84
|
"run_sequence",
|
|
209
85
|
)
|
|
210
|
-
# ---------------------------------------------------------------------------
|
|
211
|
-
# 3) Motor de secuencias + validador sintáctico
|
|
212
|
-
# ---------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
_INICIO_VALIDOS = {"emision", "recursividad"}
|
|
216
|
-
_TRAMO_INTERMEDIO = {"disonancia", "acoplamiento", "resonancia"}
|
|
217
|
-
_CIERRE_VALIDO = {"silencio", "transicion", "recursividad"}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def _validate_start(token: str) -> tuple[bool, str]:
|
|
221
|
-
"""Ensure the sequence begins with a valid structural operator."""
|
|
222
|
-
|
|
223
|
-
if not isinstance(token, str):
|
|
224
|
-
return False, "tokens must be str"
|
|
225
|
-
if token not in _INICIO_VALIDOS:
|
|
226
|
-
return False, "must start with emission or recursion"
|
|
227
|
-
return True, ""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _validate_intermediate(
|
|
231
|
-
found_recepcion: bool, found_coherencia: bool, seen_intermedio: bool
|
|
232
|
-
) -> tuple[bool, str]:
|
|
233
|
-
"""Check that the central TNFR segment is present."""
|
|
234
|
-
|
|
235
|
-
if not (found_recepcion and found_coherencia):
|
|
236
|
-
return False, "missing input→coherence segment"
|
|
237
|
-
if not seen_intermedio:
|
|
238
|
-
return False, "missing tension/coupling/resonance segment"
|
|
239
|
-
return True, ""
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def _validate_end(last_token: str, open_thol: bool) -> tuple[bool, str]:
|
|
243
|
-
"""Validate closing operator and any pending THOL blocks."""
|
|
244
|
-
|
|
245
|
-
if last_token not in _CIERRE_VALIDO:
|
|
246
|
-
return False, "sequence must end with silence/transition/recursion"
|
|
247
|
-
if open_thol:
|
|
248
|
-
return False, "THOL block without closure"
|
|
249
|
-
return True, ""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _validate_known_tokens(nombres_set: set[str]) -> tuple[bool, str]:
|
|
253
|
-
"""Ensure all tokens map to canonical operators."""
|
|
254
|
-
|
|
255
|
-
desconocidos = nombres_set - OPERADORES.keys()
|
|
256
|
-
if desconocidos:
|
|
257
|
-
return False, f"unknown tokens: {', '.join(desconocidos)}"
|
|
258
|
-
return True, ""
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def _validate_token_sequence(nombres: list[str]) -> tuple[bool, str]:
|
|
262
|
-
"""Validate token format and logical coherence in one pass."""
|
|
263
|
-
|
|
264
|
-
if not nombres:
|
|
265
|
-
return False, "empty sequence"
|
|
266
|
-
|
|
267
|
-
ok, msg = _validate_start(nombres[0])
|
|
268
|
-
if not ok:
|
|
269
|
-
return False, msg
|
|
270
|
-
|
|
271
|
-
nombres_set: set[str] = set()
|
|
272
|
-
found_recepcion = False
|
|
273
|
-
found_coherencia = False
|
|
274
|
-
seen_intermedio = False
|
|
275
|
-
open_thol = False
|
|
276
|
-
|
|
277
|
-
for n in nombres:
|
|
278
|
-
if not isinstance(n, str):
|
|
279
|
-
return False, "tokens must be str"
|
|
280
|
-
nombres_set.add(n)
|
|
281
|
-
|
|
282
|
-
if n == "recepcion" and not found_recepcion:
|
|
283
|
-
found_recepcion = True
|
|
284
|
-
elif found_recepcion and n == "coherencia" and not found_coherencia:
|
|
285
|
-
found_coherencia = True
|
|
286
|
-
elif found_coherencia and not seen_intermedio and n in _TRAMO_INTERMEDIO:
|
|
287
|
-
seen_intermedio = True
|
|
288
|
-
|
|
289
|
-
if n == "autoorganizacion":
|
|
290
|
-
open_thol = True
|
|
291
|
-
elif open_thol and n in {"silencio", "contraccion"}:
|
|
292
|
-
open_thol = False
|
|
293
|
-
|
|
294
|
-
ok, msg = _validate_known_tokens(nombres_set)
|
|
295
|
-
if not ok:
|
|
296
|
-
return False, msg
|
|
297
|
-
ok, msg = _validate_intermediate(found_recepcion, found_coherencia, seen_intermedio)
|
|
298
|
-
if not ok:
|
|
299
|
-
return False, msg
|
|
300
|
-
ok, msg = _validate_end(nombres[-1], open_thol)
|
|
301
|
-
if not ok:
|
|
302
|
-
return False, msg
|
|
303
|
-
return True, "ok"
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def validate_sequence(nombres: list[str]) -> tuple[bool, str]:
|
|
307
|
-
"""Validate minimal TNFR syntax rules."""
|
|
308
|
-
return _validate_token_sequence(nombres)
|
|
309
86
|
|
|
310
87
|
|
|
311
|
-
def run_sequence(G:
|
|
88
|
+
def run_sequence(G: TNFRGraph, node: NodeId, ops: Iterable[Operator]) -> None:
|
|
312
89
|
"""Execute a sequence of operators on ``node`` after validation."""
|
|
313
90
|
|
|
314
91
|
compute = G.graph.get("compute_delta_nfr")
|
|
315
92
|
ops_list = list(ops)
|
|
316
|
-
|
|
93
|
+
names = [op.name for op in ops_list]
|
|
317
94
|
|
|
318
|
-
ok, msg = validate_sequence(
|
|
95
|
+
ok, msg = validate_sequence(names)
|
|
319
96
|
if not ok:
|
|
320
97
|
raise ValueError(f"Invalid sequence: {msg}")
|
|
321
98
|
|