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/sense.py
CHANGED
|
@@ -1,200 +1,353 @@
|
|
|
1
|
+
"""Sense calculations."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
|
-
from typing import
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
from collections.abc import Iterable, 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 # type: ignore[import-untyped]
|
|
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 .import_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 .constants_glyphs import (
|
|
24
|
+
ANGLE_MAP,
|
|
25
|
+
GLYPHS_CANONICAL,
|
|
26
|
+
)
|
|
9
27
|
# -------------------------
|
|
10
|
-
# Canon: orden circular de
|
|
28
|
+
# Canon: orden circular de glyphs y ángulos
|
|
11
29
|
# -------------------------
|
|
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
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
31
|
+
GLYPH_UNITS: dict[str, complex] = {
|
|
32
|
+
g: complex(math.cos(a), math.sin(a)) for g, a in ANGLE_MAP.items()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
__all__ = (
|
|
36
|
+
"GLYPH_UNITS",
|
|
37
|
+
"glyph_angle",
|
|
38
|
+
"glyph_unit",
|
|
39
|
+
"sigma_vector_node",
|
|
40
|
+
"sigma_vector",
|
|
41
|
+
"sigma_vector_from_graph",
|
|
42
|
+
"push_sigma_snapshot",
|
|
43
|
+
"register_sigma_callback",
|
|
44
|
+
"sigma_rose",
|
|
45
|
+
)
|
|
40
46
|
|
|
41
47
|
# -------------------------
|
|
42
48
|
# Utilidades básicas
|
|
43
49
|
# -------------------------
|
|
44
50
|
|
|
51
|
+
|
|
52
|
+
T = TypeVar("T")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_glyph(g: str, mapping: Mapping[str, T]) -> T:
|
|
56
|
+
"""Return ``mapping[g]`` or raise ``KeyError`` with a standard message."""
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
return mapping[g]
|
|
60
|
+
except KeyError as e: # pragma: no cover - small helper
|
|
61
|
+
raise KeyError(f"Glyph desconocido: {g}") from e
|
|
62
|
+
|
|
63
|
+
|
|
45
64
|
def glyph_angle(g: str) -> float:
|
|
46
|
-
|
|
65
|
+
"""Return angle for glyph ``g``."""
|
|
66
|
+
|
|
67
|
+
return float(_resolve_glyph(g, ANGLE_MAP))
|
|
47
68
|
|
|
48
69
|
|
|
49
70
|
def glyph_unit(g: str) -> complex:
|
|
50
|
-
|
|
51
|
-
return complex(math.cos(a), math.sin(a))
|
|
71
|
+
"""Return unit vector for glyph ``g``."""
|
|
52
72
|
|
|
73
|
+
return _resolve_glyph(g, GLYPH_UNITS)
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
|
|
76
|
+
ALIAS_SI = get_aliases("SI")
|
|
77
|
+
ALIAS_EPI = get_aliases("EPI")
|
|
78
|
+
|
|
79
|
+
MODE_FUNCS = {
|
|
80
|
+
"Si": lambda nd: clamp01(get_attr(nd, ALIAS_SI, 0.5)),
|
|
81
|
+
"EPI": lambda nd: max(0.0, get_attr(nd, ALIAS_EPI, 0.0)),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _weight(nd, mode: str) -> float:
|
|
86
|
+
return MODE_FUNCS.get(mode, lambda _: 1.0)(nd)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _node_weight(nd, weight_mode: str) -> tuple[str, float, complex] | None:
|
|
90
|
+
"""Return ``(glyph, weight, weighted_unit)`` or ``None`` if no glyph."""
|
|
91
|
+
g = last_glyph(nd)
|
|
92
|
+
if not g:
|
|
93
|
+
return None
|
|
94
|
+
w = _weight(nd, weight_mode)
|
|
95
|
+
z = glyph_unit(g) * w # precompute weighted unit vector
|
|
96
|
+
return g, w, z
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _sigma_cfg(G):
|
|
100
|
+
return get_graph_param(G, "SIGMA", dict)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _to_complex(val: complex | float | int) -> complex:
|
|
104
|
+
"""Return ``val`` as complex, promoting real numbers."""
|
|
105
|
+
|
|
106
|
+
if isinstance(val, complex):
|
|
107
|
+
return val
|
|
108
|
+
if isinstance(val, (int, float)):
|
|
109
|
+
return complex(val, 0.0)
|
|
110
|
+
raise TypeError("values must be an iterable of real or complex numbers")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _empty_sigma(fallback_angle: float) -> dict[str, float]:
|
|
114
|
+
"""Return an empty σ-vector with ``fallback_angle``.
|
|
115
|
+
|
|
116
|
+
Helps centralise the default structure returned when no values are
|
|
117
|
+
available for σ calculations.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"x": 0.0,
|
|
122
|
+
"y": 0.0,
|
|
123
|
+
"mag": 0.0,
|
|
124
|
+
"angle": float(fallback_angle),
|
|
125
|
+
"n": 0,
|
|
126
|
+
}
|
|
61
127
|
|
|
62
128
|
|
|
63
|
-
|
|
64
129
|
# -------------------------
|
|
65
130
|
# σ por nodo y σ global
|
|
66
131
|
# -------------------------
|
|
67
132
|
|
|
68
|
-
|
|
133
|
+
|
|
134
|
+
def _sigma_from_iterable(
|
|
135
|
+
values: Iterable[complex | float | int] | complex | float | int,
|
|
136
|
+
fallback_angle: float = 0.0,
|
|
137
|
+
) -> dict[str, float]:
|
|
138
|
+
"""Normalise vectors in the σ-plane.
|
|
139
|
+
|
|
140
|
+
``values`` may contain complex or real numbers; real inputs are promoted to
|
|
141
|
+
complex with zero imaginary part. The returned dictionary includes the
|
|
142
|
+
number of processed values under the ``"n"`` key.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
if isinstance(values, Iterable) and not isinstance(values, (str, bytes, bytearray, Mapping)):
|
|
146
|
+
iterator = iter(values)
|
|
147
|
+
else:
|
|
148
|
+
iterator = iter((values,))
|
|
149
|
+
|
|
150
|
+
np = get_numpy()
|
|
151
|
+
if np is not None:
|
|
152
|
+
iterator, np_iter = tee(iterator)
|
|
153
|
+
arr = np.fromiter((_to_complex(v) for v in np_iter), dtype=np.complex128)
|
|
154
|
+
cnt = int(arr.size)
|
|
155
|
+
if cnt == 0:
|
|
156
|
+
return _empty_sigma(fallback_angle)
|
|
157
|
+
x = float(np.mean(arr.real))
|
|
158
|
+
y = float(np.mean(arr.imag))
|
|
159
|
+
mag = float(np.hypot(x, y))
|
|
160
|
+
ang = float(np.arctan2(y, x)) if mag > 0 else float(fallback_angle)
|
|
161
|
+
return {
|
|
162
|
+
"x": x,
|
|
163
|
+
"y": y,
|
|
164
|
+
"mag": mag,
|
|
165
|
+
"angle": ang,
|
|
166
|
+
"n": cnt,
|
|
167
|
+
}
|
|
168
|
+
cnt = 0
|
|
169
|
+
|
|
170
|
+
def pair_iter():
|
|
171
|
+
nonlocal cnt
|
|
172
|
+
for val in iterator:
|
|
173
|
+
z = _to_complex(val)
|
|
174
|
+
cnt += 1
|
|
175
|
+
yield (z.real, z.imag)
|
|
176
|
+
|
|
177
|
+
sum_x, sum_y = kahan_sum_nd(pair_iter(), dims=2)
|
|
178
|
+
|
|
179
|
+
if cnt == 0:
|
|
180
|
+
return _empty_sigma(fallback_angle)
|
|
181
|
+
|
|
182
|
+
x = sum_x / cnt
|
|
183
|
+
y = sum_y / cnt
|
|
184
|
+
mag = math.hypot(x, y)
|
|
185
|
+
ang = math.atan2(y, x) if mag > 0 else float(fallback_angle)
|
|
186
|
+
return {
|
|
187
|
+
"x": float(x),
|
|
188
|
+
"y": float(y),
|
|
189
|
+
"mag": float(mag),
|
|
190
|
+
"angle": float(ang),
|
|
191
|
+
"n": cnt,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _ema_update(
|
|
196
|
+
prev: dict[str, float], current: dict[str, float], alpha: float
|
|
197
|
+
) -> dict[str, float]:
|
|
198
|
+
"""Exponential moving average update for σ vectors."""
|
|
199
|
+
x = (1 - alpha) * prev["x"] + alpha * current["x"]
|
|
200
|
+
y = (1 - alpha) * prev["y"] + alpha * current["y"]
|
|
201
|
+
mag = math.hypot(x, y)
|
|
202
|
+
ang = math.atan2(y, x)
|
|
203
|
+
return {"x": x, "y": y, "mag": mag, "angle": ang, "n": current.get("n", 0)}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _sigma_from_nodes(
|
|
207
|
+
nodes: Iterable[dict], weight_mode: str, fallback_angle: float = 0.0
|
|
208
|
+
) -> tuple[dict[str, float], list[tuple[str, float, complex]]]:
|
|
209
|
+
"""Aggregate weighted glyph vectors for ``nodes``.
|
|
210
|
+
|
|
211
|
+
Returns the aggregated σ vector and the list of ``(glyph, weight, vector)``
|
|
212
|
+
triples used in the calculation.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
nws = [nw for nd in nodes if (nw := _node_weight(nd, weight_mode))]
|
|
216
|
+
sv = _sigma_from_iterable((nw[2] for nw in nws), fallback_angle)
|
|
217
|
+
return sv, nws
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def sigma_vector_node(
|
|
221
|
+
G, n, weight_mode: str | None = None
|
|
222
|
+
) -> dict[str, float] | None:
|
|
223
|
+
cfg = _sigma_cfg(G)
|
|
69
224
|
nd = G.nodes[n]
|
|
70
|
-
|
|
71
|
-
|
|
225
|
+
weight_mode = weight_mode or cfg.get("weight", "Si")
|
|
226
|
+
sv, nws = _sigma_from_nodes([nd], weight_mode)
|
|
227
|
+
if not nws:
|
|
72
228
|
return None
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
229
|
+
g, w, _ = nws[0]
|
|
230
|
+
if sv["mag"] == 0:
|
|
231
|
+
sv["angle"] = glyph_angle(g)
|
|
232
|
+
sv.update({"glyph": g, "w": float(w)})
|
|
233
|
+
return sv
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def sigma_vector(dist: dict[str, float]) -> dict[str, float]:
|
|
237
|
+
"""Compute Σ⃗ from a glyph distribution.
|
|
238
|
+
|
|
239
|
+
``dist`` may contain raw counts or proportions. All ``(glyph, weight)``
|
|
240
|
+
pairs are converted to vectors and passed to :func:`_sigma_from_iterable`.
|
|
241
|
+
The resulting vector includes the number of processed pairs under ``n``.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
vectors = (glyph_unit(g) * float(w) for g, w in dist.items())
|
|
245
|
+
return _sigma_from_iterable(vectors)
|
|
79
246
|
|
|
80
247
|
|
|
81
|
-
def
|
|
82
|
-
|
|
248
|
+
def sigma_vector_from_graph(
|
|
249
|
+
G: nx.Graph, weight_mode: str | None = None
|
|
250
|
+
) -> dict[str, float]:
|
|
251
|
+
"""Global vector in the σ sense plane for a graph.
|
|
83
252
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
G:
|
|
256
|
+
NetworkX graph with per-node states.
|
|
257
|
+
weight_mode:
|
|
258
|
+
How to weight each node ("Si", "EPI" or ``None`` for unit weight).
|
|
87
259
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
dict[str, float]
|
|
263
|
+
Cartesian components, magnitude and angle of the average vector.
|
|
91
264
|
"""
|
|
92
|
-
|
|
265
|
+
|
|
266
|
+
if not isinstance(G, nx.Graph):
|
|
267
|
+
raise TypeError("sigma_vector_from_graph requiere un networkx.Graph")
|
|
268
|
+
|
|
269
|
+
cfg = _sigma_cfg(G)
|
|
93
270
|
weight_mode = weight_mode or cfg.get("weight", "Si")
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if v is None:
|
|
99
|
-
continue
|
|
100
|
-
acc += complex(v["x"], v["y"])
|
|
101
|
-
cnt += 1
|
|
102
|
-
if cnt == 0:
|
|
103
|
-
return {"x": 1.0, "y": 0.0, "mag": 1.0, "angle": 0.0, "n": 0}
|
|
104
|
-
x, y = acc.real / max(1, cnt), acc.imag / max(1, cnt)
|
|
105
|
-
mag = math.hypot(x, y)
|
|
106
|
-
ang = math.atan2(y, x)
|
|
107
|
-
return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang), "n": cnt}
|
|
271
|
+
sv, _ = _sigma_from_nodes(
|
|
272
|
+
(nd for _, nd in G.nodes(data=True)), weight_mode
|
|
273
|
+
)
|
|
274
|
+
return sv
|
|
108
275
|
|
|
109
276
|
|
|
110
277
|
# -------------------------
|
|
111
278
|
# Historia / series
|
|
112
279
|
# -------------------------
|
|
113
280
|
|
|
281
|
+
|
|
114
282
|
def push_sigma_snapshot(G, t: float | None = None) -> None:
|
|
115
|
-
cfg = G
|
|
283
|
+
cfg = _sigma_cfg(G)
|
|
116
284
|
if not cfg.get("enabled", True):
|
|
117
285
|
return
|
|
286
|
+
|
|
287
|
+
# Cache local de la historia para evitar llamadas repetidas
|
|
118
288
|
hist = ensure_history(G)
|
|
119
289
|
key = cfg.get("history_key", "sigma_global")
|
|
120
290
|
|
|
121
|
-
|
|
122
|
-
sv =
|
|
291
|
+
weight_mode = cfg.get("weight", "Si")
|
|
292
|
+
sv = sigma_vector_from_graph(G, weight_mode)
|
|
123
293
|
|
|
124
294
|
# Suavizado exponencial (EMA) opcional
|
|
125
295
|
alpha = float(cfg.get("smooth", 0.0))
|
|
126
296
|
if alpha > 0 and hist.get(key):
|
|
127
|
-
|
|
128
|
-
x = (1-alpha)*prev["x"] + alpha*sv["x"]
|
|
129
|
-
y = (1-alpha)*prev["y"] + alpha*sv["y"]
|
|
130
|
-
mag = math.hypot(x, y)
|
|
131
|
-
ang = math.atan2(y, x)
|
|
132
|
-
sv = {"x": x, "y": y, "mag": mag, "angle": ang, "n": sv.get("n", 0)}
|
|
297
|
+
sv = _ema_update(hist[key][-1], sv, alpha)
|
|
133
298
|
|
|
134
|
-
|
|
299
|
+
current_t = float(G.graph.get("_t", 0.0) if t is None else t)
|
|
300
|
+
sv["t"] = current_t
|
|
135
301
|
|
|
136
|
-
hist
|
|
302
|
+
append_metric(hist, key, sv)
|
|
137
303
|
|
|
138
|
-
# Conteo de
|
|
139
|
-
counts =
|
|
140
|
-
|
|
141
|
-
g = last_glifo(G.nodes[n])
|
|
142
|
-
if g:
|
|
143
|
-
counts[g] += 1
|
|
144
|
-
hist.setdefault("sigma_counts", []).append({"t": sv["t"], **counts})
|
|
304
|
+
# Conteo de glyphs por paso (útil para rosa glífica)
|
|
305
|
+
counts = count_glyphs(G, last_only=True)
|
|
306
|
+
append_metric(hist, "sigma_counts", {"t": current_t, **counts})
|
|
145
307
|
|
|
146
308
|
# Trayectoria por nodo (opcional)
|
|
147
309
|
if cfg.get("per_node", False):
|
|
148
310
|
per = hist.setdefault("sigma_per_node", {})
|
|
149
|
-
for n in G.nodes():
|
|
150
|
-
|
|
151
|
-
g = last_glifo(nd)
|
|
311
|
+
for n, nd in G.nodes(data=True):
|
|
312
|
+
g = last_glyph(nd)
|
|
152
313
|
if not g:
|
|
153
314
|
continue
|
|
154
|
-
a = glyph_angle(g)
|
|
155
315
|
d = per.setdefault(n, [])
|
|
156
|
-
d.append({"t":
|
|
316
|
+
d.append({"t": current_t, "g": g, "angle": glyph_angle(g)})
|
|
157
317
|
|
|
158
318
|
|
|
159
319
|
# -------------------------
|
|
160
320
|
# Registro como callback automático (after_step)
|
|
161
321
|
# -------------------------
|
|
162
322
|
|
|
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
323
|
|
|
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
|
-
}
|
|
324
|
+
def register_sigma_callback(G) -> None:
|
|
325
|
+
callback_manager.register_callback(
|
|
326
|
+
G,
|
|
327
|
+
event=CallbackEvent.AFTER_STEP.value,
|
|
328
|
+
func=push_sigma_snapshot,
|
|
329
|
+
name="sigma_snapshot",
|
|
330
|
+
)
|
|
183
331
|
|
|
184
332
|
|
|
185
|
-
def sigma_rose(G, steps: int | None = None) ->
|
|
186
|
-
"""
|
|
187
|
-
hist = G
|
|
333
|
+
def sigma_rose(G, steps: int | None = None) -> dict[str, int]:
|
|
334
|
+
"""Histogram of glyphs in the last ``steps`` steps (or all)."""
|
|
335
|
+
hist = ensure_history(G)
|
|
188
336
|
counts = hist.get("sigma_counts", [])
|
|
189
337
|
if not counts:
|
|
190
338
|
return {g: 0 for g in GLYPHS_CANONICAL}
|
|
191
|
-
if steps is None
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
339
|
+
if steps is not None:
|
|
340
|
+
steps = int(steps)
|
|
341
|
+
if steps < 0:
|
|
342
|
+
raise ValueError("steps must be non-negative")
|
|
343
|
+
rows = (
|
|
344
|
+
counts if steps >= len(counts) else counts[-steps:]
|
|
345
|
+
) # noqa: E203
|
|
346
|
+
else:
|
|
347
|
+
rows = counts
|
|
348
|
+
counter = Counter()
|
|
349
|
+
for row in rows:
|
|
350
|
+
for k, v in row.items():
|
|
351
|
+
if k != "t":
|
|
352
|
+
counter[k] += int(v)
|
|
353
|
+
return {g: int(counter.get(g, 0)) for g in GLYPHS_CANONICAL}
|