tnfr 4.5.1__py3-none-any.whl → 6.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. tnfr/__init__.py +270 -90
  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 +631 -0
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +732 -0
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +381 -0
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +89 -0
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +199 -0
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +322 -0
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +34 -0
  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/init.py +36 -0
  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 +228 -0
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +158 -0
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.py +31 -0
  35. tnfr/constants/init.pyi +12 -0
  36. tnfr/constants/metric.py +102 -0
  37. tnfr/constants/metric.pyi +19 -0
  38. tnfr/constants_glyphs.py +16 -0
  39. tnfr/constants_glyphs.pyi +12 -0
  40. tnfr/dynamics/__init__.py +136 -0
  41. tnfr/dynamics/__init__.pyi +83 -0
  42. tnfr/dynamics/adaptation.py +201 -0
  43. tnfr/dynamics/aliases.py +22 -0
  44. tnfr/dynamics/coordination.py +343 -0
  45. tnfr/dynamics/dnfr.py +2315 -0
  46. tnfr/dynamics/dnfr.pyi +33 -0
  47. tnfr/dynamics/integrators.py +561 -0
  48. tnfr/dynamics/integrators.pyi +35 -0
  49. tnfr/dynamics/runtime.py +521 -0
  50. tnfr/dynamics/sampling.py +34 -0
  51. tnfr/dynamics/sampling.pyi +7 -0
  52. tnfr/dynamics/selectors.py +680 -0
  53. tnfr/execution.py +216 -0
  54. tnfr/execution.pyi +65 -0
  55. tnfr/flatten.py +283 -0
  56. tnfr/flatten.pyi +28 -0
  57. tnfr/gamma.py +320 -89
  58. tnfr/gamma.pyi +40 -0
  59. tnfr/glyph_history.py +337 -0
  60. tnfr/glyph_history.pyi +53 -0
  61. tnfr/grammar.py +23 -153
  62. tnfr/grammar.pyi +13 -0
  63. tnfr/helpers/__init__.py +151 -0
  64. tnfr/helpers/__init__.pyi +66 -0
  65. tnfr/helpers/numeric.py +88 -0
  66. tnfr/helpers/numeric.pyi +12 -0
  67. tnfr/immutable.py +214 -0
  68. tnfr/immutable.pyi +37 -0
  69. tnfr/initialization.py +199 -0
  70. tnfr/initialization.pyi +73 -0
  71. tnfr/io.py +311 -0
  72. tnfr/io.pyi +11 -0
  73. tnfr/locking.py +37 -0
  74. tnfr/locking.pyi +7 -0
  75. tnfr/metrics/__init__.py +41 -0
  76. tnfr/metrics/__init__.pyi +20 -0
  77. tnfr/metrics/coherence.py +1469 -0
  78. tnfr/metrics/common.py +149 -0
  79. tnfr/metrics/common.pyi +15 -0
  80. tnfr/metrics/core.py +259 -0
  81. tnfr/metrics/core.pyi +13 -0
  82. tnfr/metrics/diagnosis.py +840 -0
  83. tnfr/metrics/diagnosis.pyi +89 -0
  84. tnfr/metrics/export.py +151 -0
  85. tnfr/metrics/glyph_timing.py +369 -0
  86. tnfr/metrics/reporting.py +152 -0
  87. tnfr/metrics/reporting.pyi +12 -0
  88. tnfr/metrics/sense_index.py +294 -0
  89. tnfr/metrics/sense_index.pyi +9 -0
  90. tnfr/metrics/trig.py +216 -0
  91. tnfr/metrics/trig.pyi +12 -0
  92. tnfr/metrics/trig_cache.py +105 -0
  93. tnfr/metrics/trig_cache.pyi +10 -0
  94. tnfr/node.py +255 -177
  95. tnfr/node.pyi +161 -0
  96. tnfr/observers.py +154 -150
  97. tnfr/observers.pyi +46 -0
  98. tnfr/ontosim.py +135 -134
  99. tnfr/ontosim.pyi +33 -0
  100. tnfr/operators/__init__.py +452 -0
  101. tnfr/operators/__init__.pyi +31 -0
  102. tnfr/operators/definitions.py +181 -0
  103. tnfr/operators/definitions.pyi +92 -0
  104. tnfr/operators/jitter.py +266 -0
  105. tnfr/operators/jitter.pyi +11 -0
  106. tnfr/operators/registry.py +80 -0
  107. tnfr/operators/registry.pyi +15 -0
  108. tnfr/operators/remesh.py +569 -0
  109. tnfr/presets.py +10 -23
  110. tnfr/presets.pyi +7 -0
  111. tnfr/py.typed +0 -0
  112. tnfr/rng.py +440 -0
  113. tnfr/rng.pyi +14 -0
  114. tnfr/selector.py +217 -0
  115. tnfr/selector.pyi +19 -0
  116. tnfr/sense.py +307 -142
  117. tnfr/sense.pyi +30 -0
  118. tnfr/structural.py +69 -164
  119. tnfr/structural.pyi +46 -0
  120. tnfr/telemetry/__init__.py +13 -0
  121. tnfr/telemetry/verbosity.py +37 -0
  122. tnfr/tokens.py +61 -0
  123. tnfr/tokens.pyi +41 -0
  124. tnfr/trace.py +520 -95
  125. tnfr/trace.pyi +68 -0
  126. tnfr/types.py +382 -17
  127. tnfr/types.pyi +145 -0
  128. tnfr/utils/__init__.py +158 -0
  129. tnfr/utils/__init__.pyi +133 -0
  130. tnfr/utils/cache.py +755 -0
  131. tnfr/utils/cache.pyi +156 -0
  132. tnfr/utils/data.py +267 -0
  133. tnfr/utils/data.pyi +73 -0
  134. tnfr/utils/graph.py +87 -0
  135. tnfr/utils/graph.pyi +10 -0
  136. tnfr/utils/init.py +746 -0
  137. tnfr/utils/init.pyi +85 -0
  138. tnfr/utils/io.py +157 -0
  139. tnfr/utils/io.pyi +10 -0
  140. tnfr/utils/validators.py +130 -0
  141. tnfr/utils/validators.pyi +19 -0
  142. tnfr/validation/__init__.py +25 -0
  143. tnfr/validation/__init__.pyi +17 -0
  144. tnfr/validation/compatibility.py +59 -0
  145. tnfr/validation/compatibility.pyi +8 -0
  146. tnfr/validation/grammar.py +149 -0
  147. tnfr/validation/grammar.pyi +11 -0
  148. tnfr/validation/rules.py +194 -0
  149. tnfr/validation/rules.pyi +18 -0
  150. tnfr/validation/syntax.py +151 -0
  151. tnfr/validation/syntax.pyi +7 -0
  152. tnfr-6.0.0.dist-info/METADATA +135 -0
  153. tnfr-6.0.0.dist-info/RECORD +157 -0
  154. tnfr/cli.py +0 -322
  155. tnfr/config.py +0 -41
  156. tnfr/constants.py +0 -277
  157. tnfr/dynamics.py +0 -814
  158. tnfr/helpers.py +0 -264
  159. tnfr/main.py +0 -47
  160. tnfr/metrics.py +0 -597
  161. tnfr/operators.py +0 -525
  162. tnfr/program.py +0 -176
  163. tnfr/scenarios.py +0 -34
  164. tnfr/validators.py +0 -38
  165. tnfr-4.5.1.dist-info/METADATA +0 -221
  166. tnfr-4.5.1.dist-info/RECORD +0 -28
  167. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  168. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  169. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,569 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import heapq
