tnfr 6.0.0__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 (176) hide show
  1. tnfr/__init__.py +50 -5
  2. tnfr/__init__.pyi +0 -7
  3. tnfr/_compat.py +0 -1
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +44 -2
  6. tnfr/alias.py +14 -13
  7. tnfr/alias.pyi +5 -37
  8. tnfr/cache.py +9 -729
  9. tnfr/cache.pyi +8 -224
  10. tnfr/callback_utils.py +16 -31
  11. tnfr/callback_utils.pyi +3 -29
  12. tnfr/cli/__init__.py +17 -11
  13. tnfr/cli/__init__.pyi +0 -21
  14. tnfr/cli/arguments.py +175 -14
  15. tnfr/cli/arguments.pyi +5 -11
  16. tnfr/cli/execution.py +434 -48
  17. tnfr/cli/execution.pyi +14 -24
  18. tnfr/cli/utils.py +20 -3
  19. tnfr/cli/utils.pyi +5 -5
  20. tnfr/config/__init__.py +2 -1
  21. tnfr/config/__init__.pyi +2 -0
  22. tnfr/config/feature_flags.py +83 -0
  23. tnfr/config/init.py +1 -1
  24. tnfr/config/operator_names.py +1 -14
  25. tnfr/config/presets.py +6 -26
  26. tnfr/constants/__init__.py +10 -13
  27. tnfr/constants/__init__.pyi +10 -22
  28. tnfr/constants/aliases.py +31 -0
  29. tnfr/constants/core.py +4 -3
  30. tnfr/constants/init.py +1 -1
  31. tnfr/constants/metric.py +3 -3
  32. tnfr/dynamics/__init__.py +64 -10
  33. tnfr/dynamics/__init__.pyi +3 -4
  34. tnfr/dynamics/adaptation.py +79 -13
  35. tnfr/dynamics/aliases.py +10 -9
  36. tnfr/dynamics/coordination.py +77 -35
  37. tnfr/dynamics/dnfr.py +575 -274
  38. tnfr/dynamics/dnfr.pyi +1 -10
  39. tnfr/dynamics/integrators.py +47 -33
  40. tnfr/dynamics/integrators.pyi +0 -1
  41. tnfr/dynamics/runtime.py +489 -129
  42. tnfr/dynamics/sampling.py +2 -0
  43. tnfr/dynamics/selectors.py +101 -62
  44. tnfr/execution.py +15 -8
  45. tnfr/execution.pyi +5 -25
  46. tnfr/flatten.py +7 -3
  47. tnfr/flatten.pyi +1 -8
  48. tnfr/gamma.py +22 -26
  49. tnfr/gamma.pyi +0 -6
  50. tnfr/glyph_history.py +37 -26
  51. tnfr/glyph_history.pyi +1 -19
  52. tnfr/glyph_runtime.py +16 -0
  53. tnfr/glyph_runtime.pyi +9 -0
  54. tnfr/immutable.py +20 -15
  55. tnfr/immutable.pyi +4 -7
  56. tnfr/initialization.py +5 -7
  57. tnfr/initialization.pyi +1 -9
  58. tnfr/io.py +6 -305
  59. tnfr/io.pyi +13 -8
  60. tnfr/mathematics/__init__.py +81 -0
  61. tnfr/mathematics/backend.py +426 -0
  62. tnfr/mathematics/dynamics.py +398 -0
  63. tnfr/mathematics/epi.py +254 -0
  64. tnfr/mathematics/generators.py +222 -0
  65. tnfr/mathematics/metrics.py +119 -0
  66. tnfr/mathematics/operators.py +233 -0
  67. tnfr/mathematics/operators_factory.py +71 -0
  68. tnfr/mathematics/projection.py +78 -0
  69. tnfr/mathematics/runtime.py +173 -0
  70. tnfr/mathematics/spaces.py +247 -0
  71. tnfr/mathematics/transforms.py +292 -0
  72. tnfr/metrics/__init__.py +10 -10
  73. tnfr/metrics/coherence.py +123 -94
  74. tnfr/metrics/common.py +22 -13
  75. tnfr/metrics/common.pyi +42 -11
  76. tnfr/metrics/core.py +72 -14
  77. tnfr/metrics/diagnosis.py +48 -57
  78. tnfr/metrics/diagnosis.pyi +3 -7
  79. tnfr/metrics/export.py +3 -5
  80. tnfr/metrics/glyph_timing.py +41 -31
  81. tnfr/metrics/reporting.py +13 -6
  82. tnfr/metrics/sense_index.py +884 -114
  83. tnfr/metrics/trig.py +167 -11
  84. tnfr/metrics/trig.pyi +1 -0
  85. tnfr/metrics/trig_cache.py +112 -15
  86. tnfr/node.py +400 -17
  87. tnfr/node.pyi +55 -38
  88. tnfr/observers.py +111 -8
  89. tnfr/observers.pyi +0 -15
  90. tnfr/ontosim.py +9 -6
  91. tnfr/ontosim.pyi +0 -5
  92. tnfr/operators/__init__.py +529 -42
  93. tnfr/operators/__init__.pyi +14 -0
  94. tnfr/operators/definitions.py +350 -18
  95. tnfr/operators/definitions.pyi +0 -14
  96. tnfr/operators/grammar.py +760 -0
  97. tnfr/operators/jitter.py +28 -22
  98. tnfr/operators/registry.py +7 -12
  99. tnfr/operators/registry.pyi +0 -2
  100. tnfr/operators/remesh.py +38 -61
  101. tnfr/rng.py +17 -300
  102. tnfr/schemas/__init__.py +8 -0
  103. tnfr/schemas/grammar.json +94 -0
  104. tnfr/selector.py +3 -4
  105. tnfr/selector.pyi +1 -1
  106. tnfr/sense.py +22 -24
  107. tnfr/sense.pyi +0 -7
  108. tnfr/structural.py +504 -21
  109. tnfr/structural.pyi +41 -18
  110. tnfr/telemetry/__init__.py +23 -1
  111. tnfr/telemetry/cache_metrics.py +226 -0
  112. tnfr/telemetry/nu_f.py +423 -0
  113. tnfr/telemetry/nu_f.pyi +123 -0
  114. tnfr/tokens.py +1 -4
  115. tnfr/tokens.pyi +1 -6
  116. tnfr/trace.py +20 -53
  117. tnfr/trace.pyi +9 -37
  118. tnfr/types.py +244 -15
  119. tnfr/types.pyi +200 -14
  120. tnfr/units.py +69 -0
  121. tnfr/units.pyi +16 -0
  122. tnfr/utils/__init__.py +107 -48
  123. tnfr/utils/__init__.pyi +80 -11
  124. tnfr/utils/cache.py +1705 -65
  125. tnfr/utils/cache.pyi +370 -58
  126. tnfr/utils/chunks.py +104 -0
  127. tnfr/utils/chunks.pyi +21 -0
  128. tnfr/utils/data.py +95 -5
  129. tnfr/utils/data.pyi +8 -17
  130. tnfr/utils/graph.py +2 -4
  131. tnfr/utils/init.py +31 -7
  132. tnfr/utils/init.pyi +4 -11
  133. tnfr/utils/io.py +313 -14
  134. tnfr/{helpers → utils}/numeric.py +50 -24
  135. tnfr/utils/numeric.pyi +21 -0
  136. tnfr/validation/__init__.py +92 -4
  137. tnfr/validation/__init__.pyi +77 -17
  138. tnfr/validation/compatibility.py +79 -43
  139. tnfr/validation/compatibility.pyi +4 -6
  140. tnfr/validation/grammar.py +55 -133
  141. tnfr/validation/grammar.pyi +37 -8
  142. tnfr/validation/graph.py +138 -0
  143. tnfr/validation/graph.pyi +17 -0
  144. tnfr/validation/rules.py +161 -74
  145. tnfr/validation/rules.pyi +55 -18
  146. tnfr/validation/runtime.py +263 -0
  147. tnfr/validation/runtime.pyi +31 -0
  148. tnfr/validation/soft_filters.py +170 -0
  149. tnfr/validation/soft_filters.pyi +37 -0
  150. tnfr/validation/spectral.py +159 -0
  151. tnfr/validation/spectral.pyi +46 -0
  152. tnfr/validation/syntax.py +28 -139
  153. tnfr/validation/syntax.pyi +7 -4
  154. tnfr/validation/window.py +39 -0
  155. tnfr/validation/window.pyi +1 -0
  156. tnfr/viz/__init__.py +9 -0
  157. tnfr/viz/matplotlib.py +246 -0
  158. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
  159. tnfr-7.0.0.dist-info/RECORD +185 -0
  160. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  161. tnfr/constants_glyphs.py +0 -16
  162. tnfr/constants_glyphs.pyi +0 -12
  163. tnfr/grammar.py +0 -25
  164. tnfr/grammar.pyi +0 -13
  165. tnfr/helpers/__init__.py +0 -151
  166. tnfr/helpers/__init__.pyi +0 -66
  167. tnfr/helpers/numeric.pyi +0 -12
  168. tnfr/presets.py +0 -15
  169. tnfr/presets.pyi +0 -7
  170. tnfr/utils/io.pyi +0 -10
  171. tnfr/utils/validators.py +0 -130
  172. tnfr/utils/validators.pyi +0 -19
  173. tnfr-6.0.0.dist-info/RECORD +0 -157
  174. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  175. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  176. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
