tnfr 4.5.2__py3-none-any.whl → 7.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (195) hide show
  1. tnfr/__init__.py +275 -51
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +117 -31
  8. tnfr/alias.pyi +108 -0
  9. tnfr/cache.py +6 -572
  10. tnfr/cache.pyi +16 -0
  11. tnfr/callback_utils.py +16 -38
  12. tnfr/callback_utils.pyi +79 -0
  13. tnfr/cli/__init__.py +34 -14
  14. tnfr/cli/__init__.pyi +26 -0
  15. tnfr/cli/arguments.py +211 -28
  16. tnfr/cli/arguments.pyi +27 -0
  17. tnfr/cli/execution.py +470 -50
  18. tnfr/cli/execution.pyi +70 -0
  19. tnfr/cli/utils.py +18 -3
  20. tnfr/cli/utils.pyi +8 -0
  21. tnfr/config/__init__.py +13 -0
  22. tnfr/config/__init__.pyi +10 -0
  23. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  24. tnfr/config/constants.pyi +12 -0
  25. tnfr/config/feature_flags.py +83 -0
  26. tnfr/{config.py → config/init.py} +11 -7
  27. tnfr/config/init.pyi +8 -0
  28. tnfr/config/operator_names.py +93 -0
  29. tnfr/config/operator_names.pyi +28 -0
  30. tnfr/config/presets.py +84 -0
  31. tnfr/config/presets.pyi +7 -0
  32. tnfr/constants/__init__.py +80 -29
  33. tnfr/constants/__init__.pyi +92 -0
  34. tnfr/constants/aliases.py +31 -0
  35. tnfr/constants/core.py +4 -4
  36. tnfr/constants/core.pyi +17 -0
  37. tnfr/constants/init.py +1 -1
  38. tnfr/constants/init.pyi +12 -0
  39. tnfr/constants/metric.py +7 -15
  40. tnfr/constants/metric.pyi +19 -0
  41. tnfr/dynamics/__init__.py +165 -633
  42. tnfr/dynamics/__init__.pyi +82 -0
  43. tnfr/dynamics/adaptation.py +267 -0
  44. tnfr/dynamics/aliases.py +23 -0
  45. tnfr/dynamics/coordination.py +385 -0
  46. tnfr/dynamics/dnfr.py +2283 -400
  47. tnfr/dynamics/dnfr.pyi +24 -0
  48. tnfr/dynamics/integrators.py +406 -98
  49. tnfr/dynamics/integrators.pyi +34 -0
  50. tnfr/dynamics/runtime.py +881 -0
  51. tnfr/dynamics/sampling.py +10 -5
  52. tnfr/dynamics/sampling.pyi +7 -0
  53. tnfr/dynamics/selectors.py +719 -0
  54. tnfr/execution.py +70 -48
  55. tnfr/execution.pyi +45 -0
  56. tnfr/flatten.py +13 -9
  57. tnfr/flatten.pyi +21 -0
  58. tnfr/gamma.py +66 -53
  59. tnfr/gamma.pyi +34 -0
  60. tnfr/glyph_history.py +110 -52
  61. tnfr/glyph_history.pyi +35 -0
  62. tnfr/glyph_runtime.py +16 -0
  63. tnfr/glyph_runtime.pyi +9 -0
  64. tnfr/immutable.py +69 -28
  65. tnfr/immutable.pyi +34 -0
  66. tnfr/initialization.py +16 -16
  67. tnfr/initialization.pyi +65 -0
  68. tnfr/io.py +6 -240
  69. tnfr/io.pyi +16 -0
  70. tnfr/locking.pyi +7 -0
  71. tnfr/mathematics/__init__.py +81 -0
  72. tnfr/mathematics/backend.py +426 -0
  73. tnfr/mathematics/dynamics.py +398 -0
  74. tnfr/mathematics/epi.py +254 -0
  75. tnfr/mathematics/generators.py +222 -0
  76. tnfr/mathematics/metrics.py +119 -0
  77. tnfr/mathematics/operators.py +233 -0
  78. tnfr/mathematics/operators_factory.py +71 -0
  79. tnfr/mathematics/projection.py +78 -0
  80. tnfr/mathematics/runtime.py +173 -0
  81. tnfr/mathematics/spaces.py +247 -0
  82. tnfr/mathematics/transforms.py +292 -0
  83. tnfr/metrics/__init__.py +10 -10
  84. tnfr/metrics/__init__.pyi +20 -0
  85. tnfr/metrics/coherence.py +993 -324
  86. tnfr/metrics/common.py +23 -16
  87. tnfr/metrics/common.pyi +46 -0
  88. tnfr/metrics/core.py +251 -35
  89. tnfr/metrics/core.pyi +13 -0
  90. tnfr/metrics/diagnosis.py +708 -111
  91. tnfr/metrics/diagnosis.pyi +85 -0
  92. tnfr/metrics/export.py +27 -15
  93. tnfr/metrics/glyph_timing.py +232 -42
  94. tnfr/metrics/reporting.py +33 -22
  95. tnfr/metrics/reporting.pyi +12 -0
  96. tnfr/metrics/sense_index.py +987 -43
  97. tnfr/metrics/sense_index.pyi +9 -0
  98. tnfr/metrics/trig.py +214 -23
  99. tnfr/metrics/trig.pyi +13 -0
  100. tnfr/metrics/trig_cache.py +115 -22
  101. tnfr/metrics/trig_cache.pyi +10 -0
  102. tnfr/node.py +542 -136
  103. tnfr/node.pyi +178 -0
  104. tnfr/observers.py +152 -35
  105. tnfr/observers.pyi +31 -0
  106. tnfr/ontosim.py +23 -19
  107. tnfr/ontosim.pyi +28 -0
  108. tnfr/operators/__init__.py +601 -82
  109. tnfr/operators/__init__.pyi +45 -0
  110. tnfr/operators/definitions.py +513 -0
  111. tnfr/operators/definitions.pyi +78 -0
  112. tnfr/operators/grammar.py +760 -0
  113. tnfr/operators/jitter.py +107 -38
  114. tnfr/operators/jitter.pyi +11 -0
  115. tnfr/operators/registry.py +75 -0
  116. tnfr/operators/registry.pyi +13 -0
  117. tnfr/operators/remesh.py +149 -88
  118. tnfr/py.typed +0 -0
  119. tnfr/rng.py +46 -143
  120. tnfr/rng.pyi +14 -0
  121. tnfr/schemas/__init__.py +8 -0
  122. tnfr/schemas/grammar.json +94 -0
  123. tnfr/selector.py +25 -19
  124. tnfr/selector.pyi +19 -0
  125. tnfr/sense.py +72 -62
  126. tnfr/sense.pyi +23 -0
  127. tnfr/structural.py +522 -262
  128. tnfr/structural.pyi +69 -0
  129. tnfr/telemetry/__init__.py +35 -0
  130. tnfr/telemetry/cache_metrics.py +226 -0
  131. tnfr/telemetry/nu_f.py +423 -0
  132. tnfr/telemetry/nu_f.pyi +123 -0
  133. tnfr/telemetry/verbosity.py +37 -0
  134. tnfr/tokens.py +1 -3
  135. tnfr/tokens.pyi +36 -0
  136. tnfr/trace.py +270 -113
  137. tnfr/trace.pyi +40 -0
  138. tnfr/types.py +574 -6
  139. tnfr/types.pyi +331 -0
  140. tnfr/units.py +69 -0
  141. tnfr/units.pyi +16 -0
  142. tnfr/utils/__init__.py +217 -0
  143. tnfr/utils/__init__.pyi +202 -0
  144. tnfr/utils/cache.py +2395 -0
  145. tnfr/utils/cache.pyi +468 -0
  146. tnfr/utils/chunks.py +104 -0
  147. tnfr/utils/chunks.pyi +21 -0
  148. tnfr/{collections_utils.py → utils/data.py} +147 -90
  149. tnfr/utils/data.pyi +64 -0
  150. tnfr/utils/graph.py +85 -0
  151. tnfr/utils/graph.pyi +10 -0
  152. tnfr/utils/init.py +770 -0
  153. tnfr/utils/init.pyi +78 -0
  154. tnfr/utils/io.py +456 -0
  155. tnfr/{helpers → utils}/numeric.py +51 -24
  156. tnfr/utils/numeric.pyi +21 -0
  157. tnfr/validation/__init__.py +113 -0
  158. tnfr/validation/__init__.pyi +77 -0
  159. tnfr/validation/compatibility.py +95 -0
  160. tnfr/validation/compatibility.pyi +6 -0
  161. tnfr/validation/grammar.py +71 -0
  162. tnfr/validation/grammar.pyi +40 -0
  163. tnfr/validation/graph.py +138 -0
  164. tnfr/validation/graph.pyi +17 -0
  165. tnfr/validation/rules.py +281 -0
  166. tnfr/validation/rules.pyi +55 -0
  167. tnfr/validation/runtime.py +263 -0
  168. tnfr/validation/runtime.pyi +31 -0
  169. tnfr/validation/soft_filters.py +170 -0
  170. tnfr/validation/soft_filters.pyi +37 -0
  171. tnfr/validation/spectral.py +159 -0
  172. tnfr/validation/spectral.pyi +46 -0
  173. tnfr/validation/syntax.py +40 -0
  174. tnfr/validation/syntax.pyi +10 -0
  175. tnfr/validation/window.py +39 -0
  176. tnfr/validation/window.pyi +1 -0
  177. tnfr/viz/__init__.py +9 -0
  178. tnfr/viz/matplotlib.py +246 -0
  179. tnfr-7.0.0.dist-info/METADATA +179 -0
  180. tnfr-7.0.0.dist-info/RECORD +185 -0
  181. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  182. tnfr/grammar.py +0 -344
  183. tnfr/graph_utils.py +0 -84
  184. tnfr/helpers/__init__.py +0 -71
  185. tnfr/import_utils.py +0 -228
  186. tnfr/json_utils.py +0 -162
  187. tnfr/logging_utils.py +0 -116
  188. tnfr/presets.py +0 -60
  189. tnfr/validators.py +0 -84
  190. tnfr/value_utils.py +0 -59
  191. tnfr-4.5.2.dist-info/METADATA +0 -379
  192. tnfr-4.5.2.dist-info/RECORD +0 -67
  193. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  194. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  195. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
@@ -1,39 +1,79 @@
1
1
  """Network operators."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Any, TYPE_CHECKING, Callable
5
- import math
4
+
6
5
  import heapq
6
+ import math
7
+ from collections.abc import Callable, Iterator
7
8
  from itertools import islice
8
- from statistics import fmean, StatisticsError
9
+ from statistics import StatisticsError, fmean
10
+ from typing import TYPE_CHECKING, Any
9
11
 
10
- from ..alias import get_attr
11
- from ..constants import DEFAULTS, get_aliases, get_param
12
+ from tnfr import glyph_history
12
13
 
13
- 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
14
18
  from ..metrics.trig import neighbor_phase_mean
15
- from ..import_utils import get_nodonx
16
19
  from ..rng import make_rng
17
- from tnfr import glyph_history
18
- from ..types import Glyph
19
-
20
+ from ..types import EPIValue, Glyph, NodeId, TNFRGraph
21
+ from ..utils import get_nodenx
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
+ )
20
39
  from .jitter import (
21
40
  JitterCache,
22
41
  JitterCacheManager,
23
42
  get_jitter_manager,
24
- reset_jitter_manager,
25
43
  random_jitter,
44
+ reset_jitter_manager,
26
45
  )
46
+ from .registry import OPERATORS, discover_operators, get_operator_class
27
47
  from .remesh import (
28
48
  apply_network_remesh,
29
- apply_topological_remesh,
30
49
  apply_remesh_if_globally_stable,
50
+ apply_topological_remesh,
51
+ )
52
+
53
+ _remesh_doc = (
54
+ "Trigger a remesh once the stability window is satisfied.\n\n"
55
+ "Parameters\n----------\n"
56
+ "stable_step_window : int | None\n"
57
+ " Number of consecutive stable steps required before remeshing.\n"
58
+ " Only the English keyword 'stable_step_window' is supported."
31
59
  )
60
+ if apply_remesh_if_globally_stable.__doc__:
61
+ apply_remesh_if_globally_stable.__doc__ += "\n\n" + _remesh_doc
62
+ else:
63
+ apply_remesh_if_globally_stable.__doc__ = _remesh_doc
64
+
65
+ discover_operators()
66
+
67
+ _DEFINITION_EXPORTS = {
68
+ name: getattr(_definitions, name) for name in getattr(_definitions, "__all__", ())
69
+ }
70
+ globals().update(_DEFINITION_EXPORTS)
32
71
 
33
72
  if TYPE_CHECKING: # pragma: no cover - type checking only
34
- from ..node import NodoProtocol
73
+ from ..node import NodeProtocol
35
74
 
36
- ALIAS_EPI = get_aliases("EPI")
75
+ GlyphFactors = dict[str, Any]
76
+ GlyphOperation = Callable[["NodeProtocol", GlyphFactors], None]
37
77
 
38
78
  __all__ = [
39
79
  "JitterCache",
@@ -41,6 +81,20 @@ __all__ = [
41
81
  "get_jitter_manager",
42
82
  "reset_jitter_manager",
43
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",
44
98
  "get_neighbor_epi",
45
99
  "get_glyph_factors",
46
100
  "GLYPH_OPERATIONS",
@@ -49,26 +103,120 @@ __all__ = [
49
103
  "apply_network_remesh",
50
104
  "apply_topological_remesh",
51
105
  "apply_remesh_if_globally_stable",
106
+ "OPERATORS",
107
+ "discover_operators",
108
+ "get_operator_class",
52
109
  ]
53
110
 
54
-
55
- def get_glyph_factors(node: NodoProtocol) -> dict[str, Any]:
56
- """Return glyph factors for ``node`` with defaults."""
111
+ __all__.extend(_DEFINITION_EXPORTS.keys())
112
+
113
+
114
+ def get_glyph_factors(node: NodeProtocol) -> GlyphFactors:
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
+ """
57
147
  return node.graph.get("GLYPH_FACTORS", DEFAULTS["GLYPH_FACTORS"].copy())
