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,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