@@ -2,36 +2,52 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Callable, Iterator
6
- from typing import Any, TYPE_CHECKING
7
- import math
8
5
  import heapq
6
+ import math
7
+ from collections.abc import Callable, Iterator
9
8
  from itertools import islice
10
- from statistics import fmean, StatisticsError
9
+ from statistics import StatisticsError, fmean
10
+ from typing import TYPE_CHECKING, Any
11
11
 
12
- from ..alias import get_attr
13
- from ..constants import DEFAULTS, get_aliases, get_param
12
+ from tnfr import glyph_history
14
13
 
15
- from ..helpers.numeric import angle_diff
14
+ from ..alias import get_attr
15
+ from ..constants import DEFAULTS, get_param
16
+ from ..constants.aliases import ALIAS_EPI
17
+ from ..utils import angle_diff
16
18
  from ..metrics.trig import neighbor_phase_mean
17
- from ..utils import get_nodenx
18
19
  from ..rng import make_rng
19
- from tnfr import glyph_history
20
20
  from ..types import EPIValue, Glyph, NodeId, TNFRGraph
21
-
21
+ from ..utils import get_nodenx
22
22
  from . import definitions as _definitions
23
+ from .grammar import (
24
+ GrammarContext,
25
+ StructuralGrammarError,
26
+ RepeatWindowError,
27
+ MutationPreconditionError,
28
+ TholClosureError,
29
+ TransitionCompatibilityError,
30
+ SequenceSyntaxError,
31
+ SequenceValidationResult,
32
+ _gram_state,
33
+ apply_glyph_with_grammar,
34
+ enforce_canonical_grammar,
35
+ on_applied_glyph,
36
+ parse_sequence,
37
+ validate_sequence,
38
+ )
23
39
  from .jitter import (
24
40
  JitterCache,
25
41
  JitterCacheManager,
26
42
  get_jitter_manager,
27
- reset_jitter_manager,
28
43
  random_jitter,
44
+ reset_jitter_manager,
29
45
  )
