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/observers.py
CHANGED
|
@@ -1,161 +1,159 @@
|
|
|
1
|
-
"""
|
|
2
|
-
observers.py — TNFR canónica
|
|
1
|
+
"""Observer management."""
|
|
3
2
|
|
|
4
|
-
Observadores y métricas auxiliares.
|
|
5
|
-
"""
|
|
6
3
|
from __future__ import annotations
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
import
|
|
4
|
+
from functools import partial
|
|
5
|
+
import statistics
|
|
6
|
+
from statistics import StatisticsError, pvariance
|
|
7
|
+
|
|
8
|
+
from .constants import get_aliases
|
|
9
|
+
from .alias import get_attr
|
|
10
|
+
from .helpers.numeric import angle_diff
|
|
11
|
+
from .callback_utils import CallbackEvent, callback_manager
|
|
12
|
+
from .glyph_history import (
|
|
13
|
+
ensure_history,
|
|
14
|
+
count_glyphs,
|
|
15
|
+
append_metric,
|
|
16
|
+
)
|
|
17
|
+
from .collections_utils import normalize_counter, mix_groups
|
|
18
|
+
from .constants_glyphs import GLYPH_GROUPS
|
|
19
|
+
from .gamma import kuramoto_R_psi
|
|
20
|
+
from .logging_utils import get_logger
|
|
21
|
+
from .import_utils import get_numpy
|
|
22
|
+
from .metrics.common import compute_coherence
|
|
23
|
+
from .validators import validate_window
|
|
24
|
+
|
|
25
|
+
ALIAS_THETA = get_aliases("THETA")
|
|
26
|
+
|
|
27
|
+
__all__ = (
|
|
28
|
+
"attach_standard_observer",
|
|
29
|
+
"kuramoto_metrics",
|
|
30
|
+
"phase_sync",
|
|
31
|
+
"kuramoto_order",
|
|
32
|
+
"glyph_load",
|
|
33
|
+
"wbar",
|
|
34
|
+
"DEFAULT_GLYPH_LOAD_SPAN",
|
|
35
|
+
"DEFAULT_WBAR_SPAN",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
logger = get_logger(__name__)
|
|
40
|
+
|
|
41
|
+
DEFAULT_GLYPH_LOAD_SPAN = 50
|
|
42
|
+
DEFAULT_WBAR_SPAN = 25
|
|
43
|
+
|
|
10
44
|
|
|
11
|
-
from .constants import ALIAS_DNFR, ALIAS_EPI, ALIAS_THETA, ALIAS_dEPI
|
|
12
|
-
from .helpers import _get_attr, list_mean, register_callback
|
|
13
45
|
|
|
14
46
|
# -------------------------
|
|
15
47
|
# Observador estándar Γ(R)
|
|
16
48
|
# -------------------------
|
|
17
|
-
def _std_log(
|
|
18
|
-
"""
|
|
19
|
-
h = G
|
|
20
|
-
h
|
|
49
|
+
def _std_log(kind: str, G, ctx: dict):
|
|
50
|
+
"""Store compact events in ``history['events']``."""
|
|
51
|
+
h = ensure_history(G)
|
|
52
|
+
append_metric(h, "events", (kind, dict(ctx)))
|
|
21
53
|
|
|
22
|
-
def std_before(G, ctx):
|
|
23
|
-
_std_log(G, "before", ctx)
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
_std_log
|
|
55
|
+
_STD_CALLBACKS = {
|
|
56
|
+
CallbackEvent.BEFORE_STEP.value: partial(_std_log, "before"),
|
|
57
|
+
CallbackEvent.AFTER_STEP.value: partial(_std_log, "after"),
|
|
58
|
+
CallbackEvent.ON_REMESH.value: partial(_std_log, "remesh"),
|
|
59
|
+
}
|
|
27
60
|
|
|
28
|
-
def std_on_remesh(G, ctx):
|
|
29
|
-
_std_log(G, "remesh", ctx)
|
|
30
61
|
|
|
31
62
|
def attach_standard_observer(G):
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
63
|
+
"""Register standard callbacks: before_step, after_step, on_remesh."""
|
|
64
|
+
if G.graph.get("_STD_OBSERVER"):
|
|
65
|
+
return G
|
|
66
|
+
for event, fn in _STD_CALLBACKS.items():
|
|
67
|
+
callback_manager.register_callback(G, event, fn)
|
|
68
|
+
G.graph["_STD_OBSERVER"] = "attached"
|
|
37
69
|
return G
|
|
38
70
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
|
|
72
|
+
def _ensure_nodes(G) -> bool:
|
|
73
|
+
"""Return ``True`` when the graph has nodes."""
|
|
74
|
+
return bool(G.number_of_nodes())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def kuramoto_metrics(G) -> tuple[float, float]:
|
|
78
|
+
"""Return Kuramoto order ``R`` and mean phase ``ψ``.
|
|
79
|
+
|
|
80
|
+
Delegates to :func:`kuramoto_R_psi` and performs the computation exactly
|
|
81
|
+
once per invocation.
|
|
82
|
+
"""
|
|
83
|
+
return kuramoto_R_psi(G)
|
|
44
84
|
|
|
45
85
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
Y = [math.sin(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes()]
|
|
49
|
-
if not X:
|
|
86
|
+
def phase_sync(G, R: float | None = None, psi: float | None = None) -> float:
|
|
87
|
+
if not _ensure_nodes(G):
|
|
50
88
|
return 1.0
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- math.pi
|
|
61
|
-
)
|
|
62
|
-
for n in G.nodes()
|
|
63
|
-
]
|
|
64
|
-
)
|
|
65
|
-
if len(X) > 1
|
|
66
|
-
else 0.0
|
|
89
|
+
if R is None or psi is None:
|
|
90
|
+
R_calc, psi_calc = kuramoto_metrics(G)
|
|
91
|
+
if R is None:
|
|
92
|
+
R = R_calc
|
|
93
|
+
if psi is None:
|
|
94
|
+
psi = psi_calc
|
|
95
|
+
diffs = (
|
|
96
|
+
angle_diff(get_attr(data, ALIAS_THETA, 0.0), psi)
|
|
97
|
+
for _, data in G.nodes(data=True)
|
|
67
98
|
)
|
|
99
|
+
# Try NumPy for a vectorised population variance
|
|
100
|
+
np = get_numpy()
|
|
101
|
+
if np is not None:
|
|
102
|
+
arr = np.fromiter(diffs, dtype=float)
|
|
103
|
+
var = float(np.var(arr)) if arr.size else 0.0
|
|
104
|
+
else:
|
|
105
|
+
try:
|
|
106
|
+
var = pvariance(diffs)
|
|
107
|
+
except StatisticsError:
|
|
108
|
+
var = 0.0
|
|
68
109
|
return 1.0 / (1.0 + var)
|
|
69
110
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
111
|
+
|
|
112
|
+
def kuramoto_order(
|
|
113
|
+
G, R: float | None = None, psi: float | None = None
|
|
114
|
+
) -> float:
|
|
115
|
+
"""R in [0,1], 1 means perfectly aligned phases."""
|
|
116
|
+
if not _ensure_nodes(G):
|
|
75
117
|
return 1.0
|
|
76
|
-
R
|
|
118
|
+
if R is None or psi is None:
|
|
119
|
+
R, psi = kuramoto_metrics(G)
|
|
77
120
|
return float(R)
|
|
78
121
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
122
|
+
|
|
123
|
+
def glyph_load(G, window: int | None = None) -> dict:
|
|
124
|
+
"""Return distribution of glyphs applied in the network.
|
|
125
|
+
|
|
126
|
+
- ``window``: if provided, count only the last ``window`` events per node;
|
|
127
|
+
otherwise use :data:`DEFAULT_GLYPH_LOAD_SPAN`.
|
|
128
|
+
Returns a dict with proportions per glyph and useful aggregates.
|
|
83
129
|
"""
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
seq = seq[-window:]
|
|
93
|
-
total.update(seq)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
count = sum(total.values())
|
|
130
|
+
if window == 0:
|
|
131
|
+
return {"_count": 0}
|
|
132
|
+
if window is None:
|
|
133
|
+
window_int = DEFAULT_GLYPH_LOAD_SPAN
|
|
134
|
+
else:
|
|
135
|
+
window_int = validate_window(window, positive=True)
|
|
136
|
+
total = count_glyphs(G, window=window_int, last_only=(window_int == 1))
|
|
137
|
+
dist, count = normalize_counter(total)
|
|
97
138
|
if count == 0:
|
|
98
139
|
return {"_count": 0}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Proporciones por glifo
|
|
102
|
-
dist = {k: v / count for k, v in total.items()}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# Agregados conceptuales (puedes ajustar categorías)
|
|
106
|
-
estabilizadores = ["I’L", "R’A", "U’M", "SH’A"]
|
|
107
|
-
disruptivos = ["O’Z", "Z’HIR", "NA’V", "T’HOL"]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
dist["_estabilizadores"] = sum(dist.get(k, 0.0) for k in estabilizadores)
|
|
111
|
-
dist["_disruptivos"] = sum(dist.get(k, 0.0) for k in disruptivos)
|
|
140
|
+
dist = mix_groups(dist, GLYPH_GROUPS)
|
|
112
141
|
dist["_count"] = count
|
|
113
142
|
return dist
|
|
114
143
|
|
|
115
|
-
def sigma_vector(G, window: int | None = None) -> dict:
|
|
116
|
-
"""Vector de sentido Σ⃗ a partir de la distribución glífica reciente.
|
|
117
|
-
Devuelve dict con x, y, mag (0..1) y angle (rad)."""
|
|
118
|
-
# Distribución glífica (proporciones)
|
|
119
|
-
dist = carga_glifica(G, window=window)
|
|
120
|
-
if not dist or dist.get("_count", 0) == 0:
|
|
121
|
-
return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0}
|
|
122
|
-
|
|
123
|
-
# Mapeo polar de glifos principales en el plano de sentido
|
|
124
|
-
# (ordenado estabilización→expansión→acoplamiento→silencio→disonancia→mutación→transición→autoorg.)
|
|
125
|
-
angles = {
|
|
126
|
-
"I’L": 0.0,
|
|
127
|
-
"R’A": math.pi/4,
|
|
128
|
-
"U’M": math.pi/2,
|
|
129
|
-
"SH’A": 3*math.pi/4,
|
|
130
|
-
"O’Z": math.pi,
|
|
131
|
-
"Z’HIR": 5*math.pi/4,
|
|
132
|
-
"NA’V": 3*math.pi/2,
|
|
133
|
-
"T’HOL": 7*math.pi/4,
|
|
134
|
-
}
|
|
135
|
-
# Normaliza solo sobre glifos mapeados
|
|
136
|
-
total = sum(dist.get(k, 0.0) for k in angles.keys())
|
|
137
|
-
if total <= 0:
|
|
138
|
-
return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0}
|
|
139
|
-
|
|
140
|
-
x = 0.0
|
|
141
|
-
y = 0.0
|
|
142
|
-
for k, a in angles.items():
|
|
143
|
-
p = dist.get(k, 0.0) / total
|
|
144
|
-
x += p * math.cos(a)
|
|
145
|
-
y += p * math.sin(a)
|
|
146
|
-
|
|
147
|
-
mag = (x*x + y*y) ** 0.5
|
|
148
|
-
ang = math.atan2(y, x)
|
|
149
|
-
return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang)}
|
|
150
144
|
|
|
151
145
|
def wbar(G, window: int | None = None) -> float:
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
"""Return W̄ = mean of ``C(t)`` over a recent window.
|
|
147
|
+
|
|
148
|
+
Uses :func:`ensure_history` to obtain ``G.graph['history']`` and falls back
|
|
149
|
+
to the instantaneous coherence when ``"C_steps"`` is missing or empty.
|
|
150
|
+
"""
|
|
151
|
+
hist = ensure_history(G)
|
|
152
|
+
cs = list(hist.get("C_steps", []))
|
|
155
153
|
if not cs:
|
|
156
154
|
# fallback: coherencia instantánea
|
|
157
|
-
return
|
|
158
|
-
if window is None
|
|
159
|
-
|
|
160
|
-
w = min(len(cs),
|
|
161
|
-
return float(
|
|
155
|
+
return compute_coherence(G)
|
|
156
|
+
w_param = DEFAULT_WBAR_SPAN if window is None else window
|
|
157
|
+
w = validate_window(w_param, positive=True)
|
|
158
|
+
w = min(len(cs), w)
|
|
159
|
+
return float(statistics.fmean(cs[-w:]))
|
tnfr/ontosim.py
CHANGED
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Módulo de orquestación mínima que encadena:
|
|
5
|
-
ΔNFR (campo) → Si → glifos → ecuación nodal → clamps → U’M → observadores → RE’MESH
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
import networkx as nx
|
|
9
|
-
import math
|
|
10
|
-
import random
|
|
1
|
+
"""Orchestrate the canonical simulation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
11
4
|
from collections import deque
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .callback_utils import CallbackEvent
|
|
8
|
+
from .constants import METRIC_DEFAULTS, inject_defaults, get_param
|
|
14
9
|
from .dynamics import step as _step, run as _run
|
|
15
10
|
from .dynamics import default_compute_delta_nfr
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
from .initialization import init_node_attrs
|
|
12
|
+
from .glyph_history import append_metric
|
|
13
|
+
from .import_utils import cached_import
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
16
|
+
import networkx as nx # type: ignore[import-untyped]
|
|
17
|
+
|
|
18
|
+
# API de alto nivel
|
|
19
|
+
__all__ = ("preparar_red", "step", "run")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def preparar_red(
|
|
23
|
+
G: "nx.Graph",
|
|
24
|
+
*,
|
|
25
|
+
init_attrs: bool = True,
|
|
26
|
+
override_defaults: bool = False,
|
|
27
|
+
**overrides,
|
|
28
|
+
) -> "nx.Graph":
|
|
29
|
+
"""Prepare ``G`` for simulation.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
init_attrs:
|
|
34
|
+
Run ``init_node_attrs`` when ``True`` (default), leaving node
|
|
35
|
+
attributes untouched when ``False``.
|
|
36
|
+
override_defaults:
|
|
37
|
+
If ``True``, :func:`inject_defaults` overwrites existing entries.
|
|
38
|
+
**overrides:
|
|
39
|
+
Parameters applied after the defaults phase.
|
|
40
|
+
"""
|
|
41
|
+
inject_defaults(G, override=override_defaults)
|
|
42
|
+
if overrides:
|
|
22
43
|
from .constants import merge_overrides
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
|
|
45
|
+
merge_overrides(G, **overrides)
|
|
46
|
+
# Inicializaciones blandas
|
|
47
|
+
ph_len = int(
|
|
48
|
+
G.graph.get(
|
|
49
|
+
"PHASE_HISTORY_MAXLEN", METRIC_DEFAULTS["PHASE_HISTORY_MAXLEN"]
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
hist_keys = [
|
|
53
|
+
"C_steps",
|
|
54
|
+
"stable_frac",
|
|
55
|
+
"phase_sync",
|
|
56
|
+
"kuramoto_R",
|
|
57
|
+
"sense_sigma_x",
|
|
58
|
+
"sense_sigma_y",
|
|
59
|
+
"sense_sigma_mag",
|
|
60
|
+
"sense_sigma_angle",
|
|
61
|
+
"iota",
|
|
62
|
+
"glyph_load_estab",
|
|
63
|
+
"glyph_load_disr",
|
|
64
|
+
"Si_mean",
|
|
65
|
+
"Si_hi_frac",
|
|
66
|
+
"Si_lo_frac",
|
|
67
|
+
"W_bar",
|
|
68
|
+
"phase_kG",
|
|
69
|
+
"phase_kL",
|
|
70
|
+
]
|
|
71
|
+
history = {k: [] for k in hist_keys}
|
|
72
|
+
history.update(
|
|
73
|
+
{
|
|
74
|
+
"phase_state": deque(maxlen=ph_len),
|
|
75
|
+
"phase_R": deque(maxlen=ph_len),
|
|
76
|
+
"phase_disr": deque(maxlen=ph_len),
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
G.graph.setdefault("history", history)
|
|
80
|
+
# Memoria global de REMESH
|
|
81
|
+
tau = int(get_param(G, "REMESH_TAU_GLOBAL"))
|
|
48
82
|
maxlen = max(2 * tau + 5, 64)
|
|
49
83
|
G.graph.setdefault("_epi_hist", deque(maxlen=maxlen))
|
|
50
|
-
# Auto-attach del observador estándar si se pide
|
|
51
|
-
if G.graph.get("ATTACH_STD_OBSERVER", False):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
attach_standard_observer
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
elif vf_mode == "normal":
|
|
108
|
-
vf_rng = random.Random(seed * 1000003 + idx)
|
|
109
|
-
# normal truncada simple (rechazo)
|
|
110
|
-
for _ in range(16):
|
|
111
|
-
cand = vf_rng.normalvariate(vf_mean, vf_std)
|
|
112
|
-
if vf_min_lim <= cand <= vf_max_lim:
|
|
113
|
-
vf = cand
|
|
114
|
-
break
|
|
115
|
-
else:
|
|
116
|
-
# fallback: clamp del último candidato
|
|
117
|
-
vf = min(max(vf_rng.normalvariate(vf_mean, vf_std), vf_min_lim), vf_max_lim)
|
|
118
|
-
else:
|
|
119
|
-
# fallback: conserva si existe, si no 0.5
|
|
120
|
-
vf = float(nd.get("νf", 0.5))
|
|
121
|
-
|
|
122
|
-
if clamp_to_limits:
|
|
123
|
-
vf = min(max(vf, vf_min_lim), vf_max_lim)
|
|
124
|
-
|
|
125
|
-
nd["νf"] = float(vf)
|
|
126
|
-
|
|
127
|
-
return G
|
|
128
|
-
|
|
129
|
-
def step(G: nx.Graph, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool = True) -> None:
|
|
130
|
-
_step(G, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs)
|
|
131
|
-
|
|
132
|
-
def run(G: nx.Graph, steps: int, *, dt: float | None = None, use_Si: bool = True, apply_glyphs: bool = True) -> None:
|
|
133
|
-
_run(G, steps=steps, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs)
|
|
134
|
-
|
|
135
|
-
# Helper rápido para pruebas manuales
|
|
136
|
-
if __name__ == "__main__":
|
|
137
|
-
G = nx.erdos_renyi_graph(30, 0.15)
|
|
138
|
-
preparar_red(G)
|
|
139
|
-
run(G, 100)
|
|
140
|
-
print("C(t) muestras:", G.graph["history"]["C_steps"][-5:])
|
|
84
|
+
# Auto-attach del observador estándar si se pide
|
|
85
|
+
if G.graph.get("ATTACH_STD_OBSERVER", False):
|
|
86
|
+
attach_standard_observer = cached_import(
|
|
87
|
+
"tnfr.observers",
|
|
88
|
+
"attach_standard_observer",
|
|
89
|
+
)
|
|
90
|
+
if attach_standard_observer is not None:
|
|
91
|
+
attach_standard_observer(G)
|
|
92
|
+
else:
|
|
93
|
+
append_metric(
|
|
94
|
+
G.graph,
|
|
95
|
+
"_callback_errors",
|
|
96
|
+
{"event": "attach_std_observer", "error": "ImportError"},
|
|
97
|
+
)
|
|
98
|
+
# Hook explícito para ΔNFR (se puede sustituir luego con
|
|
99
|
+
# dynamics.set_delta_nfr_hook)
|
|
100
|
+
G.graph.setdefault("compute_delta_nfr", default_compute_delta_nfr)
|
|
101
|
+
G.graph.setdefault("_dnfr_hook_name", "default_compute_delta_nfr")
|
|
102
|
+
# Callbacks Γ(R): before_step / after_step / on_remesh
|
|
103
|
+
G.graph.setdefault(
|
|
104
|
+
"callbacks",
|
|
105
|
+
{
|
|
106
|
+
CallbackEvent.BEFORE_STEP.value: [],
|
|
107
|
+
CallbackEvent.AFTER_STEP.value: [],
|
|
108
|
+
CallbackEvent.ON_REMESH.value: [],
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
G.graph.setdefault(
|
|
112
|
+
"_CALLBACKS_DOC",
|
|
113
|
+
"Interfaz Γ(R): registrar pares (name, func) con firma (G, ctx) "
|
|
114
|
+
"en callbacks['before_step'|'after_step'|'on_remesh']",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if init_attrs:
|
|
118
|
+
init_node_attrs(G, override=True)
|
|
119
|
+
return G
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def step(
|
|
123
|
+
G: "nx.Graph",
|
|
124
|
+
*,
|
|
125
|
+
dt: float | None = None,
|
|
126
|
+
use_Si: bool = True,
|
|
127
|
+
apply_glyphs: bool = True,
|
|
128
|
+
) -> None:
|
|
129
|
+
_step(G, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def run(
|
|
133
|
+
G: "nx.Graph",
|
|
134
|
+
steps: int,
|
|
135
|
+
*,
|
|
136
|
+
dt: float | None = None,
|
|
137
|
+
use_Si: bool = True,
|
|
138
|
+
apply_glyphs: bool = True,
|
|
139
|
+
) -> None:
|
|
140
|
+
_run(G, steps=steps, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs)
|