5
+ import random
6
+ from operator import ge, le
7
+ from functools import cache
8
+ from itertools import combinations
9
+ from io import StringIO
10
+ from collections import deque
11
+ from collections.abc import Hashable, Iterable, Mapping, MutableMapping, Sequence
12
+ from statistics import fmean, StatisticsError
13
+ from types import ModuleType
14
+ from typing import Any, TypedDict, cast
15
+
16
+ from .._compat import TypeAlias
17
+
18
+ from ..constants import DEFAULTS, REMESH_DEFAULTS, get_aliases, get_param
19
+ from ..helpers.numeric import kahan_sum_nd
20
+ from ..alias import get_attr, set_attr
21
+ from ..rng import make_rng
22
+ from ..callback_utils import CallbackEvent, callback_manager
23
+ from ..glyph_history import append_metric, ensure_history, current_step_idx
24
+ from ..utils import cached_import, edge_version_update
25
+
26
+ CommunityGraph: TypeAlias = Any
27
+ NetworkxModule: TypeAlias = ModuleType
28
+ CommunityModule: TypeAlias = ModuleType
29
+ RemeshEdge: TypeAlias = tuple[Hashable, Hashable]
30
+ NetworkxModules: TypeAlias = tuple[NetworkxModule, CommunityModule]
31
+
32
+
33
+ class RemeshMeta(TypedDict, total=False):
34
+ alpha: float
35
+ alpha_source: str
36
+ tau_global: int
37
+ tau_local: int
38
+ step: int | None
39
+ topo_hash: str | None
40
+ epi_mean_before: float
41
+ epi_mean_after: float
42
+ epi_checksum_before: str
43
+ epi_checksum_after: str
44
+ stable_frac_last: float
45
+ phase_sync_last: float
46
+ glyph_disr_last: float
47
+
48
+
49
+ RemeshConfigValue: TypeAlias = bool | float | int
50
+
51
+
52
+ def _as_float(value: Any, default: float = 0.0) -> float:
53
+ """Best-effort conversion to ``float`` returning ``default`` on failure."""
54
+
55
+ if value is None:
56
+ return default
57
+ try:
58
+ return float(value)
59
+ except (TypeError, ValueError):
60
+ return default
61
+
62
+
63
+ def _ordered_edge(u: Hashable, v: Hashable) -> RemeshEdge:
64
+ """Return a deterministic ordering for an undirected edge."""
65
+
66
+ return (u, v) if repr(u) <= repr(v) else (v, u)
67
+
68
+
69
+ ALIAS_EPI = get_aliases("EPI")
70
+
71
+
72
+ COOLDOWN_KEY = "REMESH_COOLDOWN_WINDOW"
73
+
74
+
75
+ @cache
76
+ def _get_networkx_modules() -> NetworkxModules:
77
+ nx = cached_import("networkx")
78
+ if nx is None:
79
+ raise ImportError(
80
+ "networkx is required for network operators; install 'networkx' "
81
+ "to enable this feature"
82
+ )
83
+ nx_comm = cached_import("networkx.algorithms", "community")
84
+ if nx_comm is None:
85
+ raise ImportError(
86
+ "networkx.algorithms.community is required for community-based "
87
+ "operations; install 'networkx' to enable this feature"
88
+ )
89
+ return cast(NetworkxModule, nx), cast(CommunityModule, nx_comm)
90
+
91
+
92
+ def _remesh_alpha_info(G: CommunityGraph) -> tuple[float, str]:
93
+ """Return ``(alpha, source)`` with explicit precedence."""
94
+ if bool(
95
+ G.graph.get("REMESH_ALPHA_HARD", REMESH_DEFAULTS["REMESH_ALPHA_HARD"])
96
+ ):
97
+ val = _as_float(
98
+ G.graph.get("REMESH_ALPHA", REMESH_DEFAULTS["REMESH_ALPHA"]),
99
+ float(REMESH_DEFAULTS["REMESH_ALPHA"]),
100
+ )
101
+ return val, "REMESH_ALPHA"
102
+ gf = G.graph.get("GLYPH_FACTORS", DEFAULTS.get("GLYPH_FACTORS", {}))
103
+ if "REMESH_alpha" in gf:
104
+ return _as_float(gf["REMESH_alpha"]), "GLYPH_FACTORS.REMESH_alpha"
105
+ if "REMESH_ALPHA" in G.graph:
106
+ return _as_float(G.graph["REMESH_ALPHA"]), "REMESH_ALPHA"
107
+ return (
108
+ float(REMESH_DEFAULTS["REMESH_ALPHA"]),
109
+ "REMESH_DEFAULTS.REMESH_ALPHA",
110
+ )
111
+
112
+
113
+ def _snapshot_topology(G: CommunityGraph, nx: NetworkxModule) -> str | None:
114
+ """Return a hash representing the current graph topology."""
115
+ try:
116
+ n_nodes = G.number_of_nodes()
117
+ n_edges = G.number_of_edges()
118
+ degs = sorted(d for _, d in G.degree())
119
+ topo_str = f"n={n_nodes};m={n_edges};deg=" + ",".join(map(str, degs))
120
+ return hashlib.sha1(topo_str.encode()).hexdigest()[:12]
121
+ except (AttributeError, TypeError, nx.NetworkXError):
122
+ return None
123
+
124
+
125
+ def _snapshot_epi(G: CommunityGraph) -> tuple[float, str]:
126
+ """Return ``(mean, checksum)`` of the node EPI values."""
127
+ buf = StringIO()
128
+ values = []
129
+ for n, data in G.nodes(data=True):
130
+ v = _as_float(get_attr(data, ALIAS_EPI, 0.0))
131
+ values.append(v)
132
+ buf.write(f"{str(n)}:{round(v, 6)};")
133
+ total = kahan_sum_nd(((v,) for v in values), dims=1)[0]
134
+ mean_val = total / len(values) if values else 0.0
135
+ checksum = hashlib.sha1(buf.getvalue().encode()).hexdigest()[:12]
136
+ return float(mean_val), checksum
137
+
138
+
139
+ def _log_remesh_event(G: CommunityGraph, meta: RemeshMeta) -> None:
140
+ """Store remesh metadata and optionally log and trigger callbacks."""
141
+ G.graph["_REMESH_META"] = meta
142
+ if G.graph.get("REMESH_LOG_EVENTS", REMESH_DEFAULTS["REMESH_LOG_EVENTS"]):
143
+ hist = G.graph.setdefault("history", {})
144
+ append_metric(hist, "remesh_events", dict(meta))
145
+ callback_manager.invoke_callbacks(
146
+ G, CallbackEvent.ON_REMESH.value, dict(meta)
147
+ )
148
+
149
+
150
+ def apply_network_remesh(G: CommunityGraph) -> None:
151
+ """Network-scale REMESH using ``_epi_hist`` with multi-scale memory."""
152
+ nx, _ = _get_networkx_modules()
153
+ tau_g = int(get_param(G, "REMESH_TAU_GLOBAL"))
154
+ tau_l = int(get_param(G, "REMESH_TAU_LOCAL"))
155
+ tau_req = max(tau_g, tau_l)
156
+ alpha, alpha_src = _remesh_alpha_info(G)
157
+ G.graph["_REMESH_ALPHA_SRC"] = alpha_src
158
+ hist = G.graph.get("_epi_hist", deque())
159
+ if len(hist) < tau_req + 1:
160
+ return
161
+
162
+ past_g = hist[-(tau_g + 1)]
163
+ past_l = hist[-(tau_l + 1)]
164
+
165
+ topo_hash = _snapshot_topology(G, nx)
166
+ epi_mean_before, epi_checksum_before = _snapshot_epi(G)
167
+
168
+ for n, nd in G.nodes(data=True):
169
+ epi_now = _as_float(get_attr(nd, ALIAS_EPI, 0.0))
170
+ epi_old_l = _as_float(past_l.get(n) if isinstance(past_l, Mapping) else None, epi_now)
171
+ epi_old_g = _as_float(past_g.get(n) if isinstance(past_g, Mapping) else None, epi_now)
172
+ mixed = (1 - alpha) * epi_now + alpha * epi_old_l
173
+ mixed = (1 - alpha) * mixed + alpha * epi_old_g
174
+ set_attr(nd, ALIAS_EPI, mixed)
175
+
176
+ epi_mean_after, epi_checksum_after = _snapshot_epi(G)
177
+
178
+ step_idx = current_step_idx(G)
179
+ meta: RemeshMeta = {
180
+ "alpha": alpha,
181
+ "alpha_source": alpha_src,
182
+ "tau_global": tau_g,
183
+ "tau_local": tau_l,
184
+ "step": step_idx,
185
+ "topo_hash": topo_hash,
186
+ "epi_mean_before": float(epi_mean_before),
187
+ "epi_mean_after": float(epi_mean_after),
188
+ "epi_checksum_before": epi_checksum_before,
189
+ "epi_checksum_after": epi_checksum_after,
190
+ }
191
+
192
+ h = ensure_history(G)
193
+ if h:
194
+ if h.get("stable_frac"):
195
+ meta["stable_frac_last"] = h["stable_frac"][-1]
196
+ if h.get("phase_sync"):
197
+ meta["phase_sync_last"] = h["phase_sync"][-1]
198
+ if h.get("glyph_load_disr"):
199
+ meta["glyph_disr_last"] = h["glyph_load_disr"][-1]
200
+
201
+ _log_remesh_event(G, meta)
202
+
203
+
204
+ def _mst_edges_from_epi(
205
+ nx: NetworkxModule,
206
+ nodes: Sequence[Hashable],
207
+ epi: Mapping[Hashable, float],
208
+ ) -> set[RemeshEdge]:
209
+ """Return MST edges based on absolute EPI distance."""
210
+ H = nx.Graph()
211
+ H.add_nodes_from(nodes)
212
+ H.add_weighted_edges_from(
213
+ (u, v, abs(epi[u] - epi[v])) for u, v in combinations(nodes, 2)
214
+ )
215
+ return {
216
+ _ordered_edge(u, v)
217
+ for u, v in nx.minimum_spanning_edges(H, data=False)
218
+ }
219
+
220
+
221
+ def _knn_edges(
222
+ nodes: Sequence[Hashable],
223
+ epi: Mapping[Hashable, float],
224
+ k_val: int,
225
+ p_rewire: float,
226
+ rnd: random.Random,
227
+ ) -> set[RemeshEdge]:
228
+ """Edges linking each node to its ``k`` nearest neighbours in EPI."""
229
+ new_edges = set()
230
+ node_set = set(nodes)
231
+ for u in nodes:
232
+ epi_u = epi[u]
233
+ neighbours = [
234
+ v
235
+ for _, v in heapq.nsmallest(
236
+ k_val,
237
+ ((abs(epi_u - epi[v]), v) for v in nodes if v != u),
238
+ )
239
+ ]
240
+ for v in neighbours:
241
+ if rnd.random() < p_rewire:
242
+ choices = list(node_set - {u, v})
243
+ if choices:
244
+ v = rnd.choice(choices)
245
+ new_edges.add(_ordered_edge(u, v))
246
+ return new_edges
247
+
248
+
249
+ def _community_graph(
250
+ comms: Iterable[Iterable[Hashable]],
251
+ epi: Mapping[Hashable, float],
252
+ nx: NetworkxModule,
253
+ ) -> CommunityGraph:
254
+ """Return community graph ``C`` with mean EPI per community."""
255
+ C = nx.Graph()
256
+ for idx, comm in enumerate(comms):
257
+ members = list(comm)
258
+ try:
259
+ epi_mean = fmean(_as_float(epi.get(n)) for n in members)
260
+ except StatisticsError:
261
+ epi_mean = 0.0
262
+ C.add_node(idx)
263
+ set_attr(C.nodes[idx], ALIAS_EPI, epi_mean)
264
+ C.nodes[idx]["members"] = members
265
+ for i, j in combinations(C.nodes(), 2):
266
+ w = abs(
267
+ _as_float(get_attr(C.nodes[i], ALIAS_EPI, 0.0))
268
+ - _as_float(get_attr(C.nodes[j], ALIAS_EPI, 0.0))
269
+ )
270
+ C.add_edge(i, j, weight=w)
271
+ return cast(CommunityGraph, C)
272
+
273
+
274
+ def _community_k_neighbor_edges(
275
+ C: CommunityGraph,
276
+ k_val: int,
277
+ p_rewire: float,
278
+ rnd: random.Random,
279
+ ) -> tuple[set[RemeshEdge], dict[int, int], list[tuple[int, int, int]]]:
280
+ """Edges linking each community to its ``k`` nearest neighbours."""
281
+ epi_vals = {n: _as_float(get_attr(C.nodes[n], ALIAS_EPI, 0.0)) for n in C.nodes()}
282
+ ordered = sorted(C.nodes(), key=lambda v: epi_vals[v])
283
+ new_edges = set()
284
+ attempts = {n: 0 for n in C.nodes()}
285
+ rewired = []
286
+ node_set = set(C.nodes())
287
+ for idx, u in enumerate(ordered):
288
+ epi_u = epi_vals[u]
289
+ left = idx - 1
290
+ right = idx + 1
291
+ added = 0
292
+ while added < k_val and (left >= 0 or right < len(ordered)):
293
+ if left < 0:
294
+ v = ordered[right]
295
+ right += 1
296
+ elif right >= len(ordered):
297
+ v = ordered[left]
298
+ left -= 1
299
+ else:
300
+ if abs(epi_u - epi_vals[ordered[left]]) <= abs(
301
+ epi_vals[ordered[right]] - epi_u
302
+ ):
303
+ v = ordered[left]
304
+ left -= 1
305
+ else:
306
+ v = ordered[right]
307
+ right += 1
308
+ original_v = v
309
+ rewired_now = False
310
+ if rnd.random() < p_rewire:
311
+ choices = list(node_set - {u, original_v})
312
+ if choices:
313
+ v = rnd.choice(choices)
314
+ rewired_now = True
315
+ new_edges.add(_ordered_edge(u, v))
316
+ attempts[u] += 1
317
+ if rewired_now:
318
+ rewired.append((u, original_v, v))
319
+ added += 1
320
+ return new_edges, attempts, rewired
321
+
322
+
323
+ def _community_remesh(
324
+ G: CommunityGraph,
325
+ epi: Mapping[Hashable, float],
326
+ k_val: int,
327
+ p_rewire: float,
328
+ rnd: random.Random,
329
+ nx: NetworkxModule,
330
+ nx_comm: CommunityModule,
331
+ mst_edges: Iterable[RemeshEdge],
332
+ n_before: int,
333
+ ) -> None:
334
+ """Remesh ``G`` replacing nodes by modular communities."""
335
+ comms = list(nx_comm.greedy_modularity_communities(G))
336
+ if len(comms) <= 1:
337
+ with edge_version_update(G):
338
+ G.clear_edges()
339
+ G.add_edges_from(mst_edges)
340
+ return
341
+ C = _community_graph(comms, epi, nx)
342
+ mst_c = nx.minimum_spanning_tree(C, weight="weight")
343
+ new_edges: set[RemeshEdge] = {
344
+ _ordered_edge(u, v) for u, v in mst_c.edges()
345
+ }
346
+ extra_edges, attempts, rewired_edges = _community_k_neighbor_edges(
347
+ C, k_val, p_rewire, rnd
348
+ )
349
+ new_edges |= extra_edges
350
+
351
+ extra_degrees = {idx: 0 for idx in C.nodes()}
352
+ for u, v in extra_edges:
353
+ extra_degrees[u] += 1
354
+ extra_degrees[v] += 1
355
+
356
+ with edge_version_update(G):
357
+ G.clear_edges()
358
+ G.remove_nodes_from(list(G.nodes()))
359
+ for idx in C.nodes():
360
+ data = dict(C.nodes[idx])
361
+ G.add_node(idx, **data)
362
+ G.add_edges_from(new_edges)
363
+
364
+ if G.graph.get("REMESH_LOG_EVENTS", REMESH_DEFAULTS["REMESH_LOG_EVENTS"]):
365
+ hist = G.graph.setdefault("history", {})
366
+ mapping = {idx: C.nodes[idx].get("members", []) for idx in C.nodes()}
367
+ append_metric(
368
+ hist,
369
+ "remesh_events",
370
+ {
371
+ "mode": "community",
372
+ "n_before": n_before,
373
+ "n_after": G.number_of_nodes(),
374
+ "mapping": mapping,
375
+ "k": int(k_val),
376
+ "p_rewire": float(p_rewire),
377
+ "extra_edges_added": len(extra_edges),
378
+ "extra_edge_attempts": attempts,
379
+ "extra_edge_degrees": extra_degrees,
380
+ "rewired_edges": [
381
+ {"source": int(u), "from": int(v0), "to": int(v1)}
382
+ for u, v0, v1 in rewired_edges
383
+ ],
384
+ },
385
+ )
386
+
387
+
388
+ def apply_topological_remesh(
389
+ G: CommunityGraph,
390
+ mode: str | None = None,
391
+ *,
392
+ k: int | None = None,
393
+ p_rewire: float = 0.2,
394
+ seed: int | None = None,
395
+ ) -> None:
396
+ """Approximate topological remeshing.
397
+
398
+ When ``seed`` is ``None`` the RNG draws its base seed from
399
+ ``G.graph['RANDOM_SEED']`` to keep runs reproducible.
400
+ """
401
+ nodes = list(G.nodes())
402
+ n_before = len(nodes)
403
+ if n_before <= 1:
404
+ return
405
+ if seed is None:
406
+ base_seed = int(G.graph.get("RANDOM_SEED", 0))
407
+ else:
408
+ base_seed = int(seed)
409
+ rnd = make_rng(base_seed, -2, G)
410
+
411
+ if mode is None:
412
+ mode = str(
413
+ G.graph.get(
414
+ "REMESH_MODE", REMESH_DEFAULTS.get("REMESH_MODE", "knn")
415
+ )
416
+ )
417
+ mode = str(mode)
418
+ nx, nx_comm = _get_networkx_modules()
419
+ epi = {n: _as_float(get_attr(G.nodes[n], ALIAS_EPI, 0.0)) for n in nodes}
420
+ mst_edges = _mst_edges_from_epi(nx, nodes, epi)
421
+ default_k = int(
422
+ G.graph.get(
423
+ "REMESH_COMMUNITY_K", REMESH_DEFAULTS.get("REMESH_COMMUNITY_K", 2)
424
+ )
425
+ )
426
+ k_val = max(1, int(k) if k is not None else default_k)
427
+
428
+ if mode == "community":
429
+ _community_remesh(
430
+ G,
431
+ epi,
432
+ k_val,
433
+ p_rewire,
434
+ rnd,
435
+ nx,
436
+ nx_comm,
437
+ mst_edges,
438
+ n_before,
439
+ )
440
+ return
441
+
442
+ new_edges = set(mst_edges)
443
+ if mode == "knn":
444
+ new_edges |= _knn_edges(nodes, epi, k_val, p_rewire, rnd)
445
+
446
+ with edge_version_update(G):
447
+ G.clear_edges()
448
+ G.add_edges_from(new_edges)
449
+
450
+
451
+ def _extra_gating_ok(
452
+ hist: MutableMapping[str, Sequence[float]],
453
+ cfg: Mapping[str, RemeshConfigValue],
454
+ w_estab: int,
455
+ ) -> bool:
456
+ """Check additional stability gating conditions."""
457
+ checks = [
458
+ ("phase_sync", "REMESH_MIN_PHASE_SYNC", ge),
459
+ ("glyph_load_disr", "REMESH_MAX_GLYPH_DISR", le),
460
+ ("sense_sigma_mag", "REMESH_MIN_SIGMA_MAG", ge),
461
+ ("kuramoto_R", "REMESH_MIN_KURAMOTO_R", ge),
462
+ ("Si_hi_frac", "REMESH_MIN_SI_HI_FRAC", ge),
463
+ ]
464
+ for hist_key, cfg_key, op in checks:
465
+ series = hist.get(hist_key)
466
+ if series is not None and len(series) >= w_estab:
467
+ win = series[-w_estab:]
468
+ avg = sum(win) / len(win)
469
+ threshold = _as_float(cfg[cfg_key])
470
+ if not op(avg, threshold):
471
+ return False
472
+ return True
473
+
474
+
475
+ def apply_remesh_if_globally_stable(
476
+ G: CommunityGraph,
477
+ stable_step_window: int | None = None,
478
+ **kwargs: Any,
479
+ ) -> None:
480
+ if kwargs:
481
+ unexpected = ", ".join(sorted(kwargs))
482
+ raise TypeError(
483
+ "apply_remesh_if_globally_stable() got unexpected keyword argument(s): "
484
+ f"{unexpected}"
485
+ )
486
+
487
+ params = [
488
+ (
489
+ "REMESH_STABILITY_WINDOW",
490
+ int,
491
+ REMESH_DEFAULTS["REMESH_STABILITY_WINDOW"],
492
+ ),
493
+ (
494
+ "REMESH_REQUIRE_STABILITY",
495
+ bool,
496
+ REMESH_DEFAULTS["REMESH_REQUIRE_STABILITY"],
497
+ ),
498
+ (
499
+ "REMESH_MIN_PHASE_SYNC",
500
+ float,
501
+ REMESH_DEFAULTS["REMESH_MIN_PHASE_SYNC"],
502
+ ),
503
+ (
504
+ "REMESH_MAX_GLYPH_DISR",
505
+ float,
506
+ REMESH_DEFAULTS["REMESH_MAX_GLYPH_DISR"],
507
+ ),
508
+ (
509
+ "REMESH_MIN_SIGMA_MAG",
510
+ float,
511
+ REMESH_DEFAULTS["REMESH_MIN_SIGMA_MAG"],
512
+ ),
513
+ (
514
+ "REMESH_MIN_KURAMOTO_R",
515
+ float,
516
+ REMESH_DEFAULTS["REMESH_MIN_KURAMOTO_R"],
517
+ ),
518
+ (
519
+ "REMESH_MIN_SI_HI_FRAC",
520
+ float,
521
+ REMESH_DEFAULTS["REMESH_MIN_SI_HI_FRAC"],
522
+ ),
523
+ (COOLDOWN_KEY, int, REMESH_DEFAULTS[COOLDOWN_KEY]),
524
+ ("REMESH_COOLDOWN_TS", float, REMESH_DEFAULTS["REMESH_COOLDOWN_TS"]),
525
+ ]
526
+ cfg = {}
527
+ for key, conv, _default in params:
528
+ cfg[key] = conv(get_param(G, key))
529
+ frac_req = _as_float(get_param(G, "FRACTION_STABLE_REMESH"))
530
+ w_estab = (
531
+ stable_step_window
532
+ if stable_step_window is not None
533
+ else cfg["REMESH_STABILITY_WINDOW"]
534
+ )
535
+
536
+ hist = ensure_history(G)
537
+ sf = hist.setdefault("stable_frac", [])
538
+ if len(sf) < w_estab:
539
+ return
540
+ win_sf = sf[-w_estab:]
541
+ if not all(v >= frac_req for v in win_sf):
542
+ return
543
+ if cfg["REMESH_REQUIRE_STABILITY"] and not _extra_gating_ok(
544
+ hist, cfg, w_estab
545
+ ):
546
+ return
547
+
548
+ last = G.graph.get("_last_remesh_step", -(10**9))
549
+ step_idx = len(sf)
550
+ if step_idx - last < cfg[COOLDOWN_KEY]:
551
+ return
552
+ t_now = _as_float(G.graph.get("_t", 0.0))
553
+ last_ts = _as_float(G.graph.get("_last_remesh_ts", -1e12))
554
+ if (
555
+ cfg["REMESH_COOLDOWN_TS"] > 0
556
+ and (t_now - last_ts) < cfg["REMESH_COOLDOWN_TS"]
557
+ ):
558
+ return
559
+
560
+ apply_network_remesh(G)
561
+ G.graph["_last_remesh_step"] = step_idx
562
+ G.graph["_last_remesh_ts"] = t_now
563
+
564
+
565
+ __all__ = [
566
+ "apply_network_remesh",
567
+ "apply_topological_remesh",
568
+ "apply_remesh_if_globally_stable",
569
+ ]
tnfr/presets.py CHANGED
@@ -1,28 +1,15 @@
1
+ """Backward compatibility shim for configuration presets."""
2
+
1
3
  from __future__ import annotations
