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.
Files changed (161) hide show
  1. tnfr/__init__.py +228 -49
  2. tnfr/__init__.pyi +40 -0
  3. tnfr/_compat.py +11 -0
  4. tnfr/_version.py +7 -0
  5. tnfr/_version.pyi +7 -0
  6. tnfr/alias.py +106 -21
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +666 -512
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +2 -9
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +21 -7
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +42 -20
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +54 -20
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +0 -2
  19. tnfr/cli/utils.pyi +8 -0
  20. tnfr/config/__init__.py +12 -0
  21. tnfr/config/__init__.pyi +8 -0
  22. tnfr/config/constants.py +104 -0
  23. tnfr/config/constants.pyi +12 -0
  24. tnfr/{config.py → config/init.py} +11 -7
  25. tnfr/config/init.pyi +8 -0
  26. tnfr/config/operator_names.py +106 -0
  27. tnfr/config/operator_names.pyi +28 -0
  28. tnfr/config/presets.py +104 -0
  29. tnfr/config/presets.pyi +7 -0
  30. tnfr/constants/__init__.py +78 -24
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +1 -2
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.pyi +12 -0
  35. tnfr/constants/metric.py +4 -12
  36. tnfr/constants/metric.pyi +19 -0
  37. tnfr/constants_glyphs.py +9 -91
  38. tnfr/constants_glyphs.pyi +12 -0
  39. tnfr/dynamics/__init__.py +112 -634
  40. tnfr/dynamics/__init__.pyi +83 -0
  41. tnfr/dynamics/adaptation.py +201 -0
  42. tnfr/dynamics/aliases.py +22 -0
  43. tnfr/dynamics/coordination.py +343 -0
  44. tnfr/dynamics/dnfr.py +1936 -354
  45. tnfr/dynamics/dnfr.pyi +33 -0
  46. tnfr/dynamics/integrators.py +369 -75
  47. tnfr/dynamics/integrators.pyi +35 -0
  48. tnfr/dynamics/runtime.py +521 -0
  49. tnfr/dynamics/sampling.py +8 -5
  50. tnfr/dynamics/sampling.pyi +7 -0
  51. tnfr/dynamics/selectors.py +680 -0
  52. tnfr/execution.py +56 -41
  53. tnfr/execution.pyi +65 -0
  54. tnfr/flatten.py +7 -7
  55. tnfr/flatten.pyi +28 -0
  56. tnfr/gamma.py +54 -37
  57. tnfr/gamma.pyi +40 -0
  58. tnfr/glyph_history.py +85 -38
  59. tnfr/glyph_history.pyi +53 -0
  60. tnfr/grammar.py +19 -338
  61. tnfr/grammar.pyi +13 -0
  62. tnfr/helpers/__init__.py +110 -30
  63. tnfr/helpers/__init__.pyi +66 -0
  64. tnfr/helpers/numeric.py +1 -0
  65. tnfr/helpers/numeric.pyi +12 -0
  66. tnfr/immutable.py +55 -19
  67. tnfr/immutable.pyi +37 -0
  68. tnfr/initialization.py +12 -10
  69. tnfr/initialization.pyi +73 -0
  70. tnfr/io.py +99 -34
  71. tnfr/io.pyi +11 -0
  72. tnfr/locking.pyi +7 -0
  73. tnfr/metrics/__init__.pyi +20 -0
  74. tnfr/metrics/coherence.py +934 -294
  75. tnfr/metrics/common.py +1 -3
  76. tnfr/metrics/common.pyi +15 -0
  77. tnfr/metrics/core.py +192 -34
  78. tnfr/metrics/core.pyi +13 -0
  79. tnfr/metrics/diagnosis.py +707 -101
  80. tnfr/metrics/diagnosis.pyi +89 -0
  81. tnfr/metrics/export.py +27 -13
  82. tnfr/metrics/glyph_timing.py +218 -38
  83. tnfr/metrics/reporting.py +22 -18
  84. tnfr/metrics/reporting.pyi +12 -0
  85. tnfr/metrics/sense_index.py +199 -25
  86. tnfr/metrics/sense_index.pyi +9 -0
  87. tnfr/metrics/trig.py +53 -18
  88. tnfr/metrics/trig.pyi +12 -0
  89. tnfr/metrics/trig_cache.py +3 -7
  90. tnfr/metrics/trig_cache.pyi +10 -0
  91. tnfr/node.py +148 -125
  92. tnfr/node.pyi +161 -0
  93. tnfr/observers.py +44 -30
  94. tnfr/observers.pyi +46 -0
  95. tnfr/ontosim.py +14 -13
  96. tnfr/ontosim.pyi +33 -0
  97. tnfr/operators/__init__.py +84 -52
  98. tnfr/operators/__init__.pyi +31 -0
  99. tnfr/operators/definitions.py +181 -0
  100. tnfr/operators/definitions.pyi +92 -0
  101. tnfr/operators/jitter.py +86 -23
  102. tnfr/operators/jitter.pyi +11 -0
  103. tnfr/operators/registry.py +80 -0
  104. tnfr/operators/registry.pyi +15 -0
  105. tnfr/operators/remesh.py +141 -57
  106. tnfr/presets.py +9 -54
  107. tnfr/presets.pyi +7 -0
  108. tnfr/py.typed +0 -0
  109. tnfr/rng.py +259 -73
  110. tnfr/rng.pyi +14 -0
  111. tnfr/selector.py +24 -17
  112. tnfr/selector.pyi +19 -0
  113. tnfr/sense.py +55 -43
  114. tnfr/sense.pyi +30 -0
  115. tnfr/structural.py +44 -267
  116. tnfr/structural.pyi +46 -0
  117. tnfr/telemetry/__init__.py +13 -0
  118. tnfr/telemetry/verbosity.py +37 -0
  119. tnfr/tokens.py +3 -2
  120. tnfr/tokens.pyi +41 -0
  121. tnfr/trace.py +272 -82
  122. tnfr/trace.pyi +68 -0
  123. tnfr/types.py +345 -6
  124. tnfr/types.pyi +145 -0
  125. tnfr/utils/__init__.py +158 -0
  126. tnfr/utils/__init__.pyi +133 -0
  127. tnfr/utils/cache.py +755 -0
  128. tnfr/utils/cache.pyi +156 -0
  129. tnfr/{collections_utils.py → utils/data.py} +57 -90
  130. tnfr/utils/data.pyi +73 -0
  131. tnfr/utils/graph.py +87 -0
  132. tnfr/utils/graph.pyi +10 -0
  133. tnfr/utils/init.py +746 -0
  134. tnfr/utils/init.pyi +85 -0
  135. tnfr/{json_utils.py → utils/io.py} +13 -18
  136. tnfr/utils/io.pyi +10 -0
  137. tnfr/utils/validators.py +130 -0
  138. tnfr/utils/validators.pyi +19 -0
  139. tnfr/validation/__init__.py +25 -0
  140. tnfr/validation/__init__.pyi +17 -0
  141. tnfr/validation/compatibility.py +59 -0
  142. tnfr/validation/compatibility.pyi +8 -0
  143. tnfr/validation/grammar.py +149 -0
  144. tnfr/validation/grammar.pyi +11 -0
  145. tnfr/validation/rules.py +194 -0
  146. tnfr/validation/rules.pyi +18 -0
  147. tnfr/validation/syntax.py +151 -0
  148. tnfr/validation/syntax.pyi +7 -0
  149. tnfr-6.0.0.dist-info/METADATA +135 -0
  150. tnfr-6.0.0.dist-info/RECORD +157 -0
  151. tnfr/graph_utils.py +0 -84
  152. tnfr/import_utils.py +0 -228
  153. tnfr/logging_utils.py +0 -116
  154. tnfr/validators.py +0 -84
  155. tnfr/value_utils.py +0 -59
  156. tnfr-4.5.2.dist-info/METADATA +0 -379
  157. tnfr-4.5.2.dist-info/RECORD +0 -67
  158. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  159. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  160. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  161. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/dynamics/__init__.py CHANGED