30
46
  from .registry import OPERATORS, discover_operators, get_operator_class
31
47
  from .remesh import (
32
48
  apply_network_remesh,
33
- apply_topological_remesh,
34
49
  apply_remesh_if_globally_stable,
50
+ apply_topological_remesh,
35
51
  )
36
52
 
37
53
  _remesh_doc = (
@@ -49,8 +65,7 @@ else:
49
65
  discover_operators()
50
66
 
51
67
  _DEFINITION_EXPORTS = {
52
- name: getattr(_definitions, name)
53
- for name in getattr(_definitions, "__all__", ())
68
+ name: getattr(_definitions, name) for name in getattr(_definitions, "__all__", ())
54
69
  }
55
70
  globals().update(_DEFINITION_EXPORTS)
56
71
 
@@ -60,14 +75,26 @@ if TYPE_CHECKING: # pragma: no cover - type checking only
60
75
  GlyphFactors = dict[str, Any]
61
76
  GlyphOperation = Callable[["NodeProtocol", GlyphFactors], None]
62
77
 
63
- ALIAS_EPI = get_aliases("EPI")
64
-
65
78
  __all__ = [
66
79
  "JitterCache",
67
80
  "JitterCacheManager",
68
81
  "get_jitter_manager",
69
82
  "reset_jitter_manager",
70
83
  "random_jitter",
84
+ "GrammarContext",
85
+ "StructuralGrammarError",
86
+ "RepeatWindowError",
87
+ "MutationPreconditionError",
88
+ "TholClosureError",
89
+ "TransitionCompatibilityError",
90
+ "SequenceValidationResult",
91
+ "SequenceSyntaxError",
92
+ "_gram_state",
93
+ "apply_glyph_with_grammar",
94
+ "parse_sequence",
95
+ "validate_sequence",
96
+ "enforce_canonical_grammar",
97
+ "on_applied_glyph",
71
98
  "get_neighbor_epi",
72
99
  "get_glyph_factors",
73
100
  "GLYPH_OPERATIONS",
@@ -85,22 +112,111 @@ __all__.extend(_DEFINITION_EXPORTS.keys())
85
112
 
86
113
 
87
114
  def get_glyph_factors(node: NodeProtocol) -> GlyphFactors:
88
- """Return glyph factors for ``node`` with defaults."""
115
+ """Fetch glyph tuning factors for a node.
116
+
117
+ The glyph factors expose per-operator coefficients that modulate how an
118
+ operator reorganizes a node's Primary Information Structure (EPI),
119
+ structural frequency (νf), internal reorganization differential (ΔNFR), and
120
+ phase. Missing factors fall back to the canonical defaults stored at the
121
+ graph level.
122
+
123
+ Parameters
124
+ ----------
125
+ node : NodeProtocol
126
+ TNFR node providing a ``graph`` mapping where glyph factors may be
127
+ cached under ``"GLYPH_FACTORS"``.
128
+
129
+ Returns
130
+ -------
131
+ GlyphFactors
132
+ Mapping with operator-specific coefficients merged with the canonical
133
+ defaults. Mutating the returned mapping does not affect the graph.
134
+
135
+ Examples
136
+ --------
137
+ >>> class MockNode:
138
+ ... def __init__(self):
139
+ ... self.graph = {"GLYPH_FACTORS": {"AL_boost": 0.2}}
140
+ >>> node = MockNode()
141
+ >>> factors = get_glyph_factors(node)
142
+ >>> factors["AL_boost"]
143
+ 0.2
144
+ >>> factors["EN_mix"] # Fallback to the default reception mix
145
+ 0.25
146
+ """
89
147
  return node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"].copy())
90
148
 
91
149
 
92
150
  def get_factor(gf: GlyphFactors, key: str, default: float) -> float:
93
- """Return ``gf[key]`` as ``float`` with ``default`` fallback."""
151
+ """Return a glyph factor as ``float`` with a default fallback.
152
+
153
+ Parameters
154
+ ----------
155
+ gf : GlyphFactors
156
+ Mapping of glyph names to numeric factors.
157
+ key : str
158
+ Factor identifier to look up.
159
+ default : float
160
+ Value used when ``key`` is absent. This typically corresponds to the
161
+ canonical operator tuning and protects structural invariants.
162
+
163
+ Returns
164
+ -------
165
+ float
166
+ The resolved factor converted to ``float``.
167
+
168
+ Examples
169
+ --------
170
+ >>> get_factor({"AL_boost": 0.3}, "AL_boost", 0.05)
171
+ 0.3
172
+ >>> get_factor({}, "IL_dnfr_factor", 0.7)
173
+ 0.7
174
+ """
94
175
  return float(gf.get(key, default))
95
176
 
96
177
 
97
178
  # -------------------------
98
- # Glyphs (operadores locales)
179
+ # Glyphs (local operators)
99
180
  # -------------------------
100
181
 
101
182
 
102
183
  def get_neighbor_epi(node: NodeProtocol) -> tuple[list[NodeProtocol], EPIValue]:
103
- """Return neighbour list and their mean ``EPI`` without mutating ``node``."""
184
+ """Collect neighbour nodes and their mean EPI.
185
+
186
+ The neighbour EPI is used by reception-like glyphs (e.g., EN, RA) to
187
+ harmonise the node's EPI with the surrounding field without mutating νf,
188
+ ΔNFR, or phase. When a neighbour lacks a direct ``EPI`` attribute the
189
+ function resolves it from NetworkX metadata using known aliases.
190
+
191
+ Parameters
192
+ ----------
193
+ node : NodeProtocol
194
+ Node whose neighbours participate in the averaging.
195
+
196
+ Returns
197
+ -------
198
+ list of NodeProtocol
199
+ Concrete neighbour objects that expose TNFR attributes.
200
+ EPIValue
201
+ Arithmetic mean of the neighbouring EPIs. Equals the node EPI when no
202
+ valid neighbours are found, allowing glyphs to preserve the node state.
203
+
204
+ Examples
205
+ --------
206
+ >>> class MockNode:
207
+ ... def __init__(self, epi, neighbors):
208
+ ... self.EPI = epi
209
+ ... self._neighbors = neighbors
210
+ ... self.graph = {}
211
+ ... def neighbors(self):
212
+ ... return self._neighbors
213
+ >>> neigh_a = MockNode(1.0, [])
214
+ >>> neigh_b = MockNode(2.0, [])
215
+ >>> node = MockNode(0.5, [neigh_a, neigh_b])
216
+ >>> neighbors, epi_bar = get_neighbor_epi(node)
217
+ >>> len(neighbors), round(epi_bar, 2)
218
+ (2, 1.5)
219
+ """
104
220
 
105
221
  epi = node.EPI
106
222
  neigh = list(node.neighbors())
@@ -134,8 +250,7 @@ def get_neighbor_epi(node: NodeProtocol) -> tuple[list[NodeProtocol], EPIValue]:
134
250
  if NodeNX is None:
135
251
  raise ImportError("NodeNX is unavailable")
136
252
  neigh = [
137
- v if hasattr(v, "EPI") else NodeNX.from_graph(node.G, v)
138
- for v in neigh
253
+ v if hasattr(v, "EPI") else NodeNX.from_graph(node.G, v) for v in neigh
139
254
  ]
140
255
  else:
141
256
  try:
@@ -149,7 +264,35 @@ def get_neighbor_epi(node: NodeProtocol) -> tuple[list[NodeProtocol], EPIValue]:
149
264
  def _determine_dominant(
150
265
  neigh: list[NodeProtocol], default_kind: str
151
266
  ) -> tuple[str, float]:
152
- """Return dominant ``epi_kind`` among ``neigh`` and its absolute ``EPI``."""
267
+ """Resolve the dominant ``epi_kind`` across neighbours.
268
+
269
+ The dominant kind guides glyphs that synchronise EPI, ensuring that
270
+ reshaping a node's EPI also maintains a coherent semantic label for the
271
+ structural phase space.
272
+
273
+ Parameters
274
+ ----------
275
+ neigh : list of NodeProtocol
276
+ Neighbouring nodes providing EPI magnitude and semantic kind.
277
+ default_kind : str
278
+ Fallback label when no neighbour exposes an ``epi_kind``.
279
+
280
+ Returns
281
+ -------
282
+ tuple of (str, float)
283
+ The dominant ``epi_kind`` together with the maximum absolute EPI. The
284
+ amplitude assists downstream logic when choosing between the node's own
285
+ label and the neighbour-driven kind.
286
+
287
+ Examples
288
+ --------
289
+ >>> class Mock:
290
+ ... def __init__(self, epi, kind):
291
+ ... self.EPI = epi
292
+ ... self.epi_kind = kind
293
+ >>> _determine_dominant([Mock(0.2, "seed"), Mock(-1.0, "pulse")], "seed")
294
+ ('pulse', 1.0)
295
+ """
153
296
  best_kind: str | None = None
154
297
  best_abs = 0.0
155
298
  for v in neigh:
@@ -165,11 +308,46 @@ def _determine_dominant(
165
308
  def _mix_epi_with_neighbors(
166
309
  node: NodeProtocol, mix: float, default_glyph: Glyph | str
167
310
  ) -> tuple[float, str]:
168
- """Mix ``EPI`` of ``node`` with the mean of its neighbours."""
311
+ """Blend node EPI with the neighbour field and update its semantic label.
312
+
313
+ The routine is shared by reception-like glyphs. It interpolates between the
314
+ node EPI and the neighbour mean while selecting a dominant ``epi_kind``.
315
+ ΔNFR, νf, and phase remain untouched; the function focuses on reconciling
316
+ form.
317
+
318
+ Parameters
319
+ ----------
320
+ node : NodeProtocol
321
+ Node that exposes ``EPI`` and ``epi_kind`` attributes.
322
+ mix : float
323
+ Interpolation weight for the neighbour mean. ``mix = 0`` preserves the
324
+ current EPI, while ``mix = 1`` adopts the average neighbour field.
325
+ default_glyph : Glyph or str
326
+ Glyph driving the mix. Its value informs the fallback ``epi_kind``.
327
+
328
+ Returns
329
+ -------
330
+ tuple of (float, str)
331
+ The neighbour mean EPI and the resolved ``epi_kind`` after mixing.
332
+
333
+ Examples
334
+ --------
335
+ >>> class MockNode:
336
+ ... def __init__(self, epi, kind, neighbors):
337
+ ... self.EPI = epi
338
+ ... self.epi_kind = kind
339
+ ... self.graph = {}
340
+ ... self._neighbors = neighbors
341
+ ... def neighbors(self):
342
+ ... return self._neighbors
343
+ >>> neigh = [MockNode(0.8, "wave", []), MockNode(1.2, "wave", [])]
344
+ >>> node = MockNode(0.0, "seed", neigh)
345
+ >>> _, kind = _mix_epi_with_neighbors(node, 0.5, Glyph.EN)
346
+ >>> round(node.EPI, 2), kind
347
+ (0.5, 'wave')
348
+ """
169
349
  default_kind = (
170
- default_glyph.value
171
- if isinstance(default_glyph, Glyph)
172
- else str(default_glyph)
350
+ default_glyph.value if isinstance(default_glyph, Glyph) else str(default_glyph)
173
351
  )
174
352
  epi = node.EPI
175
353
  neigh, epi_bar = get_neighbor_epi(node)
@@ -189,21 +367,119 @@ def _mix_epi_with_neighbors(
189
367
 
190
368
 
191
369
  def _op_AL(node: NodeProtocol, gf: GlyphFactors) -> None: # AL — Emission
370
+ """Amplify the node EPI via the Emission glyph.
371
+
372
+ Emission injects additional coherence into the node by boosting its EPI
373
+ without touching νf, ΔNFR, or phase. The boost amplitude is controlled by
374
+ ``AL_boost``.
375
+
376
+ Parameters
377
+ ----------
378
+ node : NodeProtocol
379
+ Node whose EPI is increased.
380
+ gf : GlyphFactors
381
+ Factor mapping used to resolve ``AL_boost``.
382
+
383
+ Examples
384
+ --------
385
+ >>> class MockNode:
386
+ ... def __init__(self, epi):
387
+ ... self.EPI = epi
388
+ >>> node = MockNode(0.8)
389
+ >>> _op_AL(node, {"AL_boost": 0.2})
390
+ >>> node.EPI
391
+ 1.0
392
+ """
192
393
  f = get_factor(gf, "AL_boost", 0.05)
193
394
  node.EPI = node.EPI + f
194
395
 
195
396
 
196
397
  def _op_EN(node: NodeProtocol, gf: GlyphFactors) -> None: # EN — Reception
398
+ """Mix the node EPI with the neighbour field via Reception.
399
+
400
+ Reception reorganizes the node's EPI towards the neighbourhood mean while
401
+ choosing a coherent ``epi_kind``. νf, ΔNFR, and phase remain unchanged.
402
+
403
+ Parameters
404
+ ----------
405
+ node : NodeProtocol
406
+ Node whose EPI is being reconciled.
407
+ gf : GlyphFactors
408
+ Source of the ``EN_mix`` blending coefficient.
409
+
410
+ Examples
411
+ --------
412
+ >>> class MockNode:
413
+ ... def __init__(self, epi, neighbors):
414
+ ... self.EPI = epi
415
+ ... self.epi_kind = "seed"
416
+ ... self.graph = {}
417
+ ... self._neighbors = neighbors
418
+ ... def neighbors(self):
419
+ ... return self._neighbors
420
+ >>> neigh = [MockNode(1.0, []), MockNode(0.0, [])]
421
+ >>> node = MockNode(0.4, neigh)
422
+ >>> _op_EN(node, {"EN_mix": 0.5})
423
+ >>> round(node.EPI, 2)
424
+ 0.7
425
+ """
197
426
  mix = get_factor(gf, "EN_mix", 0.25)
198
427
  _mix_epi_with_neighbors(node, mix, Glyph.EN)
199
428
 
200
429
 
201
430
  def _op_IL(node: NodeProtocol, gf: GlyphFactors) -> None: # IL — Coherence
431
+ """Dampen ΔNFR magnitudes through the Coherence glyph.
432
+
433
+ Coherence contracts the internal reorganization differential (ΔNFR) while
434
+ leaving EPI, νf, and phase untouched. The contraction preserves the sign of
435
+ ΔNFR, increasing structural stability.
436
+
437
+ Parameters
438
+ ----------
439
+ node : NodeProtocol
440
+ Node whose ΔNFR is being scaled.
441
+ gf : GlyphFactors
442
+ Provides ``IL_dnfr_factor`` controlling the contraction strength.
443
+
444
+ Examples
445
+ --------
446
+ >>> class MockNode:
447
+ ... def __init__(self, dnfr):
448
+ ... self.dnfr = dnfr
449
+ >>> node = MockNode(0.5)
450
+ >>> _op_IL(node, {"IL_dnfr_factor": 0.2})
451
+ >>> node.dnfr
452
+ 0.1
453
+ """
202
454
  factor = get_factor(gf, "IL_dnfr_factor", 0.7)
203
455
  node.dnfr = factor * getattr(node, "dnfr", 0.0)
204
456
 
205
457
 
206
458
  def _op_OZ(node: NodeProtocol, gf: GlyphFactors) -> None: # OZ — Dissonance
459
+ """Excite ΔNFR through the Dissonance glyph.
460
+
461
+ Dissonance amplifies ΔNFR or injects jitter, testing the node's stability.
462
+ EPI, νf, and phase remain unaffected while ΔNFR grows to trigger potential
463
+ bifurcations.
464
+
465
+ Parameters
466
+ ----------
467
+ node : NodeProtocol
468
+ Node whose ΔNFR is being stressed.
469
+ gf : GlyphFactors
470
+ Supplies ``OZ_dnfr_factor`` and optional noise parameters.
471
+
472
+ Examples
473
+ --------
474
+ >>> class MockNode:
475
+ ... def __init__(self, dnfr):
476
+ ... self.dnfr = dnfr
477
+ ... self.graph = {}
478
+ >>> node = MockNode(0.2)
479
+ >>> _op_OZ(node, {"OZ_dnfr_factor": 2.0})
480
+ >>> node.dnfr
481
+ 0.4
482
+ """
207
483
  factor = get_factor(gf, "OZ_dnfr_factor", 1.3)
208
484
  dnfr = getattr(node, "dnfr", 0.0)
209
485
  if bool(node.graph.get("OZ_NOISE_MODE", False)):
@@ -226,9 +502,7 @@ def _um_candidate_iter(node: NodeProtocol) -> Iterator[NodeProtocol]:
226
502
  else:
227
503
  base = node.all_nodes()
228
504
  for j in base:
229
- same = (j is node) or (
230
- getattr(node, "n", None) == getattr(j, "n", None)
231
- )
505
+ same = (j is node) or (getattr(node, "n", None) == getattr(j, "n", None))
232
506
  if same or node.has_edge(j):
233
507
  continue
234
508
  yield j
@@ -265,6 +539,45 @@ def _um_select_candidates(
265
539
 
266
540
 
267
541
  def _op_UM(node: NodeProtocol, gf: GlyphFactors) -> None: # UM — Coupling
542
+ """Align node phase with neighbours and optionally create links.
543
+
544
+ Coupling shifts the node phase ``theta`` towards the neighbour mean while
545
+ respecting νf and EPI. When functional links are enabled it may add edges
546
+ based on combined phase, EPI, and sense-index similarity.
547
+
548
+ Parameters
549
+ ----------
550
+ node : NodeProtocol
551
+ Node whose phase is being synchronised.
552
+ gf : GlyphFactors
553
+ Provides ``UM_theta_push`` and optional selection parameters.
554
+
555
+ Examples
556
+ --------
557
+ >>> import math
558
+ >>> class MockNode:
559
+ ... def __init__(self, theta, neighbors):
560
+ ... self.theta = theta
561
+ ... self.EPI = 1.0
562
+ ... self.Si = 0.5
563
+ ... self.graph = {}
564
+ ... self._neighbors = neighbors
565
+ ... def neighbors(self):
566
+ ... return self._neighbors
567
+ ... def offset(self):
568
+ ... return 0
569
+ ... def all_nodes(self):
570
+ ... return []
571
+ ... def has_edge(self, _):
572
+ ... return False
573
+ ... def add_edge(self, *_):
574
+ ... raise AssertionError("not used in example")
575
+ >>> neighbor = MockNode(math.pi / 2, [])
576
+ >>> node = MockNode(0.0, [neighbor])
577
+ >>> _op_UM(node, {"UM_theta_push": 0.5})
578
+ >>> round(node.theta, 2)
579
+ 0.79
580
+ """
268
581
  k = get_factor(gf, "UM_theta_push", 0.25)
269
582
  th = node.theta
270
583
  thL = neighbor_phase_mean(node)
@@ -292,9 +605,7 @@ def _op_UM(node: NodeProtocol, gf: GlyphFactors) -> None: # UM — Coupling
292
605
  dphi = abs(angle_diff(th_j, th)) / math.pi
293
606
  epi_j = j.EPI
294
607
  si_j = j.Si
295
- epi_sim = 1.0 - abs(epi_i - epi_j) / (
296
- abs(epi_i) + abs(epi_j) + 1e-9
297
- )
608
+ epi_sim = 1.0 - abs(epi_i - epi_j) / (abs(epi_i) + abs(epi_j) + 1e-9)
298
609
  si_sim = 1.0 - abs(si_i - si_j)
299
610
  compat = (1 - dphi) * 0.5 + 0.25 * epi_sim + 0.25 * si_sim
300
611
  if compat >= thr:
@@ -302,11 +613,63 @@ def _op_UM(node: NodeProtocol, gf: GlyphFactors) -> None: # UM — Coupling
302
613
 
303
614
 
304
615
  def _op_RA(node: NodeProtocol, gf: GlyphFactors) -> None: # RA — Resonance
616
+ """Diffuse EPI to the node through the Resonance glyph.
617
+
618
+ Resonance propagates EPI along existing couplings without affecting νf,
619
+ ΔNFR, or phase. The glyph nudges the node towards the neighbour mean using
620
+ ``RA_epi_diff``.
621
+
622
+ Parameters
623
+ ----------
624
+ node : NodeProtocol
625
+ Node harmonising with its neighbourhood.
626
+ gf : GlyphFactors
627
+ Provides ``RA_epi_diff`` as the mixing coefficient.
628
+
629
+ Examples
630
+ --------
631
+ >>> class MockNode:
632
+ ... def __init__(self, epi, neighbors):
633
+ ... self.EPI = epi
634
+ ... self.epi_kind = "seed"
635
+ ... self.graph = {}
636
+ ... self._neighbors = neighbors
637
+ ... def neighbors(self):
638
+ ... return self._neighbors
639
+ >>> neighbor = MockNode(1.0, [])
640
+ >>> node = MockNode(0.2, [neighbor])
641
+ >>> _op_RA(node, {"RA_epi_diff": 0.25})
642
+ >>> round(node.EPI, 2)
643
+ 0.4
644
+ """
305
645
  diff = get_factor(gf, "RA_epi_diff", 0.15)
306
646
  _mix_epi_with_neighbors(node, diff, Glyph.RA)
307
647
 
308
648
 
309
649
  def _op_SHA(node: NodeProtocol, gf: GlyphFactors) -> None: # SHA — Silence
650
+ """Reduce νf while preserving EPI, ΔNFR, and phase.
651
+
652
+ Silence decelerates a node by scaling νf (structural frequency) towards
653
+ stillness. EPI, ΔNFR, and phase remain unchanged, signalling a temporary
654
+ suspension of structural evolution.
655
+
656
+ Parameters
657
+ ----------
658
+ node : NodeProtocol
659
+ Node whose νf is being attenuated.
660
+ gf : GlyphFactors
661
+ Provides ``SHA_vf_factor`` to scale νf.
662
+
663
+ Examples
664
+ --------
665
+ >>> class MockNode:
666
+ ... def __init__(self, vf):
667
+ ... self.vf = vf
668
+ >>> node = MockNode(1.0)
669
+ >>> _op_SHA(node, {"SHA_vf_factor": 0.5})
670
+ >>> node.vf
671
+ 0.5
672
+ """
310
673
  factor = get_factor(gf, "SHA_vf_factor", 0.85)
311
674
  node.vf = factor * node.vf
312
675
 
@@ -317,6 +680,15 @@ _SCALE_FACTORS = {Glyph.VAL: factor_val, Glyph.NUL: factor_nul}
317
680
 
318
681
 
319
682
  def _op_scale(node: NodeProtocol, factor: float) -> None:
683
+ """Scale νf with the provided factor.
684
+
685
+ Parameters
686
+ ----------
687
+ node : NodeProtocol
688
+ Node whose νf is being updated.
689
+ factor : float
690
+ Multiplicative change applied to νf.
691
+ """
320
692
  node.vf *= factor
321
693
 
322
694
 
@@ -327,26 +699,118 @@ def _make_scale_op(glyph: Glyph) -> GlyphOperation:
327
699
  factor = get_factor(gf, key, default)
328
700
  _op_scale(node, factor)
329
701
 
702
+ _op.__doc__ = (
703
+ """{} glyph scales νf to modulate expansion or contraction.
704
+
705
+ VAL (expansion) increases νf, whereas NUL (contraction) decreases it.
706
+ EPI, ΔNFR, and phase remain fixed, isolating the change to temporal
707
+ cadence.
708
+
709
+ Parameters
710
+ ----------
711
+ node : NodeProtocol
712
+ Node whose νf is updated.
713
+ gf : GlyphFactors
714
+ Provides the respective scale factor (``VAL_scale`` or
715
+ ``NUL_scale``).
716
+
717
+ Examples
718
+ --------
719
+ >>> class MockNode:
720
+ ... def __init__(self, vf):
721
+ ... self.vf = vf
722
+ >>> node = MockNode(1.0)
723
+ >>> op = _make_scale_op(Glyph.VAL)
724
+ >>> op(node, {{"VAL_scale": 1.5}})
725
+ >>> node.vf
726
+ 1.5
727
+ """.format(glyph.name)
728
+ )
330
729
  return _op
331
730
 
332
731
 
333
- def _op_THOL(
334
- node: NodeProtocol, gf: GlyphFactors
335
- ) -> None: # THOL — Self-organization
732
+ def _op_THOL(node: NodeProtocol, gf: GlyphFactors) -> None: # THOL — Self-organization
733
+ """Inject curvature from ``d2EPI`` into ΔNFR to trigger self-organization.
734
+
735
+ The glyph keeps EPI, νf, and phase fixed while increasing ΔNFR according to
736
+ the second derivative of EPI, accelerating structural rearrangement.
737
+
738
+ Parameters
739
+ ----------
740
+ node : NodeProtocol
741
+ Node contributing ``d2EPI`` to ΔNFR.
742
+ gf : GlyphFactors
743
+ Source of the ``THOL_accel`` multiplier.
744
+
745
+ Examples
746
+ --------
747
+ >>> class MockNode:
748
+ ... def __init__(self, dnfr, curvature):
749
+ ... self.dnfr = dnfr
750
+ ... self.d2EPI = curvature
751
+ >>> node = MockNode(0.1, 0.5)
752
+ >>> _op_THOL(node, {"THOL_accel": 0.2})
753
+ >>> node.dnfr
754
+ 0.2
755
+ """
336
756
  a = get_factor(gf, "THOL_accel", 0.10)
337
757
  node.dnfr = node.dnfr + a * getattr(node, "d2EPI", 0.0)
338
758
 
339
759
 
340
- def _op_ZHIR(
341
- node: NodeProtocol, gf: GlyphFactors
342
- ) -> None: # ZHIR — Mutation
760
+ def _op_ZHIR(node: NodeProtocol, gf: GlyphFactors) -> None: # ZHIR — Mutation
761
+ """Shift phase by a fixed offset to enact mutation.
762
+
763
+ Mutation changes the node's phase (θ) while preserving EPI, νf, and ΔNFR.
764
+ The glyph encodes discrete structural transitions between coherent states.
765
+
766
+ Parameters
767
+ ----------
768
+ node : NodeProtocol
769
+ Node whose phase is rotated.
770
+ gf : GlyphFactors
771
+ Supplies ``ZHIR_theta_shift`` defining the rotation.
772
+
773
+ Examples
774
+ --------
775
+ >>> import math
776
+ >>> class MockNode:
777
+ ... def __init__(self, theta):
778
+ ... self.theta = theta
779
+ >>> node = MockNode(0.0)
780
+ >>> _op_ZHIR(node, {"ZHIR_theta_shift": math.pi / 2})
781
+ >>> round(node.theta, 2)
782
+ 1.57
783
+ """
343
784
  shift = get_factor(gf, "ZHIR_theta_shift", math.pi / 2)
344
785
  node.theta = node.theta + shift
345
786
 
346
787
 
347
- def _op_NAV(
348
- node: NodeProtocol, gf: GlyphFactors
349
- ) -> None: # NAV — Transition
788
+ def _op_NAV(node: NodeProtocol, gf: GlyphFactors) -> None: # NAV — Transition
789
+ """Rebalance ΔNFR towards νf while permitting jitter.
790
+
791
+ Transition pulls ΔNFR towards a νf-aligned target, optionally adding jitter
792
+ to explore nearby states. EPI and phase remain untouched; νf may be used as
793
+ a reference but is not directly changed.
794
+
795
+ Parameters
796
+ ----------
797
+ node : NodeProtocol
798
+ Node whose ΔNFR is redirected.
799
+ gf : GlyphFactors
800
+ Supplies ``NAV_eta`` and ``NAV_jitter`` tuning parameters.
801
+
802
+ Examples
803
+ --------
804
+ >>> class MockNode:
805
+ ... def __init__(self, dnfr, vf):
806
+ ... self.dnfr = dnfr
807
+ ... self.vf = vf
808
+ ... self.graph = {"NAV_RANDOM": False}
809
+ >>> node = MockNode(-0.6, 0.4)
810
+ >>> _op_NAV(node, {"NAV_eta": 0.5, "NAV_jitter": 0.0})
811
+ >>> round(node.dnfr, 2)
812
+ -0.1
813
+ """
350
814
  dnfr = node.dnfr
351
815
  vf = node.vf
352
816
  eta = get_factor(gf, "NAV_eta", 0.5)
@@ -368,6 +832,29 @@ def _op_NAV(
368
832
  def _op_REMESH(
369
833
  node: NodeProtocol, gf: GlyphFactors | None = None
370
834
  ) -> None: # REMESH — advisory
835
+ """Record an advisory requesting network-scale remeshing.
836
+
837
+ REMESH does not change node-level EPI, νf, ΔNFR, or phase. Instead it
838
+ annotates the glyph history so orchestrators can trigger global remesh
839
+ procedures once the stability conditions are met.
840
+
841
+ Parameters
842
+ ----------
843
+ node : NodeProtocol
844
+ Node whose history records the advisory.
845
+ gf : GlyphFactors, optional
846
+ Unused but accepted for API symmetry.
847
+
848
+ Examples
849
+ --------
850
+ >>> class MockNode:
851
+ ... def __init__(self):
852
+ ... self.graph = {}
853
+ >>> node = MockNode()
854
+ >>> _op_REMESH(node)
855
+ >>> "_remesh_warn_step" in node.graph
856
+ True
857
+ """
371
858
  step_idx = glyph_history.current_step_idx(node)
372
859
  last_warn = node.graph.get("_remesh_warn_step", None)
373
860
  if last_warn != step_idx: