tnfr 4.5.1__py3-none-any.whl → 6.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tnfr/__init__.py +270 -90
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +631 -0
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +732 -0
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +381 -0
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +89 -0
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +199 -0
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +322 -0
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +34 -0
- 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/init.py +36 -0
- 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 +228 -0
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +158 -0
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +102 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +16 -0
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +136 -0
- 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 +2315 -0
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +561 -0
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +34 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +216 -0
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +283 -0
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +320 -89
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +337 -0
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +23 -153
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +151 -0
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +88 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +214 -0
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +199 -0
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +311 -0
- tnfr/io.pyi +11 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +1469 -0
- tnfr/metrics/common.py +149 -0
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +259 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +840 -0
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +151 -0
- tnfr/metrics/glyph_timing.py +369 -0
- tnfr/metrics/reporting.py +152 -0
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +294 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +216 -0
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +105 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +255 -177
- tnfr/node.pyi +161 -0
- tnfr/observers.py +154 -150
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +135 -134
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +452 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +266 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +569 -0
- tnfr/presets.py +10 -23
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +440 -0
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +217 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +307 -142
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +69 -164
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +61 -0
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +520 -95
- tnfr/trace.pyi +68 -0
- tnfr/types.py +382 -17
- 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/utils/data.py +267 -0
- 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/utils/io.py +157 -0
- 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/cli.py +0 -322
- tnfr/config.py +0 -41
- 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/validators.py +0 -38
- tnfr-4.5.1.dist-info/METADATA +0 -221
- tnfr-4.5.1.dist-info/RECORD +0 -28
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/sense.py
CHANGED
|
@@ -1,200 +1,365 @@
|
|
|
1
|
+
"""Sense calculations."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
|
-
from typing import
|
|
4
|
+
from typing import Any, Callable, TypeVar
|
|
5
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
3
6
|
import math
|
|
4
7
|
from collections import Counter
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
from itertools import tee
|
|
9
|
+
|
|
10
|
+
import networkx as nx
|
|
11
|
+
|
|
12
|
+
from .constants import get_aliases, get_graph_param
|
|
13
|
+
from .alias import get_attr
|
|
14
|
+
from .helpers.numeric import clamp01, kahan_sum_nd
|
|
15
|
+
from .utils import get_numpy
|
|
16
|
+
from .callback_utils import CallbackEvent, callback_manager
|
|
17
|
+
from .glyph_history import (
|
|
18
|
+
ensure_history,
|
|
19
|
+
last_glyph,
|
|
20
|
+
count_glyphs,
|
|
21
|
+
append_metric,
|
|
22
|
+
)
|
|
23
|
+
from .config.constants import (
|
|
24
|
+
ANGLE_MAP,
|
|
25
|
+
GLYPHS_CANONICAL,
|
|
26
|
+
)
|
|
27
|
+
from .types import NodeId, SigmaVector, TNFRGraph
|
|
9
28
|
# -------------------------
|
|
10
|
-
# Canon:
|
|
29
|
+
# Canon: circular glyph order and angles
|
|
11
30
|
# -------------------------
|
|
12
|
-
GLYPHS_CANONICAL: List[str] = [
|
|
13
|
-
"A’L", # 0
|
|
14
|
-
"E’N", # 1
|
|
15
|
-
"I’L", # 2
|
|
16
|
-
"U’M", # 3
|
|
17
|
-
"R’A", # 4
|
|
18
|
-
"VA’L", # 5
|
|
19
|
-
"O’Z", # 6
|
|
20
|
-
"Z’HIR",# 7
|
|
21
|
-
"NA’V", # 8
|
|
22
|
-
"T’HOL",# 9
|
|
23
|
-
"NU’L", #10
|
|
24
|
-
"SH’A", #11
|
|
25
|
-
"RE’MESH" #12
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
_SIGMA_ANGLES: Dict[str, float] = {g: (2.0*math.pi * i / len(GLYPHS_CANONICAL)) for i, g in enumerate(GLYPHS_CANONICAL)}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
32
|
+
GLYPH_UNITS: dict[str, complex] = {
|
|
33
|
+
g: complex(math.cos(a), math.sin(a)) for g, a in ANGLE_MAP.items()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
__all__ = (
|
|
37
|
+
"GLYPH_UNITS",
|
|
38
|
+
"glyph_angle",
|
|
39
|
+
"glyph_unit",
|
|
40
|
+
"sigma_vector_node",
|
|
41
|
+
"sigma_vector",
|
|
42
|
+
"sigma_vector_from_graph",
|
|
43
|
+
"push_sigma_snapshot",
|
|
44
|
+
"register_sigma_callback",
|
|
45
|
+
"sigma_rose",
|
|
46
|
+
)
|
|
40
47
|
|
|
41
48
|
# -------------------------
|
|
42
|
-
#
|
|
49
|
+
# Basic utilities
|
|
43
50
|
# -------------------------
|
|
44
51
|
|
|
52
|
+
|
|
53
|
+
T = TypeVar("T")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _resolve_glyph(g: str, mapping: Mapping[str, T]) -> T:
|
|
57
|
+
"""Return ``mapping[g]`` or raise ``KeyError`` with a standard message."""
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
return mapping[g]
|
|
61
|
+
except KeyError as e: # pragma: no cover - small helper
|
|
62
|
+
raise KeyError(f"Unknown glyph: {g}") from e
|
|
63
|
+
|
|
64
|
+
|
|
45
65
|
def glyph_angle(g: str) -> float:
|
|
46
|
-
|
|
66
|
+
"""Return angle for glyph ``g``."""
|
|
67
|
+
|
|
68
|
+
return float(_resolve_glyph(g, ANGLE_MAP))
|
|
47
69
|
|
|
48
70
|
|
|
49
71
|
def glyph_unit(g: str) -> complex:
|
|
50
|
-
|
|
51
|
-
return complex(math.cos(a), math.sin(a))
|
|
72
|
+
"""Return unit vector for glyph ``g``."""
|
|
52
73
|
|
|
74
|
+
return _resolve_glyph(g, GLYPH_UNITS)
|
|
53
75
|
|
|
54
|
-
def _weight(G, n, mode: str) -> float:
|
|
55
|
-
nd = G.nodes[n]
|
|
56
|
-
if mode == "Si":
|
|
57
|
-
return clamp01(_get_attr(nd, ALIAS_SI, 0.5))
|
|
58
|
-
if mode == "EPI":
|
|
59
|
-
return max(0.0, float(_get_attr(nd, ALIAS_EPI, 0.0)))
|
|
60
|
-
return 1.0
|
|
61
76
|
|
|
77
|
+
ALIAS_SI = get_aliases("SI")
|
|
78
|
+
ALIAS_EPI = get_aliases("EPI")
|
|
62
79
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
MODE_FUNCS: dict[str, Callable[[Mapping[str, Any]], float]] = {
|
|
81
|
+
"Si": lambda nd: clamp01(get_attr(nd, ALIAS_SI, 0.5)),
|
|
82
|
+
"EPI": lambda nd: max(0.0, get_attr(nd, ALIAS_EPI, 0.0)),
|
|
83
|
+
}
|
|
67
84
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
|
|
86
|
+
def _weight(nd: Mapping[str, Any], mode: str) -> float:
|
|
87
|
+
return MODE_FUNCS.get(mode, lambda _: 1.0)(nd)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _node_weight(
|
|
91
|
+
nd: Mapping[str, Any], weight_mode: str
|
|
92
|
+
) -> tuple[str, float, complex] | None:
|
|
93
|
+
"""Return ``(glyph, weight, weighted_unit)`` or ``None`` if no glyph."""
|
|
94
|
+
g = last_glyph(nd)
|
|
95
|
+
if not g:
|
|
72
96
|
return None
|
|
73
|
-
w = _weight(
|
|
74
|
-
z = glyph_unit(g) * w
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ang = math.atan2(y, x) if mag > 0 else glyph_angle(g)
|
|
78
|
-
return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang), "glifo": g, "w": float(w)}
|
|
97
|
+
w = _weight(nd, weight_mode)
|
|
98
|
+
z = glyph_unit(g) * w # precompute weighted unit vector
|
|
99
|
+
return g, w, z
|
|
100
|
+
|
|
79
101
|
|
|
102
|
+
def _sigma_cfg(G: TNFRGraph) -> dict[str, Any]:
|
|
103
|
+
return get_graph_param(G, "SIGMA", dict)
|
|
80
104
|
|
|
81
|
-
def sigma_vector_global(G, weight_mode: str | None = None) -> Dict[str, float]:
|
|
82
|
-
"""Vector global del plano del sentido σ.
|
|
83
105
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- componentes (x, y), magnitud |σ| y ángulo arg(σ).
|
|
106
|
+
def _to_complex(val: complex | float | int) -> complex:
|
|
107
|
+
"""Return ``val`` as complex, promoting real numbers."""
|
|
87
108
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
(
|
|
109
|
+
if isinstance(val, complex):
|
|
110
|
+
return val
|
|
111
|
+
if isinstance(val, (int, float)):
|
|
112
|
+
return complex(val, 0.0)
|
|
113
|
+
raise TypeError("values must be an iterable of real or complex numbers")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _empty_sigma(fallback_angle: float) -> SigmaVector:
|
|
117
|
+
"""Return an empty σ-vector with ``fallback_angle``.
|
|
118
|
+
|
|
119
|
+
Helps centralise the default structure returned when no values are
|
|
120
|
+
available for σ calculations.
|
|
91
121
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"x": 0.0,
|
|
125
|
+
"y": 0.0,
|
|
126
|
+
"mag": 0.0,
|
|
127
|
+
"angle": float(fallback_angle),
|
|
128
|
+
"n": 0,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# -------------------------
|
|
133
|
+
# σ per node and global σ
|
|
134
|
+
# -------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _sigma_from_iterable(
|
|
138
|
+
values: Iterable[complex | float | int] | complex | float | int,
|
|
139
|
+
fallback_angle: float = 0.0,
|
|
140
|
+
) -> SigmaVector:
|
|
141
|
+
"""Normalise vectors in the σ-plane.
|
|
142
|
+
|
|
143
|
+
``values`` may contain complex or real numbers; real inputs are promoted to
|
|
144
|
+
complex with zero imaginary part. The returned dictionary includes the
|
|
145
|
+
number of processed values under the ``"n"`` key.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
if isinstance(values, Iterable) and not isinstance(values, (str, bytes, bytearray, Mapping)):
|
|
149
|
+
iterator = iter(values)
|
|
150
|
+
else:
|
|
151
|
+
iterator = iter((values,))
|
|
152
|
+
|
|
153
|
+
np = get_numpy()
|
|
154
|
+
if np is not None:
|
|
155
|
+
iterator, np_iter = tee(iterator)
|
|
156
|
+
arr = np.fromiter((_to_complex(v) for v in np_iter), dtype=np.complex128)
|
|
157
|
+
cnt = int(arr.size)
|
|
158
|
+
if cnt == 0:
|
|
159
|
+
return _empty_sigma(fallback_angle)
|
|
160
|
+
x = float(np.mean(arr.real))
|
|
161
|
+
y = float(np.mean(arr.imag))
|
|
162
|
+
mag = float(np.hypot(x, y))
|
|
163
|
+
ang = float(np.arctan2(y, x)) if mag > 0 else float(fallback_angle)
|
|
164
|
+
return {
|
|
165
|
+
"x": float(x),
|
|
166
|
+
"y": float(y),
|
|
167
|
+
"mag": float(mag),
|
|
168
|
+
"angle": float(ang),
|
|
169
|
+
"n": int(cnt),
|
|
170
|
+
}
|
|
95
171
|
cnt = 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
172
|
+
|
|
173
|
+
def pair_iter() -> Iterator[tuple[float, float]]:
|
|
174
|
+
nonlocal cnt
|
|
175
|
+
for val in iterator:
|
|
176
|
+
z = _to_complex(val)
|
|
177
|
+
cnt += 1
|
|
178
|
+
yield (z.real, z.imag)
|
|
179
|
+
|
|
180
|
+
sum_x, sum_y = kahan_sum_nd(pair_iter(), dims=2)
|
|
181
|
+
|
|
102
182
|
if cnt == 0:
|
|
103
|
-
return
|
|
104
|
-
|
|
183
|
+
return _empty_sigma(fallback_angle)
|
|
184
|
+
|
|
185
|
+
x = sum_x / cnt
|
|
186
|
+
y = sum_y / cnt
|
|
187
|
+
mag = math.hypot(x, y)
|
|
188
|
+
ang = math.atan2(y, x) if mag > 0 else float(fallback_angle)
|
|
189
|
+
return {
|
|
190
|
+
"x": float(x),
|
|
191
|
+
"y": float(y),
|
|
192
|
+
"mag": float(mag),
|
|
193
|
+
"angle": float(ang),
|
|
194
|
+
"n": int(cnt),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _ema_update(
|
|
199
|
+
prev: SigmaVector, current: SigmaVector, alpha: float
|
|
200
|
+
) -> SigmaVector:
|
|
201
|
+
"""Exponential moving average update for σ vectors."""
|
|
202
|
+
x = (1 - alpha) * prev["x"] + alpha * current["x"]
|
|
203
|
+
y = (1 - alpha) * prev["y"] + alpha * current["y"]
|
|
105
204
|
mag = math.hypot(x, y)
|
|
106
205
|
ang = math.atan2(y, x)
|
|
107
|
-
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
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _sigma_from_nodes(
|
|
216
|
+
nodes: Iterable[Mapping[str, Any]],
|
|
217
|
+
weight_mode: str,
|
|
218
|
+
fallback_angle: float = 0.0,
|
|
219
|
+
) -> tuple[SigmaVector, list[tuple[str, float, complex]]]:
|
|
220
|
+
"""Aggregate weighted glyph vectors for ``nodes``.
|
|
221
|
+
|
|
222
|
+
Returns the aggregated σ vector and the list of ``(glyph, weight, vector)``
|
|
223
|
+
triples used in the calculation.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
nws = [nw for nd in nodes if (nw := _node_weight(nd, weight_mode))]
|
|
227
|
+
sv = _sigma_from_iterable((nw[2] for nw in nws), fallback_angle)
|
|
228
|
+
return sv, nws
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def sigma_vector_node(
|
|
232
|
+
G: TNFRGraph, n: NodeId, weight_mode: str | None = None
|
|
233
|
+
) -> SigmaVector | None:
|
|
234
|
+
cfg = _sigma_cfg(G)
|
|
235
|
+
nd = G.nodes[n]
|
|
236
|
+
weight_mode = weight_mode or cfg.get("weight", "Si")
|
|
237
|
+
sv, nws = _sigma_from_nodes([nd], weight_mode)
|
|
238
|
+
if not nws:
|
|
239
|
+
return None
|
|
240
|
+
g, w, _ = nws[0]
|
|
241
|
+
if sv["mag"] == 0:
|
|
242
|
+
sv["angle"] = glyph_angle(g)
|
|
243
|
+
sv["glyph"] = g
|
|
244
|
+
sv["w"] = float(w)
|
|
245
|
+
return sv
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def sigma_vector(dist: Mapping[str, float]) -> SigmaVector:
|
|
249
|
+
"""Compute Σ⃗ from a glyph distribution.
|
|
250
|
+
|
|
251
|
+
``dist`` may contain raw counts or proportions. All ``(glyph, weight)``
|
|
252
|
+
pairs are converted to vectors and passed to :func:`_sigma_from_iterable`.
|
|
253
|
+
The resulting vector includes the number of processed pairs under ``n``.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
vectors = (glyph_unit(g) * float(w) for g, w in dist.items())
|
|
257
|
+
return _sigma_from_iterable(vectors)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def sigma_vector_from_graph(
|
|
261
|
+
G: TNFRGraph, weight_mode: str | None = None
|
|
262
|
+
) -> SigmaVector:
|
|
263
|
+
"""Global vector in the σ sense plane for a graph.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
G:
|
|
268
|
+
NetworkX graph with per-node states.
|
|
269
|
+
weight_mode:
|
|
270
|
+
How to weight each node ("Si", "EPI" or ``None`` for unit weight).
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
dict[str, float]
|
|
275
|
+
Cartesian components, magnitude and angle of the average vector.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
if not isinstance(G, nx.Graph):
|
|
279
|
+
raise TypeError("sigma_vector_from_graph requires a networkx.Graph")
|
|
280
|
+
|
|
281
|
+
cfg = _sigma_cfg(G)
|
|
282
|
+
weight_mode = weight_mode or cfg.get("weight", "Si")
|
|
283
|
+
sv, _ = _sigma_from_nodes(
|
|
284
|
+
(nd for _, nd in G.nodes(data=True)), weight_mode
|
|
285
|
+
)
|
|
286
|
+
return sv
|
|
108
287
|
|
|
109
288
|
|
|
110
289
|
# -------------------------
|
|
111
|
-
#
|
|
290
|
+
# History / series
|
|
112
291
|
# -------------------------
|
|
113
292
|
|
|
114
|
-
|
|
115
|
-
|
|
293
|
+
|
|
294
|
+
def push_sigma_snapshot(G: TNFRGraph, t: float | None = None) -> None:
|
|
295
|
+
cfg = _sigma_cfg(G)
|
|
116
296
|
if not cfg.get("enabled", True):
|
|
117
297
|
return
|
|
298
|
+
|
|
299
|
+
# Local history cache to avoid repeated lookups
|
|
118
300
|
hist = ensure_history(G)
|
|
119
301
|
key = cfg.get("history_key", "sigma_global")
|
|
120
302
|
|
|
121
|
-
|
|
122
|
-
sv =
|
|
303
|
+
weight_mode = cfg.get("weight", "Si")
|
|
304
|
+
sv = sigma_vector_from_graph(G, weight_mode)
|
|
123
305
|
|
|
124
|
-
#
|
|
306
|
+
# Optional exponential smoothing (EMA)
|
|
125
307
|
alpha = float(cfg.get("smooth", 0.0))
|
|
126
308
|
if alpha > 0 and hist.get(key):
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
hist
|
|
137
|
-
|
|
138
|
-
#
|
|
139
|
-
counts = Counter()
|
|
140
|
-
for n in G.nodes():
|
|
141
|
-
g = last_glifo(G.nodes[n])
|
|
142
|
-
if g:
|
|
143
|
-
counts[g] += 1
|
|
144
|
-
hist.setdefault("sigma_counts", []).append({"t": sv["t"], **counts})
|
|
145
|
-
|
|
146
|
-
# Trayectoria por nodo (opcional)
|
|
309
|
+
sv = _ema_update(hist[key][-1], sv, alpha)
|
|
310
|
+
|
|
311
|
+
current_t = float(G.graph.get("_t", 0.0) if t is None else t)
|
|
312
|
+
sv["t"] = current_t
|
|
313
|
+
|
|
314
|
+
append_metric(hist, key, sv)
|
|
315
|
+
|
|
316
|
+
# Glyph count per step (useful for the glyph rose)
|
|
317
|
+
counts = count_glyphs(G, last_only=True)
|
|
318
|
+
append_metric(hist, "sigma_counts", {"t": current_t, **counts})
|
|
319
|
+
|
|
320
|
+
# Optional per-node trajectory
|
|
147
321
|
if cfg.get("per_node", False):
|
|
148
322
|
per = hist.setdefault("sigma_per_node", {})
|
|
149
|
-
for n in G.nodes():
|
|
150
|
-
|
|
151
|
-
g = last_glifo(nd)
|
|
323
|
+
for n, nd in G.nodes(data=True):
|
|
324
|
+
g = last_glyph(nd)
|
|
152
325
|
if not g:
|
|
153
326
|
continue
|
|
154
|
-
a = glyph_angle(g)
|
|
155
327
|
d = per.setdefault(n, [])
|
|
156
|
-
d.append({"t":
|
|
328
|
+
d.append({"t": current_t, "g": g, "angle": glyph_angle(g)})
|
|
157
329
|
|
|
158
330
|
|
|
159
331
|
# -------------------------
|
|
160
|
-
#
|
|
332
|
+
# Register as an automatic callback (after_step)
|
|
161
333
|
# -------------------------
|
|
162
334
|
|
|
163
|
-
def register_sigma_callback(G) -> None:
|
|
164
|
-
register_callback(G, when="after_step", func=push_sigma_snapshot, name="sigma_snapshot")
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
# -------------------------
|
|
168
|
-
# Series de utilidad
|
|
169
|
-
# -------------------------
|
|
170
335
|
|
|
171
|
-
def
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
"t": [float(x.get("t", i)) for i, x in enumerate(xs)],
|
|
180
|
-
"angle": [float(x["angle"]) for x in xs],
|
|
181
|
-
"mag": [float(x["mag"]) for x in xs],
|
|
182
|
-
}
|
|
336
|
+
def register_sigma_callback(G: TNFRGraph) -> None:
|
|
337
|
+
callback_manager.register_callback(
|
|
338
|
+
G,
|
|
339
|
+
event=CallbackEvent.AFTER_STEP.value,
|
|
340
|
+
func=push_sigma_snapshot,
|
|
341
|
+
name="sigma_snapshot",
|
|
342
|
+
)
|
|
183
343
|
|
|
184
344
|
|
|
185
|
-
def sigma_rose(G, steps: int | None = None) ->
|
|
186
|
-
"""
|
|
187
|
-
hist = G
|
|
345
|
+
def sigma_rose(G: TNFRGraph, steps: int | None = None) -> dict[str, int]:
|
|
346
|
+
"""Histogram of glyphs in the last ``steps`` steps (or all)."""
|
|
347
|
+
hist = ensure_history(G)
|
|
188
348
|
counts = hist.get("sigma_counts", [])
|
|
189
349
|
if not counts:
|
|
190
350
|
return {g: 0 for g in GLYPHS_CANONICAL}
|
|
191
|
-
if steps is None
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
351
|
+
if steps is not None:
|
|
352
|
+
steps = int(steps)
|
|
353
|
+
if steps < 0:
|
|
354
|
+
raise ValueError("steps must be non-negative")
|
|
355
|
+
rows = (
|
|
356
|
+
counts if steps >= len(counts) else counts[-steps:]
|
|
357
|
+
) # noqa: E203
|
|
358
|
+
else:
|
|
359
|
+
rows = counts
|
|
360
|
+
counter = Counter()
|
|
361
|
+
for row in rows:
|
|
362
|
+
for k, v in row.items():
|
|
363
|
+
if k != "t":
|
|
364
|
+
counter[k] += int(v)
|
|
365
|
+
return {g: int(counter.get(g, 0)) for g in GLYPHS_CANONICAL}
|
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]: ...
|