tnfr 4.5.1__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.

Files changed (78) hide show
  1. tnfr/__init__.py +91 -90
  2. tnfr/alias.py +546 -0
  3. tnfr/cache.py +578 -0
  4. tnfr/callback_utils.py +388 -0
  5. tnfr/cli/__init__.py +75 -0
  6. tnfr/cli/arguments.py +177 -0
  7. tnfr/cli/execution.py +288 -0
  8. tnfr/cli/utils.py +36 -0
  9. tnfr/collections_utils.py +300 -0
  10. tnfr/config.py +19 -28
  11. tnfr/constants/__init__.py +174 -0
  12. tnfr/constants/core.py +159 -0
  13. tnfr/constants/init.py +31 -0
  14. tnfr/constants/metric.py +110 -0
  15. tnfr/constants_glyphs.py +98 -0
  16. tnfr/dynamics/__init__.py +658 -0
  17. tnfr/dynamics/dnfr.py +733 -0
  18. tnfr/dynamics/integrators.py +267 -0
  19. tnfr/dynamics/sampling.py +31 -0
  20. tnfr/execution.py +201 -0
  21. tnfr/flatten.py +283 -0
  22. tnfr/gamma.py +302 -88
  23. tnfr/glyph_history.py +290 -0
  24. tnfr/grammar.py +285 -96
  25. tnfr/graph_utils.py +84 -0
  26. tnfr/helpers/__init__.py +71 -0
  27. tnfr/helpers/numeric.py +87 -0
  28. tnfr/immutable.py +178 -0
  29. tnfr/import_utils.py +228 -0
  30. tnfr/initialization.py +197 -0
  31. tnfr/io.py +246 -0
  32. tnfr/json_utils.py +162 -0
  33. tnfr/locking.py +37 -0
  34. tnfr/logging_utils.py +116 -0
  35. tnfr/metrics/__init__.py +41 -0
  36. tnfr/metrics/coherence.py +829 -0
  37. tnfr/metrics/common.py +151 -0
  38. tnfr/metrics/core.py +101 -0
  39. tnfr/metrics/diagnosis.py +234 -0
  40. tnfr/metrics/export.py +137 -0
  41. tnfr/metrics/glyph_timing.py +189 -0
  42. tnfr/metrics/reporting.py +148 -0
  43. tnfr/metrics/sense_index.py +120 -0
  44. tnfr/metrics/trig.py +181 -0
  45. tnfr/metrics/trig_cache.py +109 -0
  46. tnfr/node.py +214 -159
  47. tnfr/observers.py +126 -136
  48. tnfr/ontosim.py +134 -134
  49. tnfr/operators/__init__.py +420 -0
  50. tnfr/operators/jitter.py +203 -0
  51. tnfr/operators/remesh.py +485 -0
  52. tnfr/presets.py +46 -14
  53. tnfr/rng.py +254 -0
  54. tnfr/selector.py +210 -0
  55. tnfr/sense.py +284 -131
  56. tnfr/structural.py +207 -79
  57. tnfr/tokens.py +60 -0
  58. tnfr/trace.py +329 -94
  59. tnfr/types.py +43 -17
  60. tnfr/validators.py +70 -24
  61. tnfr/value_utils.py +59 -0
  62. tnfr-4.5.2.dist-info/METADATA +379 -0
  63. tnfr-4.5.2.dist-info/RECORD +67 -0
  64. tnfr/cli.py +0 -322
  65. tnfr/constants.py +0 -277
  66. tnfr/dynamics.py +0 -814
  67. tnfr/helpers.py +0 -264
  68. tnfr/main.py +0 -47
  69. tnfr/metrics.py +0 -597
  70. tnfr/operators.py +0 -525
  71. tnfr/program.py +0 -176
  72. tnfr/scenarios.py +0 -34
  73. tnfr-4.5.1.dist-info/METADATA +0 -221
  74. tnfr-4.5.1.dist-info/RECORD +0 -28
  75. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
  76. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
  77. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
  78. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/top_level.txt +0 -0