@@ -1,658 +1,136 @@
1
- from __future__ import annotations
2
-
3
- import math
4
- from collections import deque
5
- from operator import itemgetter
6
- from typing import Any
1
+ """Facade for TNFR dynamics submodules."""
7
2
 
8
- # Importar compute_Si y apply_glyph a nivel de módulo evita el coste de
9
- # realizar la importación en cada paso de la dinámica. Como los módulos de
10
- # origen no dependen de ``dynamics``, no se introducen ciclos.
11
- from ..operators import apply_remesh_if_globally_stable, apply_glyph
12
- from ..grammar import enforce_canonical_grammar, on_applied_glyph
13
- from ..types import Glyph
14
- from ..constants import (
15
- DEFAULTS,
16
- METRIC_DEFAULTS,
17
- get_aliases,
18
- get_param,
19
- get_graph_param,
20
- )
21
- from ..observers import DEFAULT_GLYPH_LOAD_SPAN, glyph_load, kuramoto_order
3
+ from __future__ import annotations
22
4
 
23
- from ..helpers.numeric import (
24
- clamp,
25
- clamp01,
26
- angle_diff,
27
- )
28
- from ..metrics.trig import neighbor_phase_mean
29
- from ..alias import (
30
- get_attr,
31
- set_vf,
32
- set_attr,
33
- set_theta,
34
- multi_recompute_abs_max,
5
+ from concurrent.futures import ProcessPoolExecutor
6
+
7
+ from . import coordination, dnfr, integrators
8
+ from .adaptation import adapt_vf_by_coherence
9
+ from .aliases import (
10
+ ALIAS_D2EPI,
11
+ ALIAS_DNFR,
12
+ ALIAS_DSI,
13
+ ALIAS_EPI,
14
+ ALIAS_SI,
15
+ ALIAS_VF,
35
16
  )
36
- from ..metrics.sense_index import compute_Si
37
- from ..metrics.common import compute_dnfr_accel_max, merge_and_normalize_weights
38
- from ..metrics.trig_cache import compute_theta_trig
39
- from ..callback_utils import CallbackEvent, callback_manager
40
- from ..glyph_history import recent_glyph, ensure_history, append_metric
41
- from ..selector import (
42
- _selector_thresholds,
43
- _norms_para_selector,
44
- _calc_selector_score,
45
- _apply_selector_hysteresis,
46
- )
47
-
48
- from .sampling import update_node_sample as _update_node_sample
17
+ from .coordination import coordinate_global_local_phase
49
18
  from .dnfr import (
50
- _prepare_dnfr_data,
19
+ _compute_dnfr,
20
+ _compute_neighbor_means,
51
21
  _init_dnfr_cache,
22
+ _prepare_dnfr_data,
52
23
  _refresh_dnfr_vectors,
53
- _compute_neighbor_means,
54
- _compute_dnfr,
55
24
  default_compute_delta_nfr,
56
- set_delta_nfr_hook,
57
- dnfr_phase_only,
58
25
  dnfr_epi_vf_mixed,
59
26
  dnfr_laplacian,
27
+ dnfr_phase_only,
28
+ set_delta_nfr_hook,
60
29
  )
61
30
  from .integrators import (
31
+ AbstractIntegrator,
32
+ DefaultIntegrator,
62
33
  prepare_integration_params,
63
34
  update_epi_via_nodal_equation,
64
35
  )
65
-
66
- ALIAS_VF = get_aliases("VF")
67
- ALIAS_THETA = get_aliases("THETA")
68
- ALIAS_DNFR = get_aliases("DNFR")
69
- ALIAS_EPI = get_aliases("EPI")
70
- ALIAS_SI = get_aliases("SI")
71
- ALIAS_D2EPI = get_aliases("D2EPI")
72
- ALIAS_DSI = get_aliases("DSI")
36
+ from .runtime import (
37
+ _maybe_remesh,
38
+ _normalize_job_overrides,
39
+ _prepare_dnfr,
40
+ _resolve_jobs_override,
41
+ _run_after_callbacks,
42
+ _run_before_callbacks,
43
+ _run_validators,
44
+ _update_epi_hist,
45
+ _update_nodes,
46
+ apply_canonical_clamps,
47
+ run,
48
+ step,
49
+ validate_canon,
50
+ )
51
+ from .sampling import update_node_sample as _update_node_sample
52
+ from .selectors import (
53
+ AbstractSelector,
54
+ DefaultGlyphSelector,
55
+ GlyphCode,
56
+ ParametricGlyphSelector,
57
+ _SelectorPreselection,
58
+ _apply_glyphs,
59
+ _apply_selector,
60
+ _choose_glyph,
61
+ _collect_selector_metrics,
62
+ _configure_selector_weights,
63
+ _prepare_selector_preselection,
64
+ _resolve_preselected_glyph,
65
+ _selector_parallel_jobs,
66
+ default_glyph_selector,
67
+ parametric_glyph_selector,
68
+ )
69
+ from ..operators import apply_glyph
70
+ from ..metrics.sense_index import compute_Si
71
+ from ..utils import get_numpy
72
+ from ..validation.grammar import enforce_canonical_grammar, on_applied_glyph
73
73
 
74
74
  __all__ = (
75
+ "coordination",
76
+ "dnfr",
77
+ "integrators",
78
+ "ALIAS_D2EPI",
79
+ "ALIAS_DNFR",
80
+ "ALIAS_DSI",
81
+ "ALIAS_EPI",
82
+ "ALIAS_SI",
83
+ "ALIAS_VF",
84
+ "AbstractSelector",
85
+ "DefaultGlyphSelector",
86
+ "ParametricGlyphSelector",
87
+ "GlyphCode",
88
+ "_SelectorPreselection",
89
+ "_apply_glyphs",
90
+ "_apply_selector",
91
+ "_choose_glyph",
92
+ "_collect_selector_metrics",
93
+ "_configure_selector_weights",
94
+ "ProcessPoolExecutor",
95
+ "_maybe_remesh",
96
+ "_normalize_job_overrides",
97
+ "_prepare_dnfr",
98
+ "_prepare_dnfr_data",
99
+ "_prepare_selector_preselection",
100
+ "_resolve_jobs_override",
101
+ "_resolve_preselected_glyph",
102
+ "_run_after_callbacks",
103
+ "_run_before_callbacks",
104
+ "_run_validators",
105
+ "_selector_parallel_jobs",
106
+ "_update_epi_hist",
107
+ "_update_node_sample",
108
+ "_update_nodes",
109
+ "_compute_dnfr",
110
+ "_compute_neighbor_means",
111
+ "_init_dnfr_cache",
112
+ "_refresh_dnfr_vectors",
113
+ "adapt_vf_by_coherence",
114
+ "apply_canonical_clamps",
115
+ "coordinate_global_local_phase",
116
+ "compute_Si",
75
117
  "default_compute_delta_nfr",
76
- "set_delta_nfr_hook",
77
- "dnfr_phase_only",
118
+ "default_glyph_selector",
78
119
  "dnfr_epi_vf_mixed",
79
120
  "dnfr_laplacian",
121
+ "dnfr_phase_only",
122
+ "enforce_canonical_grammar",
123
+ "get_numpy",
124
+ "on_applied_glyph",
125
+ "apply_glyph",
126
+ "parametric_glyph_selector",
127
+ "AbstractIntegrator",
128
+ "DefaultIntegrator",
80
129
  "prepare_integration_params",
130
+ "run",
131
+ "set_delta_nfr_hook",
132
+ "step",
81
133
  "update_epi_via_nodal_equation",
82
- "apply_canonical_clamps",
83
134
  "validate_canon",
84
- "coordinate_global_local_phase",
85
- "adapt_vf_by_coherence",
86
- "default_glyph_selector",
87
- "parametric_glyph_selector",
88
- "step",
89
- "run",
90
- "_prepare_dnfr_data",
91
- "_init_dnfr_cache",
92
- "_refresh_dnfr_vectors",
93
- "_compute_neighbor_means",
94
- "_compute_dnfr",
95
135
  )
96
136
 
97
-
98
- def _log_clamp(hist, node, attr, value, lo, hi):
99
- if value < lo or value > hi:
100
- hist.append({"node": node, "attr": attr, "value": float(value)})
101
-
102
-
103
- def apply_canonical_clamps(nd: dict[str, Any], G=None, node=None) -> None:
104
- g = G.graph if G is not None else DEFAULTS
105
- eps_min = float(g.get("EPI_MIN", DEFAULTS["EPI_MIN"]))
106
- eps_max = float(g.get("EPI_MAX", DEFAULTS["EPI_MAX"]))
107
- vf_min = float(g.get("VF_MIN", DEFAULTS["VF_MIN"]))
108
- vf_max = float(g.get("VF_MAX", DEFAULTS["VF_MAX"]))
109
- theta_wrap = bool(g.get("THETA_WRAP", DEFAULTS["THETA_WRAP"]))
110
-
111
- epi = get_attr(nd, ALIAS_EPI, 0.0)
112
- vf = get_attr(nd, ALIAS_VF, 0.0)
113
- th = get_attr(nd, ALIAS_THETA, 0.0)
114
-
115
- strict = bool(
116
- g.get("VALIDATORS_STRICT", DEFAULTS.get("VALIDATORS_STRICT", False))
117
- )
118
- if strict and G is not None:
119
- hist = g.setdefault("history", {}).setdefault("clamp_alerts", [])
120
- _log_clamp(hist, node, "EPI", epi, eps_min, eps_max)
121
- _log_clamp(hist, node, "VF", vf, vf_min, vf_max)
122
-
123
- set_attr(nd, ALIAS_EPI, clamp(epi, eps_min, eps_max))
124
-
125
- vf_val = clamp(vf, vf_min, vf_max)
126
- if G is not None and node is not None:
127
- set_vf(G, node, vf_val, update_max=False)
128
- else:
129
- set_attr(nd, ALIAS_VF, vf_val)
130
-
131
- if theta_wrap:
132
- new_th = (th + math.pi) % (2 * math.pi) - math.pi
133
- if G is not None and node is not None:
134
- set_theta(G, node, new_th)
135
- else:
136
- set_attr(nd, ALIAS_THETA, new_th)
137
-
138
-
139
- def validate_canon(G) -> None:
140
- """Apply canonical clamps to all nodes of ``G``.
141
-
142
- Wrap phase and constrain ``EPI`` and ``νf`` to the ranges in ``G.graph``.
143
- If ``VALIDATORS_STRICT`` is active, alerts are logged in ``history``.
144
- """
145
- for n, nd in G.nodes(data=True):
146
- apply_canonical_clamps(nd, G, n)
147
- maxes = multi_recompute_abs_max(G, {"_vfmax": ALIAS_VF})
148
- G.graph.update(maxes)
149
- return G
150
-
151
-
152
- def _read_adaptive_params(
153
- g: dict[str, Any],
154
- ) -> tuple[dict[str, Any], float, float]:
155
- """Obtain configuration and current values for phase adaptation."""
156
- cfg = g.get("PHASE_ADAPT", DEFAULTS.get("PHASE_ADAPT", {}))
157
- kG = float(g.get("PHASE_K_GLOBAL", DEFAULTS["PHASE_K_GLOBAL"]))
158
- kL = float(g.get("PHASE_K_LOCAL", DEFAULTS["PHASE_K_LOCAL"]))
159
- return cfg, kG, kL
160
-
161
-
162
- def _compute_state(G, cfg: dict[str, Any]) -> tuple[str, float, float]:
163
- """Return current state (stable/dissonant/transition) and metrics."""
164
- R = kuramoto_order(G)
165
- dist = glyph_load(G, window=DEFAULT_GLYPH_LOAD_SPAN)
166
- disr = float(dist.get("_disruptivos", 0.0)) if dist else 0.0
167
-
168
- R_hi = float(cfg.get("R_hi", 0.90))
169
- R_lo = float(cfg.get("R_lo", 0.60))
170
- disr_hi = float(cfg.get("disr_hi", 0.50))
171
- disr_lo = float(cfg.get("disr_lo", 0.25))
172
- if (R >= R_hi) and (disr <= disr_lo):
173
- state = "estable"
174
- elif (R <= R_lo) or (disr >= disr_hi):
175
- state = "disonante"
176
- else:
177
- state = "transicion"
178
- return state, float(R), disr
179
-
180
-
181
- def _smooth_adjust_k(
182
- kG: float, kL: float, state: str, cfg: dict[str, Any]
183
- ) -> tuple[float, float]:
184
- """Smoothly update kG/kL toward targets according to state."""
185
- kG_min = float(cfg.get("kG_min", 0.01))
186
- kG_max = float(cfg.get("kG_max", 0.20))
187
- kL_min = float(cfg.get("kL_min", 0.05))
188
- kL_max = float(cfg.get("kL_max", 0.25))
189
-
190
- if state == "disonante":
191
- kG_t = kG_max
192
- kL_t = 0.5 * (
193
- kL_min + kL_max
194
- ) # local medio para no perder plasticidad
195
- elif state == "estable":
196
- kG_t = kG_min
197
- kL_t = kL_min
198
- else:
199
- kG_t = 0.5 * (kG_min + kG_max)
200
- kL_t = 0.5 * (kL_min + kL_max)
201
-
202
- up = float(cfg.get("up", 0.10))
203
- down = float(cfg.get("down", 0.07))
204
-
205
- def _step(curr: float, target: float, mn: float, mx: float) -> float:
206
- gain = up if target > curr else down
207
- nxt = curr + gain * (target - curr)
208
- return max(mn, min(mx, nxt))
209
-
210
- return _step(kG, kG_t, kG_min, kG_max), _step(kL, kL_t, kL_min, kL_max)
211
-
212
-
213
- def _ensure_hist_deque(hist: dict[str, Any], key: str, maxlen: int) -> deque:
214
- """Ensure history entry ``key`` is a deque with ``maxlen``."""
215
- dq = hist.setdefault(key, deque(maxlen=maxlen))
216
- if not isinstance(dq, deque):
217
- dq = deque(dq, maxlen=maxlen)
218
- hist[key] = dq
219
- return dq
220
-
221
-
222
- def coordinate_global_local_phase(
223
- G, global_force: float | None = None, local_force: float | None = None
224
- ) -> None:
225
- """
226
- Ajusta fase con mezcla GLOBAL+VECINAL.
227
- Si no se pasan fuerzas explícitas, adapta kG/kL según estado
228
- (disonante / transición / estable).
229
- Estado se decide por R (Kuramoto) y carga glífica disruptiva reciente.
230
- """
231
- g = G.graph
232
- defaults = DEFAULTS
233
- hist = g.setdefault("history", {})
234
- maxlen = int(
235
- g.get("PHASE_HISTORY_MAXLEN", METRIC_DEFAULTS["PHASE_HISTORY_MAXLEN"])
236
- )
237
- hist_state = _ensure_hist_deque(hist, "phase_state", maxlen)
238
- hist_R = _ensure_hist_deque(hist, "phase_R", maxlen)
239
- hist_disr = _ensure_hist_deque(hist, "phase_disr", maxlen)
240
- # 0) Si hay fuerzas explícitas, usar y salir del modo adaptativo
241
- if (global_force is not None) or (local_force is not None):
242
- kG = float(
243
- global_force
244
- if global_force is not None
245
- else g.get("PHASE_K_GLOBAL", defaults["PHASE_K_GLOBAL"])
246
- )
247
- kL = float(
248
- local_force
249
- if local_force is not None
250
- else g.get("PHASE_K_LOCAL", defaults["PHASE_K_LOCAL"])
251
- )
252
- else:
253
- cfg, kG, kL = _read_adaptive_params(g)
254
-
255
- if bool(cfg.get("enabled", False)):
256
- state, R, disr = _compute_state(G, cfg)
257
- kG, kL = _smooth_adjust_k(kG, kL, state, cfg)
258
-
259
- hist_state.append(state)
260
- hist_R.append(float(R))
261
- hist_disr.append(float(disr))
262
-
263
- g["PHASE_K_GLOBAL"] = kG
264
- g["PHASE_K_LOCAL"] = kL
265
- append_metric(hist, "phase_kG", float(kG))
266
- append_metric(hist, "phase_kL", float(kL))
267
-
268
- # 6) Fase GLOBAL (centroide) para empuje
269
- trig = compute_theta_trig(G.nodes(data=True))
270
- num_nodes = G.number_of_nodes()
271
- if num_nodes:
272
- mean_cos = sum(trig.cos.values()) / num_nodes
273
- mean_sin = sum(trig.sin.values()) / num_nodes
274
- thG = math.atan2(mean_sin, mean_cos)
275
- else:
276
- thG = 0.0
277
-
278
- # 7) Aplicar corrección global+vecinal
279
- for n, nd in G.nodes(data=True):
280
- th = get_attr(nd, ALIAS_THETA, 0.0)
281
- thL = neighbor_phase_mean(G, n)
282
- dG = angle_diff(thG, th)
283
- dL = angle_diff(thL, th)
284
- set_theta(G, n, th + kG * dG + kL * dL)
285
-
286
-
287
- # -------------------------
288
- # Adaptación de νf por coherencia
289
- # -------------------------
290
-
291
-
292
- def adapt_vf_by_coherence(G) -> None:
293
- """Adjust νf toward neighbour mean in nodes with sustained stability."""
294
- tau = get_graph_param(G, "VF_ADAPT_TAU", int)
295
- mu = get_graph_param(G, "VF_ADAPT_MU")
296
- eps_dnfr = get_graph_param(G, "EPS_DNFR_STABLE")
297
- thr_sel = get_graph_param(G, "SELECTOR_THRESHOLDS", dict)
298
- thr_def = get_graph_param(G, "GLYPH_THRESHOLDS", dict)
299
- si_hi = float(thr_sel.get("si_hi", thr_def.get("hi", 0.66)))
300
- vf_min = get_graph_param(G, "VF_MIN")
301
- vf_max = get_graph_param(G, "VF_MAX")
302
-
303
- updates = {}
304
- for n, nd in G.nodes(data=True):
305
- Si = get_attr(nd, ALIAS_SI, 0.0)
306
- dnfr = abs(get_attr(nd, ALIAS_DNFR, 0.0))
307
- if Si >= si_hi and dnfr <= eps_dnfr:
308
- nd["stable_count"] = nd.get("stable_count", 0) + 1
309
- else:
310
- nd["stable_count"] = 0
311
- continue
312
-
313
- if nd["stable_count"] >= tau:
314
- vf = get_attr(nd, ALIAS_VF, 0.0)
315
- neigh = list(G.neighbors(n))
316
- if neigh:
317
- total = 0.0
318
- for v in neigh:
319
- total += float(get_attr(G.nodes[v], ALIAS_VF, vf))
320
- vf_bar = total / len(neigh)
321
- else:
322
- vf_bar = float(vf)
323
- updates[n] = vf + mu * (vf_bar - vf)
324
-
325
- for n, vf_new in updates.items():
326
- set_vf(G, n, clamp(vf_new, vf_min, vf_max))
327
-
328
-
329
- # -------------------------
330
- # Selector glífico por defecto
331
- # -------------------------
332
- def default_glyph_selector(G, n) -> str:
333
- nd = G.nodes[n]
334
- thr = _selector_thresholds(G)
335
- hi, lo, dnfr_hi = itemgetter("si_hi", "si_lo", "dnfr_hi")(thr)
336
- # Extract thresholds in one call to reduce dict lookups inside loops.
337
-
338
- norms = G.graph.get("_sel_norms")
339
- if norms is None:
340
- norms = compute_dnfr_accel_max(G)
341
- G.graph["_sel_norms"] = norms
342
- dnfr_max = float(norms.get("dnfr_max", 1.0)) or 1.0
343
-
344
- Si = clamp01(get_attr(nd, ALIAS_SI, 0.5))
345
- dnfr = abs(get_attr(nd, ALIAS_DNFR, 0.0)) / dnfr_max
346
-
347
- if Si >= hi:
348
- return "IL"
349
- if Si <= lo:
350
- return "OZ" if dnfr > dnfr_hi else "ZHIR"
351
- return "NAV" if dnfr > dnfr_hi else "RA"
352
-
353
-
354
- # -------------------------
355
- # Selector glífico multiobjetivo (paramétrico)
356
- # -------------------------
357
- def _soft_grammar_prefilter(G, n, cand, dnfr, accel):
358
- """Soft grammar: avoid repetitions before the canonical one."""
359
- gram = get_graph_param(G, "GRAMMAR", dict)
360
- gwin = int(gram.get("window", 3))
361
- avoid = set(gram.get("avoid_repeats", []))
362
- force_dn = float(gram.get("force_dnfr", 0.60))
363
- force_ac = float(gram.get("force_accel", 0.60))
364
- fallbacks = gram.get("fallbacks", {})
365
- nd = G.nodes[n]
366
- if cand in avoid and recent_glyph(nd, cand, gwin):
367
- if not (dnfr >= force_dn or accel >= force_ac):
368
- cand = fallbacks.get(cand, cand)
369
- return cand
370
-
371
-
372
- def _selector_normalized_metrics(nd, norms):
373
- """Extract and normalise Si, ΔNFR and acceleration for the selector."""
374
- dnfr_max = float(norms.get("dnfr_max", 1.0)) or 1.0
375
- acc_max = float(norms.get("accel_max", 1.0)) or 1.0
376
- Si = clamp01(get_attr(nd, ALIAS_SI, 0.5))
377
- dnfr = abs(get_attr(nd, ALIAS_DNFR, 0.0)) / dnfr_max
378
- accel = abs(get_attr(nd, ALIAS_D2EPI, 0.0)) / acc_max
379
- return Si, dnfr, accel
380
-
381
-
382
- def _selector_base_choice(Si, dnfr, accel, thr):
383
- """Base decision according to thresholds of Si, ΔNFR and acceleration."""
384
- si_hi, si_lo, dnfr_hi, acc_hi = itemgetter(
385
- "si_hi", "si_lo", "dnfr_hi", "accel_hi"
386
- )(thr) # Reduce dict lookups inside loops.
387
- if Si >= si_hi:
388
- return "IL"
389
- if Si <= si_lo:
390
- if accel >= acc_hi:
391
- return "THOL"
392
- return "OZ" if dnfr >= dnfr_hi else "ZHIR"
393
- if dnfr >= dnfr_hi or accel >= acc_hi:
394
- return "NAV"
395
- return "RA"
396
-
397
-
398
- def _configure_selector_weights(G) -> dict:
399
- """Normalise and store selector weights in ``G.graph``."""
400
- weights = merge_and_normalize_weights(
401
- G, "SELECTOR_WEIGHTS", ("w_si", "w_dnfr", "w_accel")
402
- )
403
- G.graph["_selector_weights"] = weights
404
- return weights
405
-
406
-
407
- def _compute_selector_score(G, nd, Si, dnfr, accel, cand):
408
- """Compute score and apply stagnation penalties."""
409
- W = G.graph.get("_selector_weights")
410
- if W is None:
411
- W = _configure_selector_weights(G)
412
- score = _calc_selector_score(Si, dnfr, accel, W)
413
- hist_prev = nd.get("glyph_history")
414
- if hist_prev and hist_prev[-1] == cand:
415
- delta_si = get_attr(nd, ALIAS_DSI, 0.0)
416
- h = ensure_history(G)
417
- sig = h.get("sense_sigma_mag", [])
418
- delta_sigma = sig[-1] - sig[-2] if len(sig) >= 2 else 0.0
419
- if delta_si <= 0.0 and delta_sigma <= 0.0:
420
- score -= 0.05
421
- return score
422
-
423
-
424
- def _apply_score_override(cand, score, dnfr, dnfr_lo):
425
- """Adjust final candidate smoothly according to the score."""
426
- if score >= 0.66 and cand in ("NAV", "RA", "ZHIR", "OZ"):
427
- cand = "IL"
428
- elif score <= 0.33 and cand in ("NAV", "RA", "IL"):
429
- cand = "OZ" if dnfr >= dnfr_lo else "ZHIR"
430
- return cand
431
-
432
-
433
- def parametric_glyph_selector(G, n) -> str:
434
- """Multiobjective: combine Si, |ΔNFR|_norm and |accel|_norm with
435
- hysteresis.
436
-
437
- Base rules:
438
- - High Si ⇒ IL
439
- - Low Si ⇒ OZ if |ΔNFR| high; ZHIR if |ΔNFR| low;
440
- THOL if acceleration is high
441
- - Medium Si ⇒ NAV if |ΔNFR| high (or acceleration high),
442
- otherwise RA
443
- """
444
- nd = G.nodes[n]
445
- thr = _selector_thresholds(G)
446
- margin = get_graph_param(G, "GLYPH_SELECTOR_MARGIN")
447
-
448
- norms = G.graph.get("_sel_norms") or _norms_para_selector(G)
449
- Si, dnfr, accel = _selector_normalized_metrics(nd, norms)
450
-
451
- cand = _selector_base_choice(Si, dnfr, accel, thr)
452
-
453
- hist_cand = _apply_selector_hysteresis(nd, Si, dnfr, accel, thr, margin)
454
- if hist_cand is not None:
455
- return hist_cand
456
-
457
- score = _compute_selector_score(G, nd, Si, dnfr, accel, cand)
458
-
459
- cand = _apply_score_override(cand, score, dnfr, thr["dnfr_lo"])
460
-
461
- return _soft_grammar_prefilter(G, n, cand, dnfr, accel)
462
-
463
-
464
- def _choose_glyph(G, n, selector, use_canon, h_al, h_en, al_max, en_max):
465
- """Select the glyph to apply on node ``n``."""
466
- if h_al[n] > al_max:
467
- return Glyph.AL
468
- if h_en[n] > en_max:
469
- return Glyph.EN
470
- g = selector(G, n)
471
- if use_canon:
472
- g = enforce_canonical_grammar(G, n, g)
473
- return g
474
-
475
-
476
- # -------------------------
477
- # Step / run
478
- # -------------------------
479
-
480
-
481
- def _run_before_callbacks(
482
- G, *, step_idx: int, dt: float | None, use_Si: bool, apply_glyphs: bool
483
- ) -> None:
484
- callback_manager.invoke_callbacks(
485
- G,
486
- CallbackEvent.BEFORE_STEP.value,
487
- {
488
- "step": step_idx,
489
- "dt": dt,
490
- "use_Si": use_Si,
491
- "apply_glyphs": apply_glyphs,
492
- },
493
- )
494
-
495
-
496
- def _prepare_dnfr(G, *, use_Si: bool) -> None:
497
- """Compute ΔNFR and optionally Si for the current graph state."""
498
- compute_dnfr_cb = G.graph.get(
499
- "compute_delta_nfr", default_compute_delta_nfr
500
- )
501
- compute_dnfr_cb(G)
502
- G.graph.pop("_sel_norms", None)
503
- if use_Si:
504
- compute_Si(G, inplace=True)
505
-
506
-
507
- def _apply_selector(G):
508
- """Configure and return the glyph selector for this step."""
509
- selector = G.graph.get("glyph_selector", default_glyph_selector)
510
- if selector is parametric_glyph_selector:
511
- _norms_para_selector(G)
512
- _configure_selector_weights(G)
513
- return selector
514
-
515
-
516
- def _apply_glyphs(G, selector, hist) -> None:
517
- """Apply glyphs to nodes using ``selector`` and update history."""
518
- window = int(get_param(G, "GLYPH_HYSTERESIS_WINDOW"))
519
- use_canon = bool(
520
- get_graph_param(G, "GRAMMAR_CANON", dict).get("enabled", False)
521
- )
522
- al_max = get_graph_param(G, "AL_MAX_LAG", int)
523
- en_max = get_graph_param(G, "EN_MAX_LAG", int)
524
- h_al = hist.setdefault("since_AL", {})
525
- h_en = hist.setdefault("since_EN", {})
526
- for n, _ in G.nodes(data=True):
527
- h_al[n] = int(h_al.get(n, 0)) + 1
528
- h_en[n] = int(h_en.get(n, 0)) + 1
529
- g = _choose_glyph(
530
- G, n, selector, use_canon, h_al, h_en, al_max, en_max
531
- )
532
- apply_glyph(G, n, g, window=window)
533
- if use_canon:
534
- on_applied_glyph(G, n, g)
535
- if g == Glyph.AL:
536
- h_al[n] = 0
537
- h_en[n] = min(h_en[n], en_max)
538
- elif g == Glyph.EN:
539
- h_en[n] = 0
540
-
541
-
542
- def _update_nodes(
543
- G,
544
- *,
545
- dt: float | None,
546
- use_Si: bool,
547
- apply_glyphs: bool,
548
- step_idx: int,
549
- hist,
550
- ) -> None:
551
- _update_node_sample(G, step=step_idx)
552
- _prepare_dnfr(G, use_Si=use_Si)
553
- selector = _apply_selector(G)
554
- if apply_glyphs:
555
- _apply_glyphs(G, selector, hist)
556
- _dt = get_graph_param(G, "DT") if dt is None else float(dt)
557
- method = get_graph_param(G, "INTEGRATOR_METHOD", str)
558
- update_epi_via_nodal_equation(G, dt=_dt, method=method)
559
- for n, nd in G.nodes(data=True):
560
- apply_canonical_clamps(nd, G, n)
561
- coordinate_global_local_phase(G, None, None)
562
- adapt_vf_by_coherence(G)
563
-
564
-
565
- def _update_epi_hist(G) -> None:
566
- tau_g = int(get_param(G, "REMESH_TAU_GLOBAL"))
567
- tau_l = int(get_param(G, "REMESH_TAU_LOCAL"))
568
- tau = max(tau_g, tau_l)
569
- maxlen = max(2 * tau + 5, 64)
570
- epi_hist = G.graph.get("_epi_hist")
571
- if not isinstance(epi_hist, deque) or epi_hist.maxlen != maxlen:
572
- epi_hist = deque(list(epi_hist or [])[-maxlen:], maxlen=maxlen)
573
- G.graph["_epi_hist"] = epi_hist
574
- epi_hist.append(
575
- {n: get_attr(nd, ALIAS_EPI, 0.0) for n, nd in G.nodes(data=True)}
576
- )
577
-
578
-
579
- def _maybe_remesh(G) -> None:
580
- apply_remesh_if_globally_stable(G)
581
-
582
-
583
- def _run_validators(G) -> None:
584
- from ..validators import run_validators
585
-
586
- run_validators(G)
587
-
588
-
589
- def _run_after_callbacks(G, *, step_idx: int) -> None:
590
- h = ensure_history(G)
591
- ctx = {"step": step_idx}
592
- metric_pairs = [
593
- ("C", "C_steps"),
594
- ("stable_frac", "stable_frac"),
595
- ("phase_sync", "phase_sync"),
596
- ("glyph_disr", "glyph_load_disr"),
597
- ("Si_mean", "Si_mean"),
598
- ]
599
- for dst, src in metric_pairs:
600
- values = h.get(src)
601
- if values:
602
- ctx[dst] = values[-1]
603
- callback_manager.invoke_callbacks(G, CallbackEvent.AFTER_STEP.value, ctx)
604
-
605
-
606
- def step(
607
- G,
608
- *,
609
- dt: float | None = None,
610
- use_Si: bool = True,
611
- apply_glyphs: bool = True,
612
- ) -> None:
613
- hist = ensure_history(G)
614
- step_idx = len(hist.setdefault("C_steps", []))
615
- _run_before_callbacks(
616
- G, step_idx=step_idx, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs
617
- )
618
- _update_nodes(
619
- G,
620
- dt=dt,
621
- use_Si=use_Si,
622
- apply_glyphs=apply_glyphs,
623
- step_idx=step_idx,
624
- hist=hist,
625
- )
626
- _update_epi_hist(G)
627
- _maybe_remesh(G)
628
- _run_validators(G)
629
- _run_after_callbacks(G, step_idx=step_idx)
630
-
631
-
632
- def run(
633
- G,
634
- steps: int,
635
- *,
636
- dt: float | None = None,
637
- use_Si: bool = True,
638
- apply_glyphs: bool = True,
639
- ) -> None:
640
- steps_int = int(steps)
641
- if steps_int < 0:
642
- raise ValueError("'steps' must be non-negative")
643
- stop_cfg = get_graph_param(G, "STOP_EARLY", dict)
644
- stop_enabled = False
645
- if stop_cfg and stop_cfg.get("enabled", False):
646
- w = int(stop_cfg.get("window", 25))
647
- frac = float(stop_cfg.get("fraction", 0.90))
648
- stop_enabled = True
649
- for _ in range(steps_int):
650
- step(G, dt=dt, use_Si=use_Si, apply_glyphs=apply_glyphs)
651
- # Early-stop opcional
652
- if stop_enabled:
653
- history = ensure_history(G)
654
- series = history.get("stable_frac", [])
655
- if not isinstance(series, list):
656
- series = list(series)
657
- if len(series) >= w and all(v >= frac for v in series[-w:]):
658
- break