58
148
 
59
149
 
60
- def get_factor(gf: dict[str, Any], key: str, default: float) -> float:
61
- """Return ``gf[key]`` as ``float`` with ``default`` fallback."""
150
+ def get_factor(gf: GlyphFactors, key: str, default: float) -> float:
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
+ """
62
175
  return float(gf.get(key, default))
63
176
 
64
177
 
65
178
  # -------------------------
66
- # Glyphs (operadores locales)
179
+ # Glyphs (local operators)
67
180
  # -------------------------
68
181
 
69
182
 
70
- def get_neighbor_epi(node: NodoProtocol) -> tuple[list[NodoProtocol], float]:
71
- """Return neighbour list and their mean ``EPI`` without mutating ``node``."""
183
+ def get_neighbor_epi(node: NodeProtocol) -> tuple[list[NodeProtocol], EPIValue]:
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
+ """
72
220
 
73
221
  epi = node.EPI
74
222
  neigh = list(node.neighbors())
@@ -98,12 +246,11 @@ def get_neighbor_epi(node: NodoProtocol) -> tuple[list[NodoProtocol], float]:
98
246
  return [], epi
99
247
  epi_bar = total / count if count else float(epi)
100
248
  if needs_conversion:
101
- NodoNX = get_nodonx()
102
- if NodoNX is None:
103
- raise ImportError("NodoNX is unavailable")
249
+ NodeNX = get_nodenx()
250
+ if NodeNX is None:
251
+ raise ImportError("NodeNX is unavailable")
104
252
  neigh = [
105
- v if hasattr(v, "EPI") else NodoNX.from_graph(node.G, v)
106
- for v in neigh
253
+ v if hasattr(v, "EPI") else NodeNX.from_graph(node.G, v) for v in neigh
107
254
  ]