tnfr/observers.py CHANGED
@@ -1,169 +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 collections import Counter
8
- from typing import Dict, Any
9
- import math
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(G, kind: str, ctx: dict):
18
- """Guarda eventos compactos en history['events']."""
19
- h = G.graph.setdefault("history", {})
20
- h.setdefault("events", []).append((kind, dict(ctx)))
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
- def std_after(G, ctx):
26
- _std_log(G, "after", ctx)
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
- """Registra callbacks estándar: before_step, after_step, on_remesh."""
33
- register_callback(G, "before_step", std_before)
34
- register_callback(G, "after_step", std_after)
35
- register_callback(G, "on_remesh", std_on_remesh)
36
- G.graph.setdefault("_STD_OBSERVER", "attached")
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
- def coherencia_global(G) -> float:
40
- """Proxy de C(t): alta cuando |ΔNFR| y |dEPI_dt| son pequeños."""
41
- dnfr = list_mean(abs(_get_attr(G.nodes[n], ALIAS_DNFR, 0.0)) for n in G.nodes())
42
- dEPI = list_mean(abs(_get_attr(G.nodes[n], ALIAS_dEPI, 0.0)) for n in G.nodes())
43
- return 1.0 / (1.0 + dnfr + dEPI)
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 sincronía_fase(G) -> float:
47
- X = [math.cos(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes()]
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
- th = math.atan2(sum(Y) / len(Y), sum(X) / len(X))
52
- # varianza angular aproximada (0 = muy sincronizado)
53
- import statistics as st
54
- var = (
55
- st.pvariance(
56
- [
57
- (
58
- (_get_attr(G.nodes[n], ALIAS_THETA, 0.0) - th + math.pi)
59
- % (2 * math.pi)
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
- def orden_kuramoto(G) -> float:
71
- """R en [0,1], 1 = fases perfectamente alineadas."""
72
- X = [math.cos(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes()]
73
- Y = [math.sin(_get_attr(G.nodes[n], ALIAS_THETA, 0.0)) for n in G.nodes()]
74
- if not X:
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 = ((sum(X)**2 + sum(Y)**2) ** 0.5) / max(1, len(X))
118
+ if R is None or psi is None:
119
+ R, psi = kuramoto_metrics(G)
77
120
  return float(R)
78
121
 
79
- def carga_glifica(G, window: int | None = None) -> dict:
80
- """Devuelve distribución de glifos aplicados en la red.
81
- - window: si se indica, cuenta solo los últimos `window` eventos por nodo; si no, usa el maxlen del deque.
82
- Retorna un dict con proporciones por glifo y agregados útiles.
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
- total = Counter()
85
- for n in G.nodes():
86
- nd = G.nodes[n]
87
- hist = nd.get("hist_glifos")
88
- if not hist:
89
- continue
90
- seq = list(hist)
91
- if window is not None and window > 0:
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
- # Agregados conceptuales (puedes ajustar categorías)
105
- # Glifos que consolidan la coherencia nodal: I’L estabiliza el flujo (cap. 6),
106
- # R’A propaga la resonancia (cap. 9), U’M acopla nodos en fase (cap. 8)
107
- # y SH’A ofrece silencio regenerativo (cap. 10). Véase manual TNFR,
108
- # sec. 18.19 "Análisis morfosintáctico" para la taxonomía funcional.
109
- estabilizadores = ["I’L", "R’A", "U’M", "SH’A"]
110
-
111
- # Glifos que perturban o reconfiguran la red: O’Z introduce disonancia
112
- # evolutiva (cap. 7), Z’HIR muta la estructura (cap. 14), NA’V marca
113
- # el tránsito entre estados (cap. 15) y T’HOL autoorganiza un nuevo
114
- # orden (cap. 13). Véase manual TNFR, sec. 18.19 para esta clasificación.
115
- disruptivos = ["O’Z", "Z’HIR", "NA’V", "T’HOL"]
116
-
117
-
118
- dist["_estabilizadores"] = sum(dist.get(k, 0.0) for k in estabilizadores)
119
- dist["_disruptivos"] = sum(dist.get(k, 0.0) for k in disruptivos)
140
+ dist = mix_groups(dist, GLYPH_GROUPS)
120
141
  dist["_count"] = count
121
142
  return dist
122
143
 
123
- def sigma_vector(G, window: int | None = None) -> dict:
124
- """Vector de sentido Σ⃗ a partir de la distribución glífica reciente.
125
- Devuelve dict con x, y, mag (0..1) y angle (rad)."""
126
- # Distribución glífica (proporciones)
127
- dist = carga_glifica(G, window=window)
128
- if not dist or dist.get("_count", 0) == 0:
129
- return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0}
130
-
131
- # Mapeo polar de glifos principales en el plano de sentido
132
- # (ordenado estabilización→expansión→acoplamiento→silencio→disonancia→mutación→transición→autoorg.)
133
- angles = {
134
- "I’L": 0.0,
135
- "R’A": math.pi/4,
136
- "U’M": math.pi/2,
137
- "SH’A": 3*math.pi/4,
138
- "O’Z": math.pi,
139
- "Z’HIR": 5*math.pi/4,
140
- "NA’V": 3*math.pi/2,
141
- "T’HOL": 7*math.pi/4,
142
- }
143
- # Normaliza solo sobre glifos mapeados
144
- total = sum(dist.get(k, 0.0) for k in angles.keys())
145
- if total <= 0:
146
- return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0}
147
-
148
- x = 0.0
149
- y = 0.0
150
- for k, a in angles.items():
151
- p = dist.get(k, 0.0) / total
152
- x += p * math.cos(a)
153
- y += p * math.sin(a)
154
-
155
- mag = (x*x + y*y) ** 0.5
156
- ang = math.atan2(y, x)
157
- return {"x": float(x), "y": float(y), "mag": float(mag), "angle": float(ang)}
158
144
 
159
145
  def wbar(G, window: int | None = None) -> float:
160
- """Devuelve W̄ = media de C(t) en una ventana reciente."""
161
- hist = G.graph.get("history", {})
162
- cs = hist.get("C_steps", [])
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", []))
163
153
  if not cs:
164
154
  # fallback: coherencia instantánea
165
- return coherencia_global(G)
166
- if window is None:
167
- window = int(G.graph.get("WBAR_WINDOW", 25))
168
- w = min(len(cs), max(1, int(window)))
169
- return float(sum(cs[-w:]) / w)
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
- ontosim.py — TNFR canónica
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
- from .constants import DEFAULTS, attach_defaults
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
- # API de alto nivel
18
-
19
- def preparar_red(G: nx.Graph, *, override_defaults: bool = False, **overrides) -> nx.Graph:
20
- attach_defaults(G, override=override_defaults)
21
- if overrides:
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
- merge_overrides(G, **overrides)
24
- # Inicializaciones blandas
25
- G.graph.setdefault("history", {
26
- "C_steps": [],
27
- "stable_frac": [],
28
- "phase_sync": [],
29
- "kuramoto_R": [],
30
- "sense_sigma_x": [],
31
- "sense_sigma_y": [],
32
- "sense_sigma_mag": [],
33
- "sense_sigma_angle": [],
34
- "iota": [],
35
- "glyph_load_estab": [],
36
- "glyph_load_disr": [],
37
- "Si_mean": [],
38
- "Si_hi_frac": [],
39
- "Si_lo_frac": [],
40
- "W_bar": [],
41
- "phase_kG": [],
42
- "phase_kL": [],
43
- "phase_state": [],
44
- "phase_R": [],
45
- "phase_disr": [],
46
- })
47
- tau = int(G.graph.get("REMESH_TAU", DEFAULTS["REMESH_TAU"]))
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
- try:
53
- from .observers import attach_standard_observer
54
- attach_standard_observer(G)
55
- except Exception as e:
56
- G.graph.setdefault("_callback_errors", []).append(
57
- {"event":"attach_std_observer","error":repr(e)}
58
- )
59
- # Hook explícito para ΔNFR (se puede sustituir luego con dynamics.set_delta_nfr_hook)
60
- G.graph.setdefault("compute_delta_nfr", default_compute_delta_nfr)
61
- G.graph.setdefault("_dnfr_hook_name", "default_compute_delta_nfr")
62
- # Callbacks Γ(R): before_step / after_step / on_remesh
63
- G.graph.setdefault("callbacks", {
64
- "before_step": [],
65
- "after_step": [],
66
- "on_remesh": [],
67
- })
68
- G.graph.setdefault("_CALLBACKS_DOC",
69
- "Interfaz Γ(R): registrar funciones (G, ctx) en callbacks['before_step'|'after_step'|'on_remesh']")
70
-
71
- # --- Inicialización configurable de θ y νf ---
72
- seed = int(G.graph.get("RANDOM_SEED", 0))
73
- init_rand_phase = bool(G.graph.get("INIT_RANDOM_PHASE", DEFAULTS.get("INIT_RANDOM_PHASE", True)))
74
-
75
- th_min = float(G.graph.get("INIT_THETA_MIN", DEFAULTS.get("INIT_THETA_MIN", -math.pi)))
76
- th_max = float(G.graph.get("INIT_THETA_MAX", DEFAULTS.get("INIT_THETA_MAX", math.pi)))
77
-
78
- vf_mode = str(G.graph.get("INIT_VF_MODE", DEFAULTS.get("INIT_VF_MODE", "uniform"))).lower()
79
- vf_min_lim = float(G.graph.get("VF_MIN", DEFAULTS["VF_MIN"]))
80
- vf_max_lim = float(G.graph.get("VF_MAX", DEFAULTS["VF_MAX"]))
81
-
82
- vf_uniform_min = G.graph.get("INIT_VF_MIN", DEFAULTS.get("INIT_VF_MIN", None))
83
- vf_uniform_max = G.graph.get("INIT_VF_MAX", DEFAULTS.get("INIT_VF_MAX", None))
84
- if vf_uniform_min is None: vf_uniform_min = vf_min_lim
85
- if vf_uniform_max is None: vf_uniform_max = vf_max_lim
86
-
87
- vf_mean = float(G.graph.get("INIT_VF_MEAN", DEFAULTS.get("INIT_VF_MEAN", 0.5)))
88
- vf_std = float(G.graph.get("INIT_VF_STD", DEFAULTS.get("INIT_VF_STD", 0.15)))
89
- clamp_to_limits = bool(G.graph.get("INIT_VF_CLAMP_TO_LIMITS", DEFAULTS.get("INIT_VF_CLAMP_TO_LIMITS", True)))
90
-
91
- for idx, n in enumerate(G.nodes()):
92
- nd = G.nodes[n]
93
- # EPI canónico
94
- nd.setdefault("EPI", 0.0)
95
-
96
- # θ aleatoria (opt-in por flag)
97
- if init_rand_phase:
98
- th_rng = random.Random(seed + 1009 * idx)
99
- nd["θ"] = th_rng.uniform(th_min, th_max)
100
- else:
101
- nd.setdefault("θ", 0.0)
102
-
103
- # νf distribuida
104
- if vf_mode == "uniform":
105
- vf_rng = random.Random(seed * 1000003 + idx)
106
- vf = vf_rng.uniform(float(vf_uniform_min), float(vf_uniform_max))
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)