tnfr 4.5.2__py3-none-any.whl → 7.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.

Potentially problematic release.


This version of tnfr might be problematic. Click here for more details.

Files changed (195) hide show
  1. tnfr/__init__.py +275 -51
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +117 -31
  8. tnfr/alias.pyi +108 -0
  9. tnfr/cache.py +6 -572
  10. tnfr/cache.pyi +16 -0
  11. tnfr/callback_utils.py +16 -38
  12. tnfr/callback_utils.pyi +79 -0
  13. tnfr/cli/__init__.py +34 -14
  14. tnfr/cli/__init__.pyi +26 -0
  15. tnfr/cli/arguments.py +211 -28
  16. tnfr/cli/arguments.pyi +27 -0
  17. tnfr/cli/execution.py +470 -50
  18. tnfr/cli/execution.pyi +70 -0
  19. tnfr/cli/utils.py +18 -3
  20. tnfr/cli/utils.pyi +8 -0
  21. tnfr/config/__init__.py +13 -0
  22. tnfr/config/__init__.pyi +10 -0
  23. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  24. tnfr/config/constants.pyi +12 -0
  25. tnfr/config/feature_flags.py +83 -0
  26. tnfr/{config.py → config/init.py} +11 -7
  27. tnfr/config/init.pyi +8 -0
  28. tnfr/config/operator_names.py +93 -0
  29. tnfr/config/operator_names.pyi +28 -0
  30. tnfr/config/presets.py +84 -0
  31. tnfr/config/presets.pyi +7 -0
  32. tnfr/constants/__init__.py +80 -29
  33. tnfr/constants/__init__.pyi +92 -0
  34. tnfr/constants/aliases.py +31 -0
  35. tnfr/constants/core.py +4 -4
  36. tnfr/constants/core.pyi +17 -0
  37. tnfr/constants/init.py +1 -1
  38. tnfr/constants/init.pyi +12 -0
  39. tnfr/constants/metric.py +7 -15
  40. tnfr/constants/metric.pyi +19 -0
  41. tnfr/dynamics/__init__.py +165 -633
  42. tnfr/dynamics/__init__.pyi +82 -0
  43. tnfr/dynamics/adaptation.py +267 -0
  44. tnfr/dynamics/aliases.py +23 -0
  45. tnfr/dynamics/coordination.py +385 -0
  46. tnfr/dynamics/dnfr.py +2283 -400
  47. tnfr/dynamics/dnfr.pyi +24 -0
  48. tnfr/dynamics/integrators.py +406 -98
  49. tnfr/dynamics/integrators.pyi +34 -0
  50. tnfr/dynamics/runtime.py +881 -0
  51. tnfr/dynamics/sampling.py +10 -5
  52. tnfr/dynamics/sampling.pyi +7 -0
  53. tnfr/dynamics/selectors.py +719 -0
  54. tnfr/execution.py +70 -48
  55. tnfr/execution.pyi +45 -0
  56. tnfr/flatten.py +13 -9
  57. tnfr/flatten.pyi +21 -0
  58. tnfr/gamma.py +66 -53
  59. tnfr/gamma.pyi +34 -0
  60. tnfr/glyph_history.py +110 -52
  61. tnfr/glyph_history.pyi +35 -0
  62. tnfr/glyph_runtime.py +16 -0
  63. tnfr/glyph_runtime.pyi +9 -0
  64. tnfr/immutable.py +69 -28
  65. tnfr/immutable.pyi +34 -0
  66. tnfr/initialization.py +16 -16
  67. tnfr/initialization.pyi +65 -0
  68. tnfr/io.py +6 -240
  69. tnfr/io.pyi +16 -0
  70. tnfr/locking.pyi +7 -0
  71. tnfr/mathematics/__init__.py +81 -0
  72. tnfr/mathematics/backend.py +426 -0
  73. tnfr/mathematics/dynamics.py +398 -0
  74. tnfr/mathematics/epi.py +254 -0
  75. tnfr/mathematics/generators.py +222 -0
  76. tnfr/mathematics/metrics.py +119 -0
  77. tnfr/mathematics/operators.py +233 -0
  78. tnfr/mathematics/operators_factory.py +71 -0
  79. tnfr/mathematics/projection.py +78 -0
  80. tnfr/mathematics/runtime.py +173 -0
  81. tnfr/mathematics/spaces.py +247 -0
  82. tnfr/mathematics/transforms.py +292 -0
  83. tnfr/metrics/__init__.py +10 -10
  84. tnfr/metrics/__init__.pyi +20 -0
  85. tnfr/metrics/coherence.py +993 -324
  86. tnfr/metrics/common.py +23 -16
  87. tnfr/metrics/common.pyi +46 -0
  88. tnfr/metrics/core.py +251 -35
  89. tnfr/metrics/core.pyi +13 -0
  90. tnfr/metrics/diagnosis.py +708 -111
  91. tnfr/metrics/diagnosis.pyi +85 -0
  92. tnfr/metrics/export.py +27 -15
  93. tnfr/metrics/glyph_timing.py +232 -42
  94. tnfr/metrics/reporting.py +33 -22
  95. tnfr/metrics/reporting.pyi +12 -0
  96. tnfr/metrics/sense_index.py +987 -43
  97. tnfr/metrics/sense_index.pyi +9 -0
  98. tnfr/metrics/trig.py +214 -23
  99. tnfr/metrics/trig.pyi +13 -0
  100. tnfr/metrics/trig_cache.py +115 -22
  101. tnfr/metrics/trig_cache.pyi +10 -0
  102. tnfr/node.py +542 -136
  103. tnfr/node.pyi +178 -0
  104. tnfr/observers.py +152 -35
  105. tnfr/observers.pyi +31 -0
  106. tnfr/ontosim.py +23 -19
  107. tnfr/ontosim.pyi +28 -0
  108. tnfr/operators/__init__.py +601 -82
  109. tnfr/operators/__init__.pyi +45 -0
  110. tnfr/operators/definitions.py +513 -0
  111. tnfr/operators/definitions.pyi +78 -0
  112. tnfr/operators/grammar.py +760 -0
  113. tnfr/operators/jitter.py +107 -38
  114. tnfr/operators/jitter.pyi +11 -0
  115. tnfr/operators/registry.py +75 -0
  116. tnfr/operators/registry.pyi +13 -0
  117. tnfr/operators/remesh.py +149 -88
  118. tnfr/py.typed +0 -0
  119. tnfr/rng.py +46 -143
  120. tnfr/rng.pyi +14 -0
  121. tnfr/schemas/__init__.py +8 -0
  122. tnfr/schemas/grammar.json +94 -0
  123. tnfr/selector.py +25 -19
  124. tnfr/selector.pyi +19 -0
  125. tnfr/sense.py +72 -62
  126. tnfr/sense.pyi +23 -0
  127. tnfr/structural.py +522 -262
  128. tnfr/structural.pyi +69 -0
  129. tnfr/telemetry/__init__.py +35 -0
  130. tnfr/telemetry/cache_metrics.py +226 -0
  131. tnfr/telemetry/nu_f.py +423 -0
  132. tnfr/telemetry/nu_f.pyi +123 -0
  133. tnfr/telemetry/verbosity.py +37 -0
  134. tnfr/tokens.py +1 -3
  135. tnfr/tokens.pyi +36 -0
  136. tnfr/trace.py +270 -113
  137. tnfr/trace.pyi +40 -0
  138. tnfr/types.py +574 -6
  139. tnfr/types.pyi +331 -0
  140. tnfr/units.py +69 -0
  141. tnfr/units.pyi +16 -0
  142. tnfr/utils/__init__.py +217 -0
  143. tnfr/utils/__init__.pyi +202 -0
  144. tnfr/utils/cache.py +2395 -0
  145. tnfr/utils/cache.pyi +468 -0
  146. tnfr/utils/chunks.py +104 -0
  147. tnfr/utils/chunks.pyi +21 -0
  148. tnfr/{collections_utils.py → utils/data.py} +147 -90
  149. tnfr/utils/data.pyi +64 -0
  150. tnfr/utils/graph.py +85 -0
  151. tnfr/utils/graph.pyi +10 -0
  152. tnfr/utils/init.py +770 -0
  153. tnfr/utils/init.pyi +78 -0
  154. tnfr/utils/io.py +456 -0
  155. tnfr/{helpers → utils}/numeric.py +51 -24
  156. tnfr/utils/numeric.pyi +21 -0
  157. tnfr/validation/__init__.py +113 -0
  158. tnfr/validation/__init__.pyi +77 -0
  159. tnfr/validation/compatibility.py +95 -0
  160. tnfr/validation/compatibility.pyi +6 -0
  161. tnfr/validation/grammar.py +71 -0
  162. tnfr/validation/grammar.pyi +40 -0
  163. tnfr/validation/graph.py +138 -0
  164. tnfr/validation/graph.pyi +17 -0
  165. tnfr/validation/rules.py +281 -0
  166. tnfr/validation/rules.pyi +55 -0
  167. tnfr/validation/runtime.py +263 -0
  168. tnfr/validation/runtime.pyi +31 -0
  169. tnfr/validation/soft_filters.py +170 -0
  170. tnfr/validation/soft_filters.pyi +37 -0
  171. tnfr/validation/spectral.py +159 -0
  172. tnfr/validation/spectral.pyi +46 -0
  173. tnfr/validation/syntax.py +40 -0
  174. tnfr/validation/syntax.pyi +10 -0
  175. tnfr/validation/window.py +39 -0
  176. tnfr/validation/window.pyi +1 -0
  177. tnfr/viz/__init__.py +9 -0
  178. tnfr/viz/matplotlib.py +246 -0
  179. tnfr-7.0.0.dist-info/METADATA +179 -0
  180. tnfr-7.0.0.dist-info/RECORD +185 -0
  181. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  182. tnfr/grammar.py +0 -344
  183. tnfr/graph_utils.py +0 -84
  184. tnfr/helpers/__init__.py +0 -71
  185. tnfr/import_utils.py +0 -228
  186. tnfr/json_utils.py +0 -162
  187. tnfr/logging_utils.py +0 -116
  188. tnfr/presets.py +0 -60
  189. tnfr/validators.py +0 -84
  190. tnfr/value_utils.py +0 -59
  191. tnfr-4.5.2.dist-info/METADATA +0 -379
  192. tnfr-4.5.2.dist-info/RECORD +0 -67
  193. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  194. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  195. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/operators/remesh.py CHANGED
