tnfr 4.5.2__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 (161) hide show
  1. tnfr/__init__.py +228 -49
  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 +106 -21
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +666 -512
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +2 -9
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +21 -7
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +42 -20
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +54 -20
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +0 -2
  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.py → config/init.py} +11 -7
  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 +78 -24
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +1 -2
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.pyi +12 -0
  35. tnfr/constants/metric.py +4 -12
  36. tnfr/constants/metric.pyi +19 -0
  37. tnfr/constants_glyphs.py +9 -91
  38. tnfr/constants_glyphs.pyi +12 -0
  39. tnfr/dynamics/__init__.py +112 -634
  40. tnfr/dynamics/__init__.pyi +83 -0
  41. tnfr/dynamics/adaptation.py +201 -0
  42. tnfr/dynamics/aliases.py +22 -0
  43. tnfr/dynamics/coordination.py +343 -0
  44. tnfr/dynamics/dnfr.py +1936 -354
  45. tnfr/dynamics/dnfr.pyi +33 -0
  46. tnfr/dynamics/integrators.py +369 -75
  47. tnfr/dynamics/integrators.pyi +35 -0
  48. tnfr/dynamics/runtime.py +521 -0
  49. tnfr/dynamics/sampling.py +8 -5
  50. tnfr/dynamics/sampling.pyi +7 -0
  51. tnfr/dynamics/selectors.py +680 -0
  52. tnfr/execution.py +56 -41
  53. tnfr/execution.pyi +65 -0
  54. tnfr/flatten.py +7 -7
  55. tnfr/flatten.pyi +28 -0
  56. tnfr/gamma.py +54 -37
  57. tnfr/gamma.pyi +40 -0
  58. tnfr/glyph_history.py +85 -38
  59. tnfr/glyph_history.pyi +53 -0
  60. tnfr/grammar.py +19 -338
  61. tnfr/grammar.pyi +13 -0
  62. tnfr/helpers/__init__.py +110 -30
  63. tnfr/helpers/__init__.pyi +66 -0
  64. tnfr/helpers/numeric.py +1 -0
  65. tnfr/helpers/numeric.pyi +12 -0
  66. tnfr/immutable.py +55 -19
  67. tnfr/immutable.pyi +37 -0
  68. tnfr/initialization.py +12 -10
  69. tnfr/initialization.pyi +73 -0
  70. tnfr/io.py +99 -34
  71. tnfr/io.pyi +11 -0
  72. tnfr/locking.pyi +7 -0
  73. tnfr/metrics/__init__.pyi +20 -0
  74. tnfr/metrics/coherence.py +934 -294
  75. tnfr/metrics/common.py +1 -3
  76. tnfr/metrics/common.pyi +15 -0
  77. tnfr/metrics/core.py +192 -34
  78. tnfr/metrics/core.pyi +13 -0
  79. tnfr/metrics/diagnosis.py +707 -101
  80. tnfr/metrics/diagnosis.pyi +89 -0
  81. tnfr/metrics/export.py +27 -13
  82. tnfr/metrics/glyph_timing.py +218 -38
  83. tnfr/metrics/reporting.py +22 -18
  84. tnfr/metrics/reporting.pyi +12 -0
  85. tnfr/metrics/sense_index.py +199 -25
  86. tnfr/metrics/sense_index.pyi +9 -0
  87. tnfr/metrics/trig.py +53 -18
  88. tnfr/metrics/trig.pyi +12 -0
  89. tnfr/metrics/trig_cache.py +3 -7
  90. tnfr/metrics/trig_cache.pyi +10 -0
  91. tnfr/node.py +148 -125
  92. tnfr/node.pyi +161 -0
  93. tnfr/observers.py +44 -30
  94. tnfr/observers.pyi +46 -0
  95. tnfr/ontosim.py +14 -13
  96. tnfr/ontosim.pyi +33 -0
  97. tnfr/operators/__init__.py +84 -52
  98. tnfr/operators/__init__.pyi +31 -0
  99. tnfr/operators/definitions.py +181 -0
  100. tnfr/operators/definitions.pyi +92 -0
  101. tnfr/operators/jitter.py +86 -23
  102. tnfr/operators/jitter.pyi +11 -0
  103. tnfr/operators/registry.py +80 -0
  104. tnfr/operators/registry.pyi +15 -0
  105. tnfr/operators/remesh.py +141 -57
  106. tnfr/presets.py +9 -54
  107. tnfr/presets.pyi +7 -0
  108. tnfr/py.typed +0 -0
  109. tnfr/rng.py +259 -73
  110. tnfr/rng.pyi +14 -0
  111. tnfr/selector.py +24 -17
  112. tnfr/selector.pyi +19 -0
  113. tnfr/sense.py +55 -43
  114. tnfr/sense.pyi +30 -0
  115. tnfr/structural.py +44 -267
  116. tnfr/structural.pyi +46 -0
  117. tnfr/telemetry/__init__.py +13 -0
  118. tnfr/telemetry/verbosity.py +37 -0
  119. tnfr/tokens.py +3 -2
  120. tnfr/tokens.pyi +41 -0
  121. tnfr/trace.py +272 -82
  122. tnfr/trace.pyi +68 -0
  123. tnfr/types.py +345 -6
  124. tnfr/types.pyi +145 -0
  125. tnfr/utils/__init__.py +158 -0
  126. tnfr/utils/__init__.pyi +133 -0
  127. tnfr/utils/cache.py +755 -0
  128. tnfr/utils/cache.pyi +156 -0
  129. tnfr/{collections_utils.py → utils/data.py} +57 -90
  130. tnfr/utils/data.pyi +73 -0
  131. tnfr/utils/graph.py +87 -0
  132. tnfr/utils/graph.pyi +10 -0
  133. tnfr/utils/init.py +746 -0
  134. tnfr/utils/init.pyi +85 -0
  135. tnfr/{json_utils.py → utils/io.py} +13 -18
  136. tnfr/utils/io.pyi +10 -0
  137. tnfr/utils/validators.py +130 -0
  138. tnfr/utils/validators.pyi +19 -0
  139. tnfr/validation/__init__.py +25 -0
  140. tnfr/validation/__init__.pyi +17 -0
  141. tnfr/validation/compatibility.py +59 -0
  142. tnfr/validation/compatibility.pyi +8 -0
  143. tnfr/validation/grammar.py +149 -0
  144. tnfr/validation/grammar.pyi +11 -0
  145. tnfr/validation/rules.py +194 -0
  146. tnfr/validation/rules.pyi +18 -0
  147. tnfr/validation/syntax.py +151 -0
  148. tnfr/validation/syntax.pyi +7 -0
  149. tnfr-6.0.0.dist-info/METADATA +135 -0
  150. tnfr-6.0.0.dist-info/RECORD +157 -0
  151. tnfr/graph_utils.py +0 -84
  152. tnfr/import_utils.py +0 -228
  153. tnfr/logging_utils.py +0 -116
  154. tnfr/validators.py +0 -84
  155. tnfr/value_utils.py +0 -59
  156. tnfr-4.5.2.dist-info/METADATA +0 -379
  157. tnfr-4.5.2.dist-info/RECORD +0 -67
  158. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  159. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  160. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  161. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -0,0 +1,92 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from ..types import Glyph, TNFRGraph