2
- from .program import seq, block, wait, ejemplo_canonico_basico
3
4
 
5
+ import warnings
4
6
 
5
- _PRESETS = {
6
- "arranque_resonante": seq("A’L", "E’N", "I’L", "R’A", "VA’L", "U’M", wait(3), "SH’A"),
7
- "mutacion_contenida": seq("A’L", "E’N", block("O’Z", "Z’HIR", "I’L", repeat=2), "R’A", "SH’A"),
8
- "exploracion_acople": seq(
9
- "A’L",
10
- "E’N",
11
- "I’L",
12
- "VA’L",
13
- "U’M",
14
- block("O’Z", "NA’V", "I’L", repeat=1),
15
- "R’A",
16
- "SH’A",
17
- ),
18
- "ejemplo_canonico": ejemplo_canonico_basico(),
19
- # Topologías fractales: expansión/contracción modular
20
- "fractal_expand": seq(block("T’HOL", "VA’L", "U’M", repeat=2, close="NU’L"), "R’A"),
21
- "fractal_contract": seq(block("T’HOL", "NU’L", "U’M", repeat=2, close="SH’A"), "R’A"),
22
- }
7
+ from .config.presets import get_preset
23
8
 
9
+ warnings.warn(
10
+ "'tnfr.presets' is deprecated; use 'tnfr.config.presets' instead",
11
+ DeprecationWarning,
12
+ stacklevel=2,
13
+ )
24
14
 
25
- def get_preset(name: str):
26
- if name not in _PRESETS:
27
- raise KeyError(f"Preset no encontrado: {name}")
28
- return _PRESETS[name]
15
+ __all__ = ("get_preset",)
tnfr/presets.pyi ADDED
@@ -0,0 +1,7 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ get_preset: Any
tnfr/py.typed ADDED
File without changes