@@ -1,27 +1,61 @@
1
+ """Adaptive remeshing operators preserving TNFR structural coherence."""
2
+
1
3
  from __future__ import annotations
4
+
2
5
  import hashlib
3
6
  import heapq
4
- from operator import ge, le
7
+ import random
8
+ from collections import deque
9
+ from collections.abc import Hashable, Iterable, Mapping, MutableMapping, Sequence
5
10
  from functools import cache
6
- from itertools import combinations
7
11
  from io import StringIO
8
- from collections import deque
9
- from statistics import fmean, StatisticsError
12
+ from itertools import combinations
13
+ from operator import ge, le
14
+ from statistics import StatisticsError, fmean
15
+ from types import ModuleType
16
+ from typing import TYPE_CHECKING, Any, cast
10
17
 
11
- from ..cache import edge_version_update
12
- from ..constants import DEFAULTS, REMESH_DEFAULTS, get_aliases, get_param
13
- from ..helpers.numeric import kahan_sum_nd
18
+ from .._compat import TypeAlias
14
19
  from ..alias import get_attr, set_attr
20
+ from ..constants import DEFAULTS, REMESH_DEFAULTS, get_param
21
+ from ..constants.aliases import ALIAS_EPI
15
22
  from ..rng import make_rng
16
- from ..callback_utils import CallbackEvent, callback_manager
17
- from ..glyph_history import append_metric, ensure_history, current_step_idx
18
- from ..import_utils import cached_import
23
+ from ..types import RemeshMeta
24
+ from ..utils import cached_import, edge_version_update, kahan_sum_nd
25
+
26
+ if TYPE_CHECKING: # pragma: no cover - type checking only
27
+ from ..callback_utils import CallbackEvent, CallbackManager
28
+
29
+ CommunityGraph: TypeAlias = Any
30
+ NetworkxModule: TypeAlias = ModuleType
31
+ CommunityModule: TypeAlias = ModuleType
32
+ RemeshEdge: TypeAlias = tuple[Hashable, Hashable]
33
+ NetworkxModules: TypeAlias = tuple[NetworkxModule, CommunityModule]
34
+ RemeshConfigValue: TypeAlias = bool | float | int
35
+
36
+
37
+ def _as_float(value: Any, default: float = 0.0) -> float:
38
+ """Best-effort conversion to ``float`` returning ``default`` on failure."""
39
+
40
+ if value is None:
41
+ return default
42
+ try:
43
+ return float(value)
44
+ except (TypeError, ValueError):
45
+ return default
46
+
47
+
48
+ def _ordered_edge(u: Hashable, v: Hashable) -> RemeshEdge:
49
+ """Return a deterministic ordering for an undirected edge."""
19
50
 