108
255
  else:
109
256
  try:
@@ -115,9 +262,37 @@ def get_neighbor_epi(node: NodoProtocol) -> tuple[list[NodoProtocol], float]:
115
262
 
116
263
 
117
264
  def _determine_dominant(
118
- neigh: list[NodoProtocol], default_kind: str
265
+ neigh: list[NodeProtocol], default_kind: str
119
266
  ) -> tuple[str, float]:
120
- """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
+ """
121
296
  best_kind: str | None = None
122
297
  best_abs = 0.0
123
298
  for v in neigh:
@@ -131,13 +306,48 @@ def _determine_dominant(
131
306
 
132
307
 
133
308
  def _mix_epi_with_neighbors(
134
- node: NodoProtocol, mix: float, default_glyph: Glyph | str
309
+ node: NodeProtocol, mix: float, default_glyph: Glyph | str
135
310
  ) -> tuple[float, str]:
136
- """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
+ """
137
349
  default_kind = (
138
- default_glyph.value
139
- if isinstance(default_glyph, Glyph)
140
- else str(default_glyph)
350
+ default_glyph.value if isinstance(default_glyph, Glyph) else str(default_glyph)
141
351
  )
142
352
  epi = node.EPI
143
353
  neigh, epi_bar = get_neighbor_epi(node)
@@ -156,22 +366,120 @@ def _mix_epi_with_neighbors(
156
366
  return epi_bar, final
157
367
 
158
368
 
159
- def _op_AL(node: NodoProtocol, gf: dict[str, Any]) -> None: # AL — Emisión
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
+ """
160
393
  f = get_factor(gf, "AL_boost", 0.05)
161
394
  node.EPI = node.EPI + f
162
395
 
163
396
 
164
- def _op_EN(node: NodoProtocol, gf: dict[str, Any]) -> None: # EN — Recepción
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
+ """
165
426
  mix = get_factor(gf, "EN_mix", 0.25)
166
427
  _mix_epi_with_neighbors(node, mix, Glyph.EN)
167
428
 
168
429
 
169
- def _op_IL(node: NodoProtocol, gf: dict[str, Any]) -> None: # IL — Coherencia
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
+ """
170
454
  factor = get_factor(gf, "IL_dnfr_factor", 0.7)
171
455
  node.dnfr = factor * getattr(node, "dnfr", 0.0)
172
456
 
173
457
 
174
- def _op_OZ(node: NodoProtocol, gf: dict[str, Any]) -> None: # OZ — Disonancia
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
+ """
175
483
  factor = get_factor(gf, "OZ_dnfr_factor", 1.3)
176
484
  dnfr = getattr(node, "dnfr", 0.0)
177
485
  if bool(node.graph.get("OZ_NOISE_MODE", False)):
@@ -184,31 +492,29 @@ def _op_OZ(node: NodoProtocol, gf: dict[str, Any]) -> None: # OZ — Disonancia
184
492
  node.dnfr = factor * dnfr if abs(dnfr) > 1e-9 else 0.1
185
493
 
186
494
 
187
- def _um_candidate_iter(node: NodoProtocol):
495
+ def _um_candidate_iter(node: NodeProtocol) -> Iterator[NodeProtocol]:
188
496
  sample_ids = node.graph.get("_node_sample")
189
497
  if sample_ids is not None and hasattr(node, "G"):
190
- NodoNX = get_nodonx()
191
- if NodoNX is None:
192
- raise ImportError("NodoNX is unavailable")
193
- base = (NodoNX.from_graph(node.G, j) for j in sample_ids)
498
+ NodeNX = get_nodenx()
499
+ if NodeNX is None:
500
+ raise ImportError("NodeNX is unavailable")
501
+ base = (NodeNX.from_graph(node.G, j) for j in sample_ids)
194
502
  else:
195
503
  base = node.all_nodes()
196
504
  for j in base:
197
- same = (j is node) or (
198
- getattr(node, "n", None) == getattr(j, "n", None)
199
- )
505
+ same = (j is node) or (getattr(node, "n", None) == getattr(j, "n", None))
200
506
  if same or node.has_edge(j):
201
507
  continue
202
508
  yield j
203
509
 
204
510
 
205
511
  def _um_select_candidates(
206
- node: NodoProtocol,
207
- candidates,
512
+ node: NodeProtocol,
513
+ candidates: Iterator[NodeProtocol],
208
514
  limit: int,
209
515
  mode: str,
210
516
  th: float,
211
- ):
517
+ ) -> list[NodeProtocol]:
212
518
  """Select a subset of ``candidates`` for UM coupling."""
213
519
  rng = make_rng(int(node.graph.get("RANDOM_SEED", 0)), node.offset(), node.G)
214
520
 
@@ -232,7 +538,46 @@ def _um_select_candidates(
232
538
  return reservoir
233
539
 
234
540
 
235
- def _op_UM(node: NodoProtocol, gf: dict[str, Any]) -> None: # UM — Coupling
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
+ """
236
581
  k = get_factor(gf, "UM_theta_push", 0.25)
237
582
  th = node.theta
238
583
  thL = neighbor_phase_mean(node)
@@ -260,21 +605,71 @@ def _op_UM(node: NodoProtocol, gf: dict[str, Any]) -> None: # UM — Coupling
260
605
  dphi = abs(angle_diff(th_j, th)) / math.pi
261
606
  epi_j = j.EPI
262
607
  si_j = j.Si
263
- epi_sim = 1.0 - abs(epi_i - epi_j) / (
264
- abs(epi_i) + abs(epi_j) + 1e-9
265
- )
608
+ epi_sim = 1.0 - abs(epi_i - epi_j) / (abs(epi_i) + abs(epi_j) + 1e-9)
266
609
  si_sim = 1.0 - abs(si_i - si_j)
267
610
  compat = (1 - dphi) * 0.5 + 0.25 * epi_sim + 0.25 * si_sim
268
611
  if compat >= thr:
269
612
  node.add_edge(j, compat)
270
613
 
271
614
 
272
- def _op_RA(node: NodoProtocol, gf: dict[str, Any]) -> None: # RA — Resonancia
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
+ """
273
645
  diff = get_factor(gf, "RA_epi_diff", 0.15)
274
646
  _mix_epi_with_neighbors(node, diff, Glyph.RA)
275
647
 
276
648
 
277
- def _op_SHA(node: NodoProtocol, gf: dict[str, Any]) -> None: # SHA — Silencio
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
+ """
278
673
  factor = get_factor(gf, "SHA_vf_factor", 0.85)
279
674
  node.vf = factor * node.vf
280
675
 
@@ -284,37 +679,138 @@ factor_nul = 0.85
284
679
  _SCALE_FACTORS = {Glyph.VAL: factor_val, Glyph.NUL: factor_nul}
285
680
 
286
681
 
287
- def _op_scale(node: NodoProtocol, factor: float) -> None:
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
+ """
288
692
  node.vf *= factor
289
693
 
290
694
 
291
- def _make_scale_op(glyph: Glyph):
292
- def _op(node: NodoProtocol, gf: dict[str, Any]) -> None:
695
+ def _make_scale_op(glyph: Glyph) -> GlyphOperation:
696
+ def _op(node: NodeProtocol, gf: GlyphFactors) -> None:
293
697
  key = "VAL_scale" if glyph is Glyph.VAL else "NUL_scale"
294
698
  default = _SCALE_FACTORS[glyph]
295
699
  factor = get_factor(gf, key, default)
296
700
  _op_scale(node, factor)
297
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
+ )
298
729
  return _op
299
730
 
300
731
 
301
- def _op_THOL(
302
- node: NodoProtocol, gf: dict[str, Any]
303
- ) -> None: # THOL — Autoorganización
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
+ """
304
756
  a = get_factor(gf, "THOL_accel", 0.10)
305
757
  node.dnfr = node.dnfr + a * getattr(node, "d2EPI", 0.0)
306
758
 
307
759
 
308
- def _op_ZHIR(
309
- node: NodoProtocol, gf: dict[str, Any]
310
- ) -> None: # ZHIR — Mutación
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
+ """
311
784
  shift = get_factor(gf, "ZHIR_theta_shift", math.pi / 2)
312
785
  node.theta = node.theta + shift
313
786
 
314
787
 
315
- def _op_NAV(
316
- node: NodoProtocol, gf: dict[str, Any]
317
- ) -> None: # NAV — Transición
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
+ """
318
814
  dnfr = node.dnfr
319
815
  vf = node.vf
320
816
  eta = get_factor(gf, "NAV_eta", 0.5)
@@ -334,14 +830,37 @@ def _op_NAV(
334
830
 
335
831
 
336
832
  def _op_REMESH(
337
- node: NodoProtocol, gf: dict[str, Any] | None = None
338
- ) -> None: # REMESH — aviso
833
+ node: NodeProtocol, gf: GlyphFactors | None = None
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
+ """
339
858
  step_idx = glyph_history.current_step_idx(node)
340
859
  last_warn = node.graph.get("_remesh_warn_step", None)
341
860
  if last_warn != step_idx:
342
861
  msg = (
343
- "REMESH es a escala de red. Usa apply_remesh_if_globally_"
344
- "stable(G) o apply_network_remesh(G)."
862
+ "REMESH operates at network scale. Use apply_remesh_if_globally_"
863
+ "stable(G) or apply_network_remesh(G)."
345
864
  )
346
865
  hist = glyph_history.ensure_history(node)
347
866
  glyph_history.append_metric(
@@ -357,7 +876,7 @@ def _op_REMESH(
357
876
  # Dispatcher
358
877
  # -------------------------
359
878
 
360
- GLYPH_OPERATIONS: dict[Glyph, Callable[["NodoProtocol", dict[str, Any]], None]] = {
879
+ GLYPH_OPERATIONS: dict[Glyph, GlyphOperation] = {
361
880
  Glyph.AL: _op_AL,
362
881
  Glyph.EN: _op_EN,
363
882
  Glyph.IL: _op_IL,
@@ -375,9 +894,9 @@ GLYPH_OPERATIONS: dict[Glyph, Callable[["NodoProtocol", dict[str, Any]], None]]
375
894
 
376
895
 
377
896
  def apply_glyph_obj(
378
- node: NodoProtocol, glyph: Glyph | str, *, window: int | None = None
897
+ node: NodeProtocol, glyph: Glyph | str, *, window: int | None = None
379
898
  ) -> None:
380
- """Apply ``glyph`` to an object satisfying :class:`NodoProtocol`."""
899
+ """Apply ``glyph`` to an object satisfying :class:`NodeProtocol`."""
381
900
 
382
901
  try:
383
902
  g = glyph if isinstance(glyph, Glyph) else Glyph(str(glyph))
@@ -392,15 +911,15 @@ def apply_glyph_obj(
392
911
  {
393
912
  "step": step_idx,
394
913
  "node": getattr(node, "n", None),
395
- "msg": f"glyph desconocido: {glyph}",
914
+ "msg": f"unknown glyph: {glyph}",
396
915
  },
397
916
  ),
398
917
  )
399
- raise ValueError(f"glyph desconocido: {glyph}")
918
+ raise ValueError(f"unknown glyph: {glyph}")
400
919
 
401
920
  op = GLYPH_OPERATIONS.get(g)
402
921
  if op is None:
403
- raise ValueError(f"glyph sin operador: {g}")
922
+ raise ValueError(f"glyph has no registered operator: {g}")
404
923
  if window is None:
405
924
  window = int(get_param(node, "GLYPH_HYSTERESIS_WINDOW"))
406
925
  gf = get_glyph_factors(node)
@@ -410,11 +929,11 @@ def apply_glyph_obj(
410
929
 
411
930
 
412
931
  def apply_glyph(
413
- G, n, glyph: Glyph | str, *, window: int | None = None
932
+ G: TNFRGraph, n: NodeId, glyph: Glyph | str, *, window: int | None = None
414
933
  ) -> None:
415
934
  """Adapter to operate on ``networkx`` graphs."""
416
- NodoNX = get_nodonx()
417
- if NodoNX is None:
418
- raise ImportError("NodoNX is unavailable")
419
- node = NodoNX(G, n)
935
+ NodeNX = get_nodenx()
936
+ if NodeNX is None:
937
+ raise ImportError("NodeNX is unavailable")
938
+ node = NodeNX(G, n)
420
939
  apply_glyph_obj(node, glyph, window=window)