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.
- tnfr/__init__.py +270 -90
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +631 -0
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +732 -0
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +381 -0
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +89 -0
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +199 -0
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +322 -0
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +34 -0
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +12 -0
- tnfr/config/__init__.pyi +8 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/init.py +36 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +106 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +104 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +228 -0
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +158 -0
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +102 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +16 -0
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +136 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +201 -0
- tnfr/dynamics/aliases.py +22 -0
- tnfr/dynamics/coordination.py +343 -0
- tnfr/dynamics/dnfr.py +2315 -0
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +561 -0
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +34 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +216 -0
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +283 -0
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +320 -89
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +337 -0
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +23 -153
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +151 -0
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +88 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +214 -0
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +199 -0
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +311 -0
- tnfr/io.pyi +11 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +1469 -0
- tnfr/metrics/common.py +149 -0
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +259 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +840 -0
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +151 -0
- tnfr/metrics/glyph_timing.py +369 -0
- tnfr/metrics/reporting.py +152 -0
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +294 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +216 -0
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +105 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +255 -177
- tnfr/node.pyi +161 -0
- tnfr/observers.py +154 -150
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +135 -134
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +452 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +266 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +569 -0
- tnfr/presets.py +10 -23
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +440 -0
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +217 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +307 -142
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +69 -164
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +61 -0
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +520 -95
- tnfr/trace.pyi +68 -0
- tnfr/types.py +382 -17
- tnfr/types.pyi +145 -0
- tnfr/utils/__init__.py +158 -0
- tnfr/utils/__init__.pyi +133 -0
- tnfr/utils/cache.py +755 -0
- tnfr/utils/cache.pyi +156 -0
- tnfr/utils/data.py +267 -0
- tnfr/utils/data.pyi +73 -0
- tnfr/utils/graph.py +87 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +746 -0
- tnfr/utils/init.pyi +85 -0
- tnfr/utils/io.py +157 -0
- tnfr/utils/io.pyi +10 -0
- tnfr/utils/validators.py +130 -0
- tnfr/utils/validators.pyi +19 -0
- tnfr/validation/__init__.py +25 -0
- tnfr/validation/__init__.pyi +17 -0
- tnfr/validation/compatibility.py +59 -0
- tnfr/validation/compatibility.pyi +8 -0
- tnfr/validation/grammar.py +149 -0
- tnfr/validation/grammar.pyi +11 -0
- tnfr/validation/rules.py +194 -0
- tnfr/validation/rules.pyi +18 -0
- tnfr/validation/syntax.py +151 -0
- tnfr/validation/syntax.pyi +7 -0
- tnfr-6.0.0.dist-info/METADATA +135 -0
- tnfr-6.0.0.dist-info/RECORD +157 -0
- tnfr/cli.py +0 -322
- tnfr/config.py +0 -41
- tnfr/constants.py +0 -277
- tnfr/dynamics.py +0 -814
- tnfr/helpers.py +0 -264
- tnfr/main.py +0 -47
- tnfr/metrics.py +0 -597
- tnfr/operators.py +0 -525
- tnfr/program.py +0 -176
- tnfr/scenarios.py +0 -34
- tnfr/validators.py +0 -38
- tnfr-4.5.1.dist-info/METADATA +0 -221
- tnfr-4.5.1.dist-info/RECORD +0 -28
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/operators/remesh.py
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
tnfr/py.typed
ADDED
|
File without changes
|