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
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Network operators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterator
|
|
6
|
+
from typing import Any, TYPE_CHECKING
|
|
7
|
+
import math
|
|
8
|
+
import heapq
|
|
9
|
+
from itertools import islice
|
|
10
|
+
from statistics import fmean, StatisticsError
|
|
11
|
+
|
|
12
|
+
from ..alias import get_attr
|
|
13
|
+
from ..constants import DEFAULTS, get_aliases, get_param
|
|
14
|
+
|
|
15
|
+
from ..helpers.numeric import angle_diff
|
|
16
|
+
from ..metrics.trig import neighbor_phase_mean
|
|
17
|
+
from ..utils import get_nodenx
|
|
18
|
+
from ..rng import make_rng
|
|
19
|
+
from tnfr import glyph_history
|
|
20
|
+
from ..types import EPIValue, Glyph, NodeId, TNFRGraph
|
|
21
|
+
|
|
22
|
+
from . import definitions as _definitions
|
|
23
|
+
from .jitter import (
|
|
24
|
+
JitterCache,
|
|
25
|
+
JitterCacheManager,
|
|
26
|
+
get_jitter_manager,
|
|
27
|
+
reset_jitter_manager,
|
|
28
|
+
random_jitter,
|
|
29
|
+
)
|
|
30
|
+
from .registry import OPERATORS, discover_operators, get_operator_class
|
|
31
|
+
from .remesh import (
|
|
32
|
+
apply_network_remesh,
|
|
33
|
+
apply_topological_remesh,
|
|
34
|
+
apply_remesh_if_globally_stable,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_remesh_doc = (
|
|
38
|
+
"Trigger a remesh once the stability window is satisfied.\n\n"
|
|
39
|
+
"Parameters\n----------\n"
|
|
40
|
+
"stable_step_window : int | None\n"
|
|
41
|
+
" Number of consecutive stable steps required before remeshing.\n"
|
|
42
|
+
" Only the English keyword 'stable_step_window' is supported."
|
|
43
|
+
)
|
|
44
|
+
if apply_remesh_if_globally_stable.__doc__:
|
|
45
|
+
apply_remesh_if_globally_stable.__doc__ += "\n\n" + _remesh_doc
|
|
46
|
+
else:
|
|
47
|
+
apply_remesh_if_globally_stable.__doc__ = _remesh_doc
|
|
48
|
+
|
|
49
|
+
discover_operators()
|
|
50
|
+
|
|
51
|
+
_DEFINITION_EXPORTS = {
|
|
52
|
+
name: getattr(_definitions, name)
|
|
53
|
+
for name in getattr(_definitions, "__all__", ())
|
|
54
|
+
}
|
|
55
|
+
globals().update(_DEFINITION_EXPORTS)
|
|
56
|
+
|
|
57
|
+
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
58
|
+
from ..node import NodeProtocol
|
|
59
|
+
|
|
60
|
+
GlyphFactors = dict[str, Any]
|
|
61
|
+
GlyphOperation = Callable[["NodeProtocol", GlyphFactors], None]
|
|
62
|
+
|
|
63
|
+
ALIAS_EPI = get_aliases("EPI")
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"JitterCache",
|
|
67
|
+
"JitterCacheManager",
|
|
68
|
+
"get_jitter_manager",
|
|
69
|
+
"reset_jitter_manager",
|
|
70
|
+
"random_jitter",
|
|
71
|
+
"get_neighbor_epi",
|
|
72
|
+
"get_glyph_factors",
|
|
73
|
+
"GLYPH_OPERATIONS",
|
|
74
|
+
"apply_glyph_obj",
|
|
75
|
+
"apply_glyph",
|
|
76
|
+
"apply_network_remesh",
|
|
77
|
+
"apply_topological_remesh",
|
|
78
|
+
"apply_remesh_if_globally_stable",
|
|
79
|
+
"OPERATORS",
|
|
80
|
+
"discover_operators",
|
|
81
|
+
"get_operator_class",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
__all__.extend(_DEFINITION_EXPORTS.keys())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_glyph_factors(node: NodeProtocol) -> GlyphFactors:
|
|
88
|
+
"""Return glyph factors for ``node`` with defaults."""
|
|
89
|
+
return node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"].copy())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_factor(gf: GlyphFactors, key: str, default: float) -> float:
|
|
93
|
+
"""Return ``gf[key]`` as ``float`` with ``default`` fallback."""
|
|
94
|
+
return float(gf.get(key, default))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# -------------------------
|
|
98
|
+
# Glyphs (operadores locales)
|
|
99
|
+
# -------------------------
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_neighbor_epi(node: NodeProtocol) -> tuple[list[NodeProtocol], EPIValue]:
|
|
103
|
+
"""Return neighbour list and their mean ``EPI`` without mutating ``node``."""
|
|
104
|
+
|
|
105
|
+
epi = node.EPI
|
|
106
|
+
neigh = list(node.neighbors())
|
|
107
|
+
if not neigh:
|
|
108
|
+
return [], epi
|
|
109
|
+
|
|
110
|
+
if hasattr(node, "G"):
|
|
111
|
+
G = node.G
|
|
112
|
+
total = 0.0
|
|
113
|
+
count = 0
|
|
114
|
+
has_valid_neighbor = False
|
|
115
|
+
needs_conversion = False
|
|
116
|
+
for v in neigh:
|
|
117
|
+
if hasattr(v, "EPI"):
|
|
118
|
+
total += float(v.EPI)
|
|
119
|
+
has_valid_neighbor = True
|
|
120
|
+
else:
|
|
121
|
+
attr = get_attr(G.nodes[v], ALIAS_EPI, None)
|
|
122
|
+
if attr is not None:
|
|
123
|
+
total += float(attr)
|
|
124
|
+
has_valid_neighbor = True
|
|
125
|
+
else:
|
|
126
|
+
total += float(epi)
|
|
127
|
+
needs_conversion = True
|
|
128
|
+
count += 1
|
|
129
|
+
if not has_valid_neighbor:
|
|
130
|
+
return [], epi
|
|
131
|
+
epi_bar = total / count if count else float(epi)
|
|
132
|
+
if needs_conversion:
|
|
133
|
+
NodeNX = get_nodenx()
|
|
134
|
+
if NodeNX is None:
|
|
135
|
+
raise ImportError("NodeNX is unavailable")
|
|
136
|
+
neigh = [
|
|
137
|
+
v if hasattr(v, "EPI") else NodeNX.from_graph(node.G, v)
|
|
138
|
+
for v in neigh
|
|
139
|
+
]
|
|
140
|
+
else:
|
|
141
|
+
try:
|
|
142
|
+
epi_bar = fmean(v.EPI for v in neigh)
|
|
143
|
+
except StatisticsError:
|
|
144
|
+
epi_bar = epi
|
|
145
|
+
|
|
146
|
+
return neigh, epi_bar
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _determine_dominant(
|
|
150
|
+
neigh: list[NodeProtocol], default_kind: str
|
|
151
|
+
) -> tuple[str, float]:
|
|
152
|
+
"""Return dominant ``epi_kind`` among ``neigh`` and its absolute ``EPI``."""
|
|
153
|
+
best_kind: str | None = None
|
|
154
|
+
best_abs = 0.0
|
|
155
|
+
for v in neigh:
|
|
156
|
+
abs_v = abs(v.EPI)
|
|
157
|
+
if abs_v > best_abs:
|
|
158
|
+
best_abs = abs_v
|
|
159
|
+
best_kind = v.epi_kind
|
|
160
|
+
if not best_kind:
|
|
161
|
+
return default_kind, 0.0
|
|
162
|
+
return best_kind, best_abs
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _mix_epi_with_neighbors(
|
|
166
|
+
node: NodeProtocol, mix: float, default_glyph: Glyph | str
|
|
167
|
+
) -> tuple[float, str]:
|
|
168
|
+
"""Mix ``EPI`` of ``node`` with the mean of its neighbours."""
|
|
169
|
+
default_kind = (
|
|
170
|
+
default_glyph.value
|
|
171
|
+
if isinstance(default_glyph, Glyph)
|
|
172
|
+
else str(default_glyph)
|
|
173
|
+
)
|
|
174
|
+
epi = node.EPI
|
|
175
|
+
neigh, epi_bar = get_neighbor_epi(node)
|
|
176
|
+
|
|
177
|
+
if not neigh:
|
|
178
|
+
node.epi_kind = default_kind
|
|
179
|
+
return epi, default_kind
|
|
180
|
+
|
|
181
|
+
dominant, best_abs = _determine_dominant(neigh, default_kind)
|
|
182
|
+
new_epi = (1 - mix) * epi + mix * epi_bar
|
|
183
|
+
node.EPI = new_epi
|
|
184
|
+
final = dominant if best_abs > abs(new_epi) else node.epi_kind
|
|
185
|
+
if not final:
|
|
186
|
+
final = default_kind
|
|
187
|
+
node.epi_kind = final
|
|
188
|
+
return epi_bar, final
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _op_AL(node: NodeProtocol, gf: GlyphFactors) -> None: # AL — Emission
|
|
192
|
+
f = get_factor(gf, "AL_boost", 0.05)
|
|
193
|
+
node.EPI = node.EPI + f
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _op_EN(node: NodeProtocol, gf: GlyphFactors) -> None: # EN — Reception
|
|
197
|
+
mix = get_factor(gf, "EN_mix", 0.25)
|
|
198
|
+
_mix_epi_with_neighbors(node, mix, Glyph.EN)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _op_IL(node: NodeProtocol, gf: GlyphFactors) -> None: # IL — Coherence
|
|
202
|
+
factor = get_factor(gf, "IL_dnfr_factor", 0.7)
|
|
203
|
+
node.dnfr = factor * getattr(node, "dnfr", 0.0)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _op_OZ(node: NodeProtocol, gf: GlyphFactors) -> None: # OZ — Dissonance
|
|
207
|
+
factor = get_factor(gf, "OZ_dnfr_factor", 1.3)
|
|
208
|
+
dnfr = getattr(node, "dnfr", 0.0)
|
|
209
|
+
if bool(node.graph.get("OZ_NOISE_MODE", False)):
|
|
210
|
+
sigma = float(node.graph.get("OZ_SIGMA", 0.1))
|
|
211
|
+
if sigma <= 0:
|
|
212
|
+
node.dnfr = dnfr
|
|
213
|
+
return
|
|
214
|
+
node.dnfr = dnfr + random_jitter(node, sigma)
|
|
215
|
+
else:
|
|
216
|
+
node.dnfr = factor * dnfr if abs(dnfr) > 1e-9 else 0.1
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _um_candidate_iter(node: NodeProtocol) -> Iterator[NodeProtocol]:
|
|
220
|
+
sample_ids = node.graph.get("_node_sample")
|
|
221
|
+
if sample_ids is not None and hasattr(node, "G"):
|
|
222
|
+
NodeNX = get_nodenx()
|
|
223
|
+
if NodeNX is None:
|
|
224
|
+
raise ImportError("NodeNX is unavailable")
|
|
225
|
+
base = (NodeNX.from_graph(node.G, j) for j in sample_ids)
|
|
226
|
+
else:
|
|
227
|
+
base = node.all_nodes()
|
|
228
|
+
for j in base:
|
|
229
|
+
same = (j is node) or (
|
|
230
|
+
getattr(node, "n", None) == getattr(j, "n", None)
|
|
231
|
+
)
|
|
232
|
+
if same or node.has_edge(j):
|
|
233
|
+
continue
|
|
234
|
+
yield j
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _um_select_candidates(
|
|
238
|
+
node: NodeProtocol,
|
|
239
|
+
candidates: Iterator[NodeProtocol],
|
|
240
|
+
limit: int,
|
|
241
|
+
mode: str,
|
|
242
|
+
th: float,
|
|
243
|
+
) -> list[NodeProtocol]:
|
|
244
|
+
"""Select a subset of ``candidates`` for UM coupling."""
|
|
245
|
+
rng = make_rng(int(node.graph.get("RANDOM_SEED", 0)), node.offset(), node.G)
|
|
246
|
+
|
|
247
|
+
if limit <= 0:
|
|
248
|
+
return list(candidates)
|
|
249
|
+
|
|
250
|
+
if mode == "proximity":
|
|
251
|
+
return heapq.nsmallest(
|
|
252
|
+
limit, candidates, key=lambda j: abs(angle_diff(j.theta, th))
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
reservoir = list(islice(candidates, limit))
|
|
256
|
+
for i, cand in enumerate(candidates, start=limit):
|
|
257
|
+
j = rng.randint(0, i)
|
|
258
|
+
if j < limit:
|
|
259
|
+
reservoir[j] = cand
|
|
260
|
+
|
|
261
|
+
if mode == "sample":
|
|
262
|
+
rng.shuffle(reservoir)
|
|
263
|
+
|
|
264
|
+
return reservoir
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _op_UM(node: NodeProtocol, gf: GlyphFactors) -> None: # UM — Coupling
|
|
268
|
+
k = get_factor(gf, "UM_theta_push", 0.25)
|
|
269
|
+
th = node.theta
|
|
270
|
+
thL = neighbor_phase_mean(node)
|
|
271
|
+
d = angle_diff(thL, th)
|
|
272
|
+
node.theta = th + k * d
|
|
273
|
+
|
|
274
|
+
if bool(node.graph.get("UM_FUNCTIONAL_LINKS", False)):
|
|
275
|
+
thr = float(
|
|
276
|
+
node.graph.get(
|
|
277
|
+
"UM_COMPAT_THRESHOLD",
|
|
278
|
+
DEFAULTS.get("UM_COMPAT_THRESHOLD", 0.75),
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
epi_i = node.EPI
|
|
282
|
+
si_i = node.Si
|
|
283
|
+
|
|
284
|
+
limit = int(node.graph.get("UM_CANDIDATE_COUNT", 0))
|
|
285
|
+
mode = str(node.graph.get("UM_CANDIDATE_MODE", "sample")).lower()
|
|
286
|
+
candidates = _um_select_candidates(
|
|
287
|
+
node, _um_candidate_iter(node), limit, mode, th
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
for j in candidates:
|
|
291
|
+
th_j = j.theta
|
|
292
|
+
dphi = abs(angle_diff(th_j, th)) / math.pi
|
|
293
|
+
epi_j = j.EPI
|
|
294
|
+
si_j = j.Si
|
|
295
|
+
epi_sim = 1.0 - abs(epi_i - epi_j) / (
|
|
296
|
+
abs(epi_i) + abs(epi_j) + 1e-9
|
|
297
|
+
)
|
|
298
|
+
si_sim = 1.0 - abs(si_i - si_j)
|
|
299
|
+
compat = (1 - dphi) * 0.5 + 0.25 * epi_sim + 0.25 * si_sim
|
|
300
|
+
if compat >= thr:
|
|
301
|
+
node.add_edge(j, compat)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _op_RA(node: NodeProtocol, gf: GlyphFactors) -> None: # RA — Resonance
|
|
305
|
+
diff = get_factor(gf, "RA_epi_diff", 0.15)
|
|
306
|
+
_mix_epi_with_neighbors(node, diff, Glyph.RA)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _op_SHA(node: NodeProtocol, gf: GlyphFactors) -> None: # SHA — Silence
|
|
310
|
+
factor = get_factor(gf, "SHA_vf_factor", 0.85)
|
|
311
|
+
node.vf = factor * node.vf
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
factor_val = 1.15
|
|
315
|
+
factor_nul = 0.85
|
|
316
|
+
_SCALE_FACTORS = {Glyph.VAL: factor_val, Glyph.NUL: factor_nul}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _op_scale(node: NodeProtocol, factor: float) -> None:
|
|
320
|
+
node.vf *= factor
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _make_scale_op(glyph: Glyph) -> GlyphOperation:
|
|
324
|
+
def _op(node: NodeProtocol, gf: GlyphFactors) -> None:
|
|
325
|
+
key = "VAL_scale" if glyph is Glyph.VAL else "NUL_scale"
|
|
326
|
+
default = _SCALE_FACTORS[glyph]
|
|
327
|
+
factor = get_factor(gf, key, default)
|
|
328
|
+
_op_scale(node, factor)
|
|
329
|
+
|
|
330
|
+
return _op
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _op_THOL(
|
|
334
|
+
node: NodeProtocol, gf: GlyphFactors
|
|
335
|
+
) -> None: # THOL — Self-organization
|
|
336
|
+
a = get_factor(gf, "THOL_accel", 0.10)
|
|
337
|
+
node.dnfr = node.dnfr + a * getattr(node, "d2EPI", 0.0)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _op_ZHIR(
|
|
341
|
+
node: NodeProtocol, gf: GlyphFactors
|
|
342
|
+
) -> None: # ZHIR — Mutation
|
|
343
|
+
shift = get_factor(gf, "ZHIR_theta_shift", math.pi / 2)
|
|
344
|
+
node.theta = node.theta + shift
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _op_NAV(
|
|
348
|
+
node: NodeProtocol, gf: GlyphFactors
|
|
349
|
+
) -> None: # NAV — Transition
|
|
350
|
+
dnfr = node.dnfr
|
|
351
|
+
vf = node.vf
|
|
352
|
+
eta = get_factor(gf, "NAV_eta", 0.5)
|
|
353
|
+
strict = bool(node.graph.get("NAV_STRICT", False))
|
|
354
|
+
if strict:
|
|
355
|
+
base = vf
|
|
356
|
+
else:
|
|
357
|
+
sign = 1.0 if dnfr >= 0 else -1.0
|
|
358
|
+
target = sign * vf
|
|
359
|
+
base = (1.0 - eta) * dnfr + eta * target
|
|
360
|
+
j = get_factor(gf, "NAV_jitter", 0.05)
|
|
361
|
+
if bool(node.graph.get("NAV_RANDOM", True)):
|
|
362
|
+
jitter = random_jitter(node, j)
|
|
363
|
+
else:
|
|
364
|
+
jitter = j * (1 if base >= 0 else -1)
|
|
365
|
+
node.dnfr = base + jitter
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _op_REMESH(
|
|
369
|
+
node: NodeProtocol, gf: GlyphFactors | None = None
|
|
370
|
+
) -> None: # REMESH — advisory
|
|
371
|
+
step_idx = glyph_history.current_step_idx(node)
|
|
372
|
+
last_warn = node.graph.get("_remesh_warn_step", None)
|
|
373
|
+
if last_warn != step_idx:
|
|
374
|
+
msg = (
|
|
375
|
+
"REMESH operates at network scale. Use apply_remesh_if_globally_"
|
|
376
|
+
"stable(G) or apply_network_remesh(G)."
|
|
377
|
+
)
|
|
378
|
+
hist = glyph_history.ensure_history(node)
|
|
379
|
+
glyph_history.append_metric(
|
|
380
|
+
hist,
|
|
381
|
+
"events",
|
|
382
|
+
("warn", {"step": step_idx, "node": None, "msg": msg}),
|
|
383
|
+
)
|
|
384
|
+
node.graph["_remesh_warn_step"] = step_idx
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# -------------------------
|
|
389
|
+
# Dispatcher
|
|
390
|
+
# -------------------------
|
|
391
|
+
|
|
392
|
+
GLYPH_OPERATIONS: dict[Glyph, GlyphOperation] = {
|
|
393
|
+
Glyph.AL: _op_AL,
|
|
394
|
+
Glyph.EN: _op_EN,
|
|
395
|
+
Glyph.IL: _op_IL,
|
|
396
|
+
Glyph.OZ: _op_OZ,
|
|
397
|
+
Glyph.UM: _op_UM,
|
|
398
|
+
Glyph.RA: _op_RA,
|
|
399
|
+
Glyph.SHA: _op_SHA,
|
|
400
|
+
Glyph.VAL: _make_scale_op(Glyph.VAL),
|
|
401
|
+
Glyph.NUL: _make_scale_op(Glyph.NUL),
|
|
402
|
+
Glyph.THOL: _op_THOL,
|
|
403
|
+
Glyph.ZHIR: _op_ZHIR,
|
|
404
|
+
Glyph.NAV: _op_NAV,
|
|
405
|
+
Glyph.REMESH: _op_REMESH,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def apply_glyph_obj(
|
|
410
|
+
node: NodeProtocol, glyph: Glyph | str, *, window: int | None = None
|
|
411
|
+
) -> None:
|
|
412
|
+
"""Apply ``glyph`` to an object satisfying :class:`NodeProtocol`."""
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
g = glyph if isinstance(glyph, Glyph) else Glyph(str(glyph))
|
|
416
|
+
except ValueError:
|
|
417
|
+
step_idx = glyph_history.current_step_idx(node)
|
|
418
|
+
hist = glyph_history.ensure_history(node)
|
|
419
|
+
glyph_history.append_metric(
|
|
420
|
+
hist,
|
|
421
|
+
"events",
|
|
422
|
+
(
|
|
423
|
+
"warn",
|
|
424
|
+
{
|
|
425
|
+
"step": step_idx,
|
|
426
|
+
"node": getattr(node, "n", None),
|
|
427
|
+
"msg": f"unknown glyph: {glyph}",
|
|
428
|
+
},
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
raise ValueError(f"unknown glyph: {glyph}")
|
|
432
|
+
|
|
433
|
+
op = GLYPH_OPERATIONS.get(g)
|
|
434
|
+
if op is None:
|
|
435
|
+
raise ValueError(f"glyph has no registered operator: {g}")
|
|
436
|
+
if window is None:
|
|
437
|
+
window = int(get_param(node, "GLYPH_HYSTERESIS_WINDOW"))
|
|
438
|
+
gf = get_glyph_factors(node)
|
|
439
|
+
op(node, gf)
|
|
440
|
+
glyph_history.push_glyph(node._glyph_storage(), g.value, window)
|
|
441
|
+
node.epi_kind = g.value
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def apply_glyph(
|
|
445
|
+
G: TNFRGraph, n: NodeId, glyph: Glyph | str, *, window: int | None = None
|
|
446
|
+
) -> None:
|
|
447
|
+
"""Adapter to operate on ``networkx`` graphs."""
|
|
448
|
+
NodeNX = get_nodenx()
|
|
449
|
+
if NodeNX is None:
|
|
450
|
+
raise ImportError("NodeNX is unavailable")
|
|
451
|
+
node = NodeNX(G, n)
|
|
452
|
+
apply_glyph_obj(node, glyph, window=window)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
Operator: Any
|
|
4
|
+
Emission: Any
|
|
5
|
+
Reception: Any
|
|
6
|
+
Coherence: Any
|
|
7
|
+
Dissonance: Any
|
|
8
|
+
Coupling: Any
|
|
9
|
+
Resonance: Any
|
|
10
|
+
Silence: Any
|
|
11
|
+
Expansion: Any
|
|
12
|
+
Contraction: Any
|
|
13
|
+
SelfOrganization: Any
|
|
14
|
+
Mutation: Any
|
|
15
|
+
Transition: Any
|
|
16
|
+
Recursivity: Any
|
|
17
|
+
GLYPH_OPERATIONS: Any
|
|
18
|
+
JitterCache: Any
|
|
19
|
+
JitterCacheManager: Any
|
|
20
|
+
OPERATORS: Any
|
|
21
|
+
apply_glyph: Any
|
|
22
|
+
apply_glyph_obj: Any
|
|
23
|
+
apply_network_remesh: Any
|
|
24
|
+
apply_remesh_if_globally_stable: Any
|
|
25
|
+
apply_topological_remesh: Any
|
|
26
|
+
discover_operators: Any
|
|
27
|
+
get_glyph_factors: Any
|
|
28
|
+
get_jitter_manager: Any
|
|
29
|
+
get_neighbor_epi: Any
|
|
30
|
+
random_jitter: Any
|
|
31
|
+
reset_jitter_manager: Any
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Definitions for canonical TNFR structural operators.
|
|
2
|
+
|
|
3
|
+
English identifiers are the public API. Spanish wrappers were removed in
|
|
4
|
+
TNFR 2.0, so downstream code must import these classes directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, ClassVar
|
|
10
|
+
|
|
11
|
+
from ..config.operator_names import (
|
|
12
|
+
COHERENCE,
|
|
13
|
+
COUPLING,
|
|
14
|
+
DISSONANCE,
|
|
15
|
+
EMISSION,
|
|
16
|
+
MUTATION,
|
|
17
|
+
RECEPTION,
|
|
18
|
+
RECURSIVITY,
|
|
19
|
+
RESONANCE,
|
|
20
|
+
SELF_ORGANIZATION,
|
|
21
|
+
SILENCE,
|
|
22
|
+
TRANSITION,
|
|
23
|
+
CONTRACTION,
|
|
24
|
+
EXPANSION,
|
|
25
|
+
)
|
|
26
|
+
from ..types import Glyph, TNFRGraph
|
|
27
|
+
from .registry import register_operator
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Operator",
|
|
31
|
+
"Emission",
|
|
32
|
+
"Reception",
|
|
33
|
+
"Coherence",
|
|
34
|
+
"Dissonance",
|
|
35
|
+
"Coupling",
|
|
36
|
+
"Resonance",
|
|
37
|
+
"Silence",
|
|
38
|
+
"Expansion",
|
|
39
|
+
"Contraction",
|
|
40
|
+
"SelfOrganization",
|
|
41
|
+
"Mutation",
|
|
42
|
+
"Transition",
|
|
43
|
+
"Recursivity",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Operator:
|
|
48
|
+
"""Base class for TNFR operators.
|
|
49
|
+
|
|
50
|
+
Each operator defines ``name`` (ASCII identifier) and ``glyph``. Calling an
|
|
51
|
+
instance applies the corresponding glyph to the node.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name: ClassVar[str] = "operator"
|
|
55
|
+
glyph: ClassVar[Glyph | None] = None
|
|
56
|
+
|
|
57
|
+
def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None:
|
|
58
|
+
if self.glyph is None:
|
|
59
|
+
raise NotImplementedError("Operator without assigned glyph")
|
|
60
|
+
from ..validation.grammar import ( # local import to avoid cycles
|
|
61
|
+
apply_glyph_with_grammar,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
apply_glyph_with_grammar(G, [node], self.glyph, kw.get("window"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@register_operator
|
|
68
|
+
class Emission(Operator):
|
|
69
|
+
"""Emission operator (glyph ``AL``)."""
|
|
70
|
+
|
|
71
|
+
__slots__ = ()
|
|
72
|
+
name: ClassVar[str] = EMISSION
|
|
73
|
+
glyph: ClassVar[Glyph] = Glyph.AL
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@register_operator
|
|
77
|
+
class Reception(Operator):
|
|
78
|
+
"""Reception operator (glyph ``EN``)."""
|
|
79
|
+
|
|
80
|
+
__slots__ = ()
|
|
81
|
+
name: ClassVar[str] = RECEPTION
|
|
82
|
+
glyph: ClassVar[Glyph] = Glyph.EN
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@register_operator
|
|
86
|
+
class Coherence(Operator):
|
|
87
|
+
"""Coherence operator (glyph ``IL``)."""
|
|
88
|
+
|
|
89
|
+
__slots__ = ()
|
|
90
|
+
name: ClassVar[str] = COHERENCE
|
|
91
|
+
glyph: ClassVar[Glyph] = Glyph.IL
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@register_operator
|
|
95
|
+
class Dissonance(Operator):
|
|
96
|
+
"""Dissonance operator (glyph ``OZ``)."""
|
|
97
|
+
|
|
98
|
+
__slots__ = ()
|
|
99
|
+
name: ClassVar[str] = DISSONANCE
|
|
100
|
+
glyph: ClassVar[Glyph] = Glyph.OZ
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@register_operator
|
|
104
|
+
class Coupling(Operator):
|
|
105
|
+
"""Coupling operator (glyph ``UM``)."""
|
|
106
|
+
|
|
107
|
+
__slots__ = ()
|
|
108
|
+
name: ClassVar[str] = COUPLING
|
|
109
|
+
glyph: ClassVar[Glyph] = Glyph.UM
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@register_operator
|
|
113
|
+
class Resonance(Operator):
|
|
114
|
+
"""Resonance operator (glyph ``RA``)."""
|
|
115
|
+
|
|
116
|
+
__slots__ = ()
|
|
117
|
+
name: ClassVar[str] = RESONANCE
|
|
118
|
+
glyph: ClassVar[Glyph] = Glyph.RA
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@register_operator
|
|
122
|
+
class Silence(Operator):
|
|
123
|
+
"""Silence operator (glyph ``SHA``)."""
|
|
124
|
+
|
|
125
|
+
__slots__ = ()
|
|
126
|
+
name: ClassVar[str] = SILENCE
|
|
127
|
+
glyph: ClassVar[Glyph] = Glyph.SHA
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@register_operator
|
|
131
|
+
class Expansion(Operator):
|
|
132
|
+
"""Expansion operator (glyph ``VAL``)."""
|
|
133
|
+
|
|
134
|
+
__slots__ = ()
|
|
135
|
+
name: ClassVar[str] = EXPANSION
|
|
136
|
+
glyph: ClassVar[Glyph] = Glyph.VAL
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@register_operator
|
|
140
|
+
class Contraction(Operator):
|
|
141
|
+
"""Contraction operator (glyph ``NUL``)."""
|
|
142
|
+
|
|
143
|
+
__slots__ = ()
|
|
144
|
+
name: ClassVar[str] = CONTRACTION
|
|
145
|
+
glyph: ClassVar[Glyph] = Glyph.NUL
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@register_operator
|
|
149
|
+
class SelfOrganization(Operator):
|
|
150
|
+
"""Self-organization operator (glyph ``THOL``)."""
|
|
151
|
+
|
|
152
|
+
__slots__ = ()
|
|
153
|
+
name: ClassVar[str] = SELF_ORGANIZATION
|
|
154
|
+
glyph: ClassVar[Glyph] = Glyph.THOL
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@register_operator
|
|
158
|
+
class Mutation(Operator):
|
|
159
|
+
"""Mutation operator (glyph ``ZHIR``)."""
|
|
160
|
+
|
|
161
|
+
__slots__ = ()
|
|
162
|
+
name: ClassVar[str] = MUTATION
|
|
163
|
+
glyph: ClassVar[Glyph] = Glyph.ZHIR
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@register_operator
|
|
167
|
+
class Transition(Operator):
|
|
168
|
+
"""Transition operator (glyph ``NAV``)."""
|
|
169
|
+
|
|
170
|
+
__slots__ = ()
|
|
171
|
+
name: ClassVar[str] = TRANSITION
|
|
172
|
+
glyph: ClassVar[Glyph] = Glyph.NAV
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@register_operator
|
|
176
|
+
class Recursivity(Operator):
|
|
177
|
+
"""Recursivity operator (glyph ``REMESH``)."""
|
|
178
|
+
|
|
179
|
+
__slots__ = ()
|
|
180
|
+
name: ClassVar[str] = RECURSIVITY
|
|
181
|
+
glyph: ClassVar[Glyph] = Glyph.REMESH
|