4
+
5
+ __all__ = (
6
+ "Operator",
7
+ "Emission",
8
+ "Reception",
9
+ "Coherence",
10
+ "Dissonance",
11
+ "Coupling",
12
+ "Resonance",
13
+ "Silence",
14
+ "Expansion",
15
+ "Contraction",
16
+ "SelfOrganization",
17
+ "Mutation",
18
+ "Transition",
19
+ "Recursivity",
20
+ )
21
+
22
+
23
+ class Operator:
24
+ name: ClassVar[str]
25
+ glyph: ClassVar[Glyph | None]
26
+
27
+ def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None: ...
28
+
29
+
30
+ class Emission(Operator):
31
+ name: ClassVar[str]
32
+ glyph: ClassVar[Glyph]
33
+
34
+
35
+ class Reception(Operator):
36
+ name: ClassVar[str]
37
+ glyph: ClassVar[Glyph]
38
+
39
+
40
+ class Coherence(Operator):
41
+ name: ClassVar[str]
42
+ glyph: ClassVar[Glyph]
43
+
44
+
45
+ class Dissonance(Operator):
46
+ name: ClassVar[str]
47
+ glyph: ClassVar[Glyph]
48
+
49
+
50
+ class Coupling(Operator):
51
+ name: ClassVar[str]
52
+ glyph: ClassVar[Glyph]
53
+
54
+
55
+ class Resonance(Operator):
56
+ name: ClassVar[str]
57
+ glyph: ClassVar[Glyph]
58
+
59
+
60
+ class Silence(Operator):
61
+ name: ClassVar[str]
62
+ glyph: ClassVar[Glyph]
63
+
64
+
65
+ class Expansion(Operator):
66
+ name: ClassVar[str]
67
+ glyph: ClassVar[Glyph]
68
+
69
+
70
+ class Contraction(Operator):
71
+ name: ClassVar[str]
72
+ glyph: ClassVar[Glyph]
73
+
74
+
75
+ class SelfOrganization(Operator):
76
+ name: ClassVar[str]
77
+ glyph: ClassVar[Glyph]
78
+
79
+
80
+ class Mutation(Operator):
81
+ name: ClassVar[str]
82
+ glyph: ClassVar[Glyph]
83
+
84
+
85
+ class Transition(Operator):
86
+ name: ClassVar[str]
87
+ glyph: ClassVar[Glyph]
88
+
89
+
90
+ class Recursivity(Operator):
91
+ name: ClassVar[str]
92
+ glyph: ClassVar[Glyph]
tnfr/operators/jitter.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
- from typing import Any, TYPE_CHECKING
2
+ import threading
3
+
4
+ from typing import Any, TYPE_CHECKING, cast
3
5
 