20
- ALIAS_EPI = get_aliases("EPI")
51
+ return (u, v) if repr(u) <= repr(v) else (v, u)
52
+
53
+
54
+ COOLDOWN_KEY = "REMESH_COOLDOWN_WINDOW"
21
55
 
22
56
 
23
57
  @cache
24
- def _get_networkx_modules():
58
+ def _get_networkx_modules() -> NetworkxModules:
25
59
  nx = cached_import("networkx")
26
60
  if nx is None:
27
61
  raise ImportError(
@@ -34,30 +68,29 @@ def _get_networkx_modules():
34
68
  "networkx.algorithms.community is required for community-based "
35
69
  "operations; install 'networkx' to enable this feature"
36
70
  )
37
- return nx, nx_comm
71
+ return cast(NetworkxModule, nx), cast(CommunityModule, nx_comm)
38
72
 
39
73
 
40
- def _remesh_alpha_info(G):
74
+ def _remesh_alpha_info(G: CommunityGraph) -> tuple[float, str]:
41
75
  """Return ``(alpha, source)`` with explicit precedence."""
42
- if bool(
43
- G.graph.get("REMESH_ALPHA_HARD", REMESH_DEFAULTS["REMESH_ALPHA_HARD"])
44
- ):
45
- val = float(
46
- G.graph.get("REMESH_ALPHA", REMESH_DEFAULTS["REMESH_ALPHA"])
76
+ if bool(G.graph.get("REMESH_ALPHA_HARD", REMESH_DEFAULTS["REMESH_ALPHA_HARD"])):
77
+ val = _as_float(
78
+ G.graph.get("REMESH_ALPHA", REMESH_DEFAULTS["REMESH_ALPHA"]),
79
+ float(REMESH_DEFAULTS["REMESH_ALPHA"]),
47
80
  )
48
81
  return val, "REMESH_ALPHA"
49
82
  gf = G.graph.get("GLYPH_FACTORS", DEFAULTS.get("GLYPH_FACTORS", {}))
50
83
  if "REMESH_alpha" in gf:
51
- return float(gf["REMESH_alpha"]), "GLYPH_FACTORS.REMESH_alpha"
84
+ return _as_float(gf["REMESH_alpha"]), "GLYPH_FACTORS.REMESH_alpha"
52
85
  if "REMESH_ALPHA" in G.graph:
53
- return float(G.graph["REMESH_ALPHA"]), "REMESH_ALPHA"
86
+ return _as_float(G.graph["REMESH_ALPHA"]), "REMESH_ALPHA"
54
87
  return (
55
88
  float(REMESH_DEFAULTS["REMESH_ALPHA"]),
56
89
  "REMESH_DEFAULTS.REMESH_ALPHA",
57
90
  )
58
91
 
59
92
 
60
- def _snapshot_topology(G, nx):
93
+ def _snapshot_topology(G: CommunityGraph, nx: NetworkxModule) -> str | None:
61
94
  """Return a hash representing the current graph topology."""
62
95
  try:
63
96
  n_nodes = G.number_of_nodes()
@@ -69,12 +102,12 @@ def _snapshot_topology(G, nx):
69
102
  return None
70
103
 
71
104
 
72
- def _snapshot_epi(G):
105
+ def _snapshot_epi(G: CommunityGraph) -> tuple[float, str]:
73
106
  """Return ``(mean, checksum)`` of the node EPI values."""
74
107
  buf = StringIO()
75
108
  values = []
76
109
  for n, data in G.nodes(data=True):
77
- v = float(get_attr(data, ALIAS_EPI, 0.0))
110
+ v = _as_float(get_attr(data, ALIAS_EPI, 0.0))
78
111
  values.append(v)
79
112
  buf.write(f"{str(n)}:{round(v, 6)};")
80
113
  total = kahan_sum_nd(((v,) for v in values), dims=1)[0]
@@ -83,19 +116,21 @@ def _snapshot_epi(G):
83
116
  return float(mean_val), checksum
84
117
 
85
118
 
86
- def _log_remesh_event(G, meta):
119
+ def _log_remesh_event(G: CommunityGraph, meta: RemeshMeta) -> None:
87
120
  """Store remesh metadata and optionally log and trigger callbacks."""
121
+ from ..callback_utils import CallbackEvent, callback_manager
122
+ from ..glyph_history import append_metric
123
+
88
124
  G.graph["_REMESH_META"] = meta
89
125
  if G.graph.get("REMESH_LOG_EVENTS", REMESH_DEFAULTS["REMESH_LOG_EVENTS"]):
90
126
  hist = G.graph.setdefault("history", {})
91
127
  append_metric(hist, "remesh_events", dict(meta))
92
- callback_manager.invoke_callbacks(
93
- G, CallbackEvent.ON_REMESH.value, dict(meta)
94
- )
128
+ callback_manager.invoke_callbacks(G, CallbackEvent.ON_REMESH.value, dict(meta))
95
129
 
96
130
 
97
- def apply_network_remesh(G) -> None:
131
+ def apply_network_remesh(G: CommunityGraph) -> None:
98
132
  """Network-scale REMESH using ``_epi_hist`` with multi-scale memory."""
133
+ from ..glyph_history import current_step_idx, ensure_history
99
134
  nx, _ = _get_networkx_modules()
100
135
  tau_g = int(get_param(G, "REMESH_TAU_GLOBAL"))
101
136
  tau_l = int(get_param(G, "REMESH_TAU_LOCAL"))
@@ -113,9 +148,13 @@ def apply_network_remesh(G) -> None:
113
148
  epi_mean_before, epi_checksum_before = _snapshot_epi(G)
114
149
 
115
150
  for n, nd in G.nodes(data=True):
116
- epi_now = get_attr(nd, ALIAS_EPI, 0.0)
117
- epi_old_l = float(past_l.get(n, epi_now))
118
- epi_old_g = float(past_g.get(n, epi_now))
151
+ epi_now = _as_float(get_attr(nd, ALIAS_EPI, 0.0))
152
+ epi_old_l = _as_float(
153
+ past_l.get(n) if isinstance(past_l, Mapping) else None, epi_now
154
+ )
155
+ epi_old_g = _as_float(
156
+ past_g.get(n) if isinstance(past_g, Mapping) else None, epi_now
157
+ )
119
158
  mixed = (1 - alpha) * epi_now + alpha * epi_old_l
120
159
  mixed = (1 - alpha) * mixed + alpha * epi_old_g
121
160
  set_attr(nd, ALIAS_EPI, mixed)
@@ -123,7 +162,7 @@ def apply_network_remesh(G) -> None:
123
162
  epi_mean_after, epi_checksum_after = _snapshot_epi(G)
124
163
 
125
164
  step_idx = current_step_idx(G)
126
- meta = {
165
+ meta: RemeshMeta = {
127
166
  "alpha": alpha,
128
167
  "alpha_source": alpha_src,
129
168
  "tau_global": tau_g,
@@ -148,20 +187,27 @@ def apply_network_remesh(G) -> None:
148
187
  _log_remesh_event(G, meta)
149
188
 
150
189
 
151
- def _mst_edges_from_epi(nx, nodes, epi):
190
+ def _mst_edges_from_epi(
191
+ nx: NetworkxModule,
192
+ nodes: Sequence[Hashable],
193
+ epi: Mapping[Hashable, float],
194
+ ) -> set[RemeshEdge]:
152
195
  """Return MST edges based on absolute EPI distance."""
153
196
  H = nx.Graph()
154
197
  H.add_nodes_from(nodes)
155
198
  H.add_weighted_edges_from(
156
199
  (u, v, abs(epi[u] - epi[v])) for u, v in combinations(nodes, 2)
157
200
  )
158
- return {
159
- tuple(sorted((u, v)))
160
- for u, v in nx.minimum_spanning_edges(H, data=False)
161
- }
201
+ return {_ordered_edge(u, v) for u, v in nx.minimum_spanning_edges(H, data=False)}
162
202
 
163
203
 
164
- def _knn_edges(nodes, epi, k_val, p_rewire, rnd):
204
+ def _knn_edges(
205
+ nodes: Sequence[Hashable],
206
+ epi: Mapping[Hashable, float],
207
+ k_val: int,
208
+ p_rewire: float,
209
+ rnd: random.Random,
210
+ ) -> set[RemeshEdge]:
165
211
  """Edges linking each node to its ``k`` nearest neighbours in EPI."""
166
212
  new_edges = set()
167
213
  node_set = set(nodes)
@@ -179,17 +225,21 @@ def _knn_edges(nodes, epi, k_val, p_rewire, rnd):
179
225
  choices = list(node_set - {u, v})
180
226
  if choices:
181
227
  v = rnd.choice(choices)
182
- new_edges.add(tuple(sorted((u, v))))
228
+ new_edges.add(_ordered_edge(u, v))
183
229
  return new_edges
184
230
 
185
231
 
186
- def _community_graph(comms, epi, nx):
232
+ def _community_graph(
233
+ comms: Iterable[Iterable[Hashable]],
234
+ epi: Mapping[Hashable, float],
235
+ nx: NetworkxModule,
236
+ ) -> CommunityGraph:
187
237
  """Return community graph ``C`` with mean EPI per community."""
188
238
  C = nx.Graph()
189
239
  for idx, comm in enumerate(comms):
190
240
  members = list(comm)
191
241
  try:
192
- epi_mean = fmean(epi[n] for n in members)
242
+ epi_mean = fmean(_as_float(epi.get(n)) for n in members)
193
243
  except StatisticsError:
194
244
  epi_mean = 0.0
195
245
  C.add_node(idx)
@@ -197,16 +247,21 @@ def _community_graph(comms, epi, nx):
197
247
  C.nodes[idx]["members"] = members
198
248
  for i, j in combinations(C.nodes(), 2):
199
249
  w = abs(
200
- get_attr(C.nodes[i], ALIAS_EPI, 0.0)
201
- - get_attr(C.nodes[j], ALIAS_EPI, 0.0)
250
+ _as_float(get_attr(C.nodes[i], ALIAS_EPI, 0.0))
251
+ - _as_float(get_attr(C.nodes[j], ALIAS_EPI, 0.0))
202
252
  )
203
253
  C.add_edge(i, j, weight=w)
204
- return C
254
+ return cast(CommunityGraph, C)
205
255
 
206
256
 
207
- def _community_k_neighbor_edges(C, k_val, p_rewire, rnd):
257
+ def _community_k_neighbor_edges(
258
+ C: CommunityGraph,
259
+ k_val: int,
260
+ p_rewire: float,
261
+ rnd: random.Random,
262
+ ) -> tuple[set[RemeshEdge], dict[int, int], list[tuple[int, int, int]]]:
208
263
  """Edges linking each community to its ``k`` nearest neighbours."""
209
- epi_vals = {n: get_attr(C.nodes[n], ALIAS_EPI, 0.0) for n in C.nodes()}
264
+ epi_vals = {n: _as_float(get_attr(C.nodes[n], ALIAS_EPI, 0.0)) for n in C.nodes()}
210
265
  ordered = sorted(C.nodes(), key=lambda v: epi_vals[v])
211
266
  new_edges = set()
212
267
  attempts = {n: 0 for n in C.nodes()}
@@ -240,7 +295,7 @@ def _community_k_neighbor_edges(C, k_val, p_rewire, rnd):
240
295
  if choices:
241
296
  v = rnd.choice(choices)
242
297
  rewired_now = True
243
- new_edges.add(tuple(sorted((u, v))))
298
+ new_edges.add(_ordered_edge(u, v))
244
299
  attempts[u] += 1
245
300
  if rewired_now:
246
301
  rewired.append((u, original_v, v))
@@ -249,17 +304,18 @@ def _community_k_neighbor_edges(C, k_val, p_rewire, rnd):
249
304
 
250
305
 
251
306
  def _community_remesh(
252
- G,
253
- epi,
254
- k_val,
255
- p_rewire,
256
- rnd,
257
- nx,
258
- nx_comm,
259
- mst_edges,
260
- n_before,
261
- ):
307
+ G: CommunityGraph,
308
+ epi: Mapping[Hashable, float],
309
+ k_val: int,
310
+ p_rewire: float,
311
+ rnd: random.Random,
312
+ nx: NetworkxModule,
313
+ nx_comm: CommunityModule,
314
+ mst_edges: Iterable[RemeshEdge],
315
+ n_before: int,
316
+ ) -> None:
262
317
  """Remesh ``G`` replacing nodes by modular communities."""
318
+ from ..glyph_history import append_metric
263
319
  comms = list(nx_comm.greedy_modularity_communities(G))
264
320
  if len(comms) <= 1:
265
321
  with edge_version_update(G):
@@ -268,7 +324,7 @@ def _community_remesh(
268
324
  return
269
325
  C = _community_graph(comms, epi, nx)
270
326
  mst_c = nx.minimum_spanning_tree(C, weight="weight")
271
- new_edges = set(mst_c.edges())
327
+ new_edges: set[RemeshEdge] = {_ordered_edge(u, v) for u, v in mst_c.edges()}
272
328
  extra_edges, attempts, rewired_edges = _community_k_neighbor_edges(
273
329
  C, k_val, p_rewire, rnd
274
330
  )
@@ -312,7 +368,7 @@ def _community_remesh(
312
368
 
313
369
 
314
370
  def apply_topological_remesh(
315
- G,
371
+ G: CommunityGraph,
316
372
  mode: str | None = None,
317
373
  *,
318
374
  k: int | None = None,
@@ -324,6 +380,7 @@ def apply_topological_remesh(
324
380
  When ``seed`` is ``None`` the RNG draws its base seed from
325
381
  ``G.graph['RANDOM_SEED']`` to keep runs reproducible.
326
382
  """
383
+ from ..glyph_history import append_metric
327
384
  nodes = list(G.nodes())
328
385
  n_before = len(nodes)
329
386
  if n_before <= 1:
@@ -336,18 +393,14 @@ def apply_topological_remesh(
336
393
 
337
394
  if mode is None:
338
395
  mode = str(
339
- G.graph.get(
340
- "REMESH_MODE", REMESH_DEFAULTS.get("REMESH_MODE", "knn")
341
- )
396
+ G.graph.get("REMESH_MODE", REMESH_DEFAULTS.get("REMESH_MODE", "knn"))
342
397
  )
343
398
  mode = str(mode)
344
399
  nx, nx_comm = _get_networkx_modules()
345
- epi = {n: get_attr(G.nodes[n], ALIAS_EPI, 0.0) for n in nodes}
400
+ epi = {n: _as_float(get_attr(G.nodes[n], ALIAS_EPI, 0.0)) for n in nodes}
346
401
  mst_edges = _mst_edges_from_epi(nx, nodes, epi)
347
402
  default_k = int(
348
- G.graph.get(
349
- "REMESH_COMMUNITY_K", REMESH_DEFAULTS.get("REMESH_COMMUNITY_K", 2)
350
- )
403
+ G.graph.get("REMESH_COMMUNITY_K", REMESH_DEFAULTS.get("REMESH_COMMUNITY_K", 2))
351
404
  )
352
405
  k_val = max(1, int(k) if k is not None else default_k)
353
406
 
@@ -374,7 +427,11 @@ def apply_topological_remesh(
374
427
  G.add_edges_from(new_edges)
375
428
 
376
429
 
377
- def _extra_gating_ok(hist, cfg, w_estab):
430
+ def _extra_gating_ok(
431
+ hist: MutableMapping[str, Sequence[float]],
432
+ cfg: Mapping[str, RemeshConfigValue],
433
+ w_estab: int,
434
+ ) -> bool:
378
435
  """Check additional stability gating conditions."""
379
436
  checks = [
380
437
  ("phase_sync", "REMESH_MIN_PHASE_SYNC", ge),
@@ -388,14 +445,27 @@ def _extra_gating_ok(hist, cfg, w_estab):
388
445
  if series is not None and len(series) >= w_estab:
389
446
  win = series[-w_estab:]
390
447
  avg = sum(win) / len(win)
391
- if not op(avg, cfg[cfg_key]):
448
+ threshold = _as_float(cfg[cfg_key])
449
+ if not op(avg, threshold):
392
450
  return False
393
451
  return True
394
452
 
395
453
 
396
454
  def apply_remesh_if_globally_stable(
397
- G, pasos_estables_consecutivos: int | None = None
455
+ G: CommunityGraph,
456
+ stable_step_window: int | None = None,
457
+ **kwargs: Any,
398
458
  ) -> None:
459
+ """Trigger remeshing when global stability indicators satisfy thresholds."""
460
+
461
+ from ..glyph_history import ensure_history
462
+ if kwargs:
463
+ unexpected = ", ".join(sorted(kwargs))
464
+ raise TypeError(
465
+ "apply_remesh_if_globally_stable() got unexpected keyword argument(s): "
466
+ f"{unexpected}"
467
+ )
468
+
399
469
  params = [
400
470
  (
401
471
  "REMESH_STABILITY_WINDOW",
@@ -432,20 +502,16 @@ def apply_remesh_if_globally_stable(
432
502
  float,
433
503
  REMESH_DEFAULTS["REMESH_MIN_SI_HI_FRAC"],
434
504
  ),
435
- (
436
- "REMESH_COOLDOWN_VENTANA",
437
- int,
438
- REMESH_DEFAULTS["REMESH_COOLDOWN_VENTANA"],
439
- ),
505
+ (COOLDOWN_KEY, int, REMESH_DEFAULTS[COOLDOWN_KEY]),
440
506
  ("REMESH_COOLDOWN_TS", float, REMESH_DEFAULTS["REMESH_COOLDOWN_TS"]),
441
507
  ]
442
508
  cfg = {}
443
509
  for key, conv, _default in params:
444
510
  cfg[key] = conv(get_param(G, key))
445
- frac_req = float(get_param(G, "FRACTION_STABLE_REMESH"))
511
+ frac_req = _as_float(get_param(G, "FRACTION_STABLE_REMESH"))
446
512
  w_estab = (
447
- pasos_estables_consecutivos
448
- if pasos_estables_consecutivos is not None
513
+ stable_step_window
514
+ if stable_step_window is not None
449
515
  else cfg["REMESH_STABILITY_WINDOW"]
450
516
  )
451
517
 
@@ -456,21 +522,16 @@ def apply_remesh_if_globally_stable(
456
522
  win_sf = sf[-w_estab:]
457
523
  if not all(v >= frac_req for v in win_sf):
458
524
  return
459
- if cfg["REMESH_REQUIRE_STABILITY"] and not _extra_gating_ok(
460
- hist, cfg, w_estab
461
- ):
525
+ if cfg["REMESH_REQUIRE_STABILITY"] and not _extra_gating_ok(hist, cfg, w_estab):
462
526
  return
463
527
 
464
528
  last = G.graph.get("_last_remesh_step", -(10**9))
465
529
  step_idx = len(sf)
466
- if step_idx - last < cfg["REMESH_COOLDOWN_VENTANA"]:
530
+ if step_idx - last < cfg[COOLDOWN_KEY]:
467
531
  return
468
- t_now = float(G.graph.get("_t", 0.0))
469
- last_ts = float(G.graph.get("_last_remesh_ts", -1e12))
470
- if (
471
- cfg["REMESH_COOLDOWN_TS"] > 0
472
- and (t_now - last_ts) < cfg["REMESH_COOLDOWN_TS"]
473
- ):
532
+ t_now = _as_float(G.graph.get("_t", 0.0))
533
+ last_ts = _as_float(G.graph.get("_last_remesh_ts", -1e12))
534
+ if cfg["REMESH_COOLDOWN_TS"] > 0 and (t_now - last_ts) < cfg["REMESH_COOLDOWN_TS"]:
474
535
  return
475
536
 
476
537
  apply_network_remesh(G)
tnfr/py.typed ADDED
File without changes