4
6
  from cachetools import LRUCache
5
7
 
6
- from ..cache import ensure_node_offset_map
7
8
  from ..rng import (
8
9
  ScopedCounterCache,
9
10
  make_rng,
@@ -12,10 +13,12 @@ from ..rng import (
12
13
  clear_rng_cache as _clear_rng_cache,
13
14
  seed_hash,
14
15
  )
15
- from ..import_utils import get_nodonx
16
+ from ..cache import CacheManager
17
+ from ..utils import ensure_node_offset_map, get_nodenx
18
+ from ..types import NodeId, TNFRGraph
16
19
 
17
20
  if TYPE_CHECKING: # pragma: no cover - type checking only
18
- from ..node import NodoProtocol
21
+ from ..node import NodeProtocol
19
22
 
20
23
  # Guarded by the cache lock to ensure thread-safe access. ``seq`` stores
21
24
  # per-scope jitter sequence counters in an LRU cache bounded to avoid
@@ -26,9 +29,44 @@ _JITTER_MAX_ENTRIES = 1024
26
29
  class JitterCache:
27
30
  """Container for jitter-related caches."""
28
31
 
29
- def __init__(self, max_entries: int = _JITTER_MAX_ENTRIES) -> None:
30
- self._sequence = ScopedCounterCache("jitter", max_entries)
31
- self.settings: dict[str, Any] = {"max_entries": self._sequence.max_entries}
32
+ def __init__(
33
+ self,
34
+ max_entries: int = _JITTER_MAX_ENTRIES,
35
+ *,
36
+ manager: CacheManager | None = None,
37
+ ) -> None:
38
+ self._manager = manager or CacheManager()
39
+ if not self._manager.has_override("scoped_counter:jitter"):
40
+ self._manager.configure(
41
+ overrides={"scoped_counter:jitter": int(max_entries)}
42
+ )
43
+ self._sequence = ScopedCounterCache(
44
+ "jitter",
45
+ max_entries=None,
46
+ manager=self._manager,
47
+ default_max_entries=int(max_entries),
48
+ )
49
+ self._settings_key = "jitter_settings"
50
+ self._manager.register(
51
+ self._settings_key,
52
+ lambda: {"max_entries": self._sequence.max_entries},
53
+ reset=self._reset_settings,
54
+ )
55
+
56
+ def _reset_settings(self, settings: dict[str, Any] | None) -> dict[str, Any]:
57
+ return {"max_entries": self._sequence.max_entries}
58
+
59
+ def _refresh_settings(self) -> None:
60
+ self._manager.update(
61
+ self._settings_key,
62
+ lambda _: {"max_entries": self._sequence.max_entries},
63
+ )
64
+
65
+ @property
66
+ def manager(self) -> CacheManager:
67
+ """Expose the cache manager backing this cache."""
68
+
69
+ return self._manager
32
70
 
33
71
  @property
34
72
  def seq(self) -> LRUCache[tuple[int, int], int]:
@@ -37,7 +75,7 @@ class JitterCache:
37
75
  return self._sequence.cache
38
76
 
39
77
  @property
40
- def lock(self):
78
+ def lock(self) -> threading.Lock | threading.RLock:
41
79
  """Return the lock protecting the sequence cache."""
42
80
 
43
81
  return self._sequence.lock
@@ -53,7 +91,13 @@ class JitterCache:
53
91
  """Set the maximum number of cached jitter sequences."""
54
92
 
55
93
  self._sequence.configure(max_entries=int(value))
56
- self.settings["max_entries"] = self._sequence.max_entries
94
+ self._refresh_settings()
95
+
96
+ @property
97
+ def settings(self) -> dict[str, Any]:
98
+ """Return jitter cache settings stored on the manager."""
99
+
100
+ return cast(dict[str, Any], self._manager.get(self._settings_key))
57
101
 
58
102
  def setup(
59
103
  self, force: bool = False, max_entries: int | None = None
@@ -61,13 +105,14 @@ class JitterCache:
61
105
  """Ensure jitter cache matches the configured size."""
62
106
 
63
107
  self._sequence.configure(force=force, max_entries=max_entries)
64
- self.settings["max_entries"] = self._sequence.max_entries
108
+ self._refresh_settings()
65
109
 
66
110
  def clear(self) -> None:
67
111
  """Clear cached RNGs and jitter state."""
68
112
 
69
113
  _clear_rng_cache()
70
114
  self._sequence.clear()
115
+ self._manager.clear(self._settings_key)
71
116
 
72
117
  def bump(self, key: tuple[int, int]) -> int:
73
118
  """Return current jitter sequence counter for ``key`` and increment it."""
@@ -78,8 +123,18 @@ class JitterCache:
78
123
  class JitterCacheManager:
79
124
  """Manager exposing the jitter cache without global reassignment."""
80
125
 
81
- def __init__(self, cache: JitterCache | None = None) -> None:
82
- self.cache = cache or JitterCache()
126
+ def __init__(
127
+ self,
128
+ cache: JitterCache | None = None,
129
+ *,
130
+ manager: CacheManager | None = None,
131
+ ) -> None:
132
+ if cache is not None:
133
+ self.cache = cache
134
+ self._manager = cache.manager
135
+ else:
136
+ self._manager = manager or CacheManager()
137
+ self.cache = JitterCache(manager=self._manager)
83
138
 
84
139
  # Convenience passthrough properties
85
140
  @property
@@ -91,17 +146,19 @@ class JitterCacheManager:
91
146
  return self.cache.settings
92
147
 
93
148
  @property
94
- def lock(self):
149
+ def lock(self) -> threading.Lock | threading.RLock:
95
150
  return self.cache.lock
96
151
 
97
152
  @property
98
153
  def max_entries(self) -> int:
99
154
  """Return the maximum number of cached jitter entries."""
155
+
100
156
  return self.cache.max_entries
101
157
 
102
158
  @max_entries.setter
103
159
  def max_entries(self, value: int) -> None:
104
160
  """Set the maximum number of cached jitter entries."""
161
+
105
162
  self.cache.max_entries = value
106
163
 
107
164
  def setup(
@@ -112,6 +169,7 @@ class JitterCacheManager:
112
169
  ``max_entries`` may be provided to explicitly resize the cache.
113
170
  When omitted the existing ``cache.max_entries`` is preserved.
114
171
  """
172
+
115
173
  if max_entries is not None:
116
174
  self.cache.setup(force=True, max_entries=max_entries)
117
175
  else:
@@ -119,6 +177,7 @@ class JitterCacheManager:
119
177
 
120
178
  def clear(self) -> None:
121
179
  """Clear cached RNGs and jitter state."""
180
+
122
181
  self.cache.clear()
123
182
 
124
183
  def bump(self, key: tuple[int, int]) -> int:
@@ -148,27 +207,31 @@ def reset_jitter_manager() -> None:
148
207
  _JITTER_MANAGER = None
149
208
 
150
209
 
151
- def _node_offset(G, n) -> int:
210
+ def _node_offset(G: TNFRGraph, n: NodeId) -> int:
152
211
  """Deterministic node index used for jitter seeds."""
153
212
  mapping = ensure_node_offset_map(G)
154
213
  return int(mapping.get(n, 0))
155
214
 
156
215
 
157
- def _resolve_jitter_seed(node: NodoProtocol) -> tuple[int, int]:
158
- NodoNX = get_nodonx()
159
- if NodoNX is None:
160
- raise ImportError("NodoNX is unavailable")
161
- if isinstance(node, NodoNX):
162
- return _node_offset(node.G, node.n), id(node.G)
216
+ def _resolve_jitter_seed(node: NodeProtocol) -> tuple[int, int]:
217
+ node_nx_type = get_nodenx()
218
+ if node_nx_type is None:
219
+ raise ImportError("NodeNX is unavailable")
220
+ if isinstance(node, node_nx_type):
221
+ graph = cast(TNFRGraph, getattr(node, "G"))
222
+ node_id = cast(NodeId, getattr(node, "n"))
223
+ return _node_offset(graph, node_id), id(graph)
163
224
  uid = getattr(node, "_noise_uid", None)
164
225
  if uid is None:
165
226
  uid = id(node)
166
227
  setattr(node, "_noise_uid", uid)
167
- return int(uid), id(node)
228
+ graph = cast(TNFRGraph | None, getattr(node, "G", None))
229
+ scope = graph if graph is not None else node
230
+ return int(uid), id(scope)
168
231
 
169
232
 
170
233
  def random_jitter(
171
- node: NodoProtocol,
234
+ node: NodeProtocol,
172
235
  amplitude: float,
173
236
  ) -> float:
174
237
  """Return deterministic noise in ``[-amplitude, amplitude]`` for ``node``.
@@ -184,7 +247,7 @@ def random_jitter(
184
247
  seed_root = base_seed(node.G)
185
248
  seed_key, scope_id = _resolve_jitter_seed(node)
186
249
 
187
- cache_key = (seed_root, scope_id)
250
+ cache_key = (seed_root, scope_id, seed_key)
188
251
  seq = 0
189
252
  if cache_enabled(node.G):
190
253
  manager = get_jitter_manager()
@@ -0,0 +1,11 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ JitterCache: Any
8
+ JitterCacheManager: Any
9
+ get_jitter_manager: Any
10
+ random_jitter: Any
11
+ reset_jitter_manager: Any
@@ -0,0 +1,80 @@
1
+ """Registry mapping operator names to their classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import pkgutil
7
+ from typing import Any, TYPE_CHECKING
8
+
9
+ from ..config.operator_names import canonical_operator_name
10
+
11
+ if TYPE_CHECKING: # pragma: no cover - type checking only
12
+ from .definitions import Operator
13
+
14
+
15
+ OPERATORS: dict[str, type["Operator"]] = {}
16
+
17
+
18
+ def register_operator(cls: type["Operator"]) -> type["Operator"]:
19
+ """Register ``cls`` under its declared ``name`` in :data:`OPERATORS`."""
20
+
21
+ name = getattr(cls, "name", None)
22
+ if not isinstance(name, str) or not name:
23
+ raise ValueError(
24
+ f"Operator {cls.__name__} must declare a non-empty 'name' attribute"
25
+ )
26
+
27
+ existing = OPERATORS.get(name)
28
+ if existing is not None and existing is not cls:
29
+ raise ValueError(f"Operator '{name}' is already registered")
30
+
31
+ OPERATORS[name] = cls
32
+ return cls
33
+
34
+
35
+ def get_operator_class(name: str) -> type["Operator"]:
36
+ """Return the operator class registered for ``name`` or its canonical alias."""
37
+
38
+ try:
39
+ return OPERATORS[name]
40
+ except KeyError:
41
+ canonical = canonical_operator_name(name)
42
+ if canonical == name:
43
+ raise
44
+ try:
45
+ return OPERATORS[canonical]
46
+ except KeyError as exc: # pragma: no cover - defensive branch
47
+ raise KeyError(name) from exc
48
+
49
+
50
+ def discover_operators() -> None:
51
+ """Import all operator submodules so their decorators run."""
52
+
53
+ package = importlib.import_module("tnfr.operators")
54
+ package_path = getattr(package, "__path__", None)
55
+ if not package_path:
56
+ return
57
+
58
+ if getattr(package, "_operators_discovered", False): # pragma: no cover - cache
59
+ return
60
+
61
+ prefix = f"{package.__name__}."
62
+ for module_info in pkgutil.walk_packages(package_path, prefix):
63
+ if module_info.name == f"{prefix}registry":
64
+ continue
65
+ importlib.import_module(module_info.name)
66
+
67
+ setattr(package, "_operators_discovered", True)
68
+
69
+
70
+ __all__ = ("OPERATORS", "register_operator", "discover_operators", "get_operator_class")
71
+
72
+
73
+ def __getattr__(name: str) -> Any:
74
+ """Provide guidance for legacy registry aliases."""
75
+
76
+ if name == "OPERADORES":
77
+ raise AttributeError(
78
+ f"module '{__name__}' has no attribute '{name}'; use 'OPERATORS' instead."
79
+ )
80
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@@ -0,0 +1,15 @@
1
+ from typing import Any
2
+
3
+ from .definitions import Operator
4
+
5
+ __all__: Any
6
+
7
+ def __getattr__(name: str) -> Any: ...
8
+
9
+ OPERATORS: dict[str, type[Operator]]
10
+
11
+ def discover_operators() -> None: ...
12
+
13
+ def register_operator(cls: type[Operator]) -> type[Operator]: ...
14
+
15
+ def get_operator_class(name: str) -> type[Operator]: ...