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
tnfr/operators/jitter.py CHANGED
@@ -1,24 +1,31 @@
1
+ """Jitter operators for reproducible phase perturbations."""
2
+
1
3
  from __future__ import annotations
2
- from typing import Any, TYPE_CHECKING
3
4
 
4
- from cachetools import LRUCache
5
+ import threading
6
+ from typing import TYPE_CHECKING, Any, cast
5
7
 
6
- from ..cache import ensure_node_offset_map
8
+ from ..rng import base_seed, cache_enabled
9
+ from ..rng import clear_rng_cache as _clear_rng_cache
7
10
  from ..rng import (
8
- ScopedCounterCache,
9
11
  make_rng,
10
- base_seed,
11
- cache_enabled,
12
- clear_rng_cache as _clear_rng_cache,
13
12
  seed_hash,
14
13
  )
15
- from ..import_utils import get_nodonx
14
+ from ..types import NodeId, TNFRGraph
15
+ from ..utils import (
16
+ CacheManager,
17
+ InstrumentedLRUCache,
18
+ ScopedCounterCache,
19
+ build_cache_manager,
20
+ ensure_node_offset_map,
21
+ get_nodenx,
22
+ )
16
23
 
17
24
  if TYPE_CHECKING: # pragma: no cover - type checking only
18
- from ..node import NodoProtocol
25
+ from ..node import NodeProtocol
19
26
 
20
27
  # Guarded by the cache lock to ensure thread-safe access. ``seq`` stores
21
- # per-scope jitter sequence counters in an LRU cache bounded to avoid
28
+ # per-scope jitter sequence counters in an instrumented LRU cache bounded to avoid
22
29
  # unbounded memory usage.
23
30
  _JITTER_MAX_ENTRIES = 1024
24
31
 
@@ -26,18 +33,53 @@ _JITTER_MAX_ENTRIES = 1024
26
33
  class JitterCache:
27
34
  """Container for jitter-related caches."""
28
35
 
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}
36
+ def __init__(
37
+ self,
38
+ max_entries: int = _JITTER_MAX_ENTRIES,
39
+ *,
40
+ manager: CacheManager | None = None,
41
+ ) -> None:
42
+ self._manager = manager or build_cache_manager()
43
+ if not self._manager.has_override("scoped_counter:jitter"):
44
+ self._manager.configure(
45
+ overrides={"scoped_counter:jitter": int(max_entries)}
46
+ )
47
+ self._sequence = ScopedCounterCache(
48
+ "jitter",
49
+ max_entries=None,
50
+ manager=self._manager,
51
+ default_max_entries=int(max_entries),
52
+ )
53
+ self._settings_key = "jitter_settings"
54
+ self._manager.register(
55
+ self._settings_key,
56
+ lambda: {"max_entries": self._sequence.max_entries},
57
+ reset=self._reset_settings,
58
+ )
59
+
60
+ def _reset_settings(self, settings: dict[str, Any] | None) -> dict[str, Any]:
61
+ return {"max_entries": self._sequence.max_entries}
62
+
63
+ def _refresh_settings(self) -> None:
64
+ self._manager.update(
65
+ self._settings_key,
66
+ lambda _: {"max_entries": self._sequence.max_entries},
67
+ )
68
+
69
+ @property
70
+ def manager(self) -> CacheManager:
71
+ """Expose the cache manager backing this cache."""
72
+
73
+ return self._manager
32
74
 
33
75
  @property
34
- def seq(self) -> LRUCache[tuple[int, int], int]:
35
- """Expose the sequence cache for tests and diagnostics."""
76
+ def seq(self) -> InstrumentedLRUCache[tuple[int, int], int]:
77
+ """Expose the instrumented sequence cache for tests and diagnostics."""
36
78
 
37
79
  return self._sequence.cache
38
80
 
39
81
  @property
40
- def lock(self):
82
+ def lock(self) -> threading.Lock | threading.RLock:
41
83
  """Return the lock protecting the sequence cache."""
42
84
 
43
85
  return self._sequence.lock
@@ -53,21 +95,26 @@ class JitterCache:
53
95
  """Set the maximum number of cached jitter sequences."""
54
96
 
55
97
  self._sequence.configure(max_entries=int(value))
56
- self.settings["max_entries"] = self._sequence.max_entries
98
+ self._refresh_settings()
57
99
 
58
- def setup(
59
- self, force: bool = False, max_entries: int | None = None
60
- ) -> None:
100
+ @property
101
+ def settings(self) -> dict[str, Any]:
102
+ """Return jitter cache settings stored on the manager."""
103
+
104
+ return cast(dict[str, Any], self._manager.get(self._settings_key))
105
+
106
+ def setup(self, force: bool = False, max_entries: int | None = None) -> None:
61
107
  """Ensure jitter cache matches the configured size."""
62
108
 
63
109
  self._sequence.configure(force=force, max_entries=max_entries)
64
- self.settings["max_entries"] = self._sequence.max_entries
110
+ self._refresh_settings()
65
111
 
66
112
  def clear(self) -> None:
67
113
  """Clear cached RNGs and jitter state."""
68
114
 
69
115
  _clear_rng_cache()
70
116
  self._sequence.clear()
117
+ self._manager.clear(self._settings_key)
71
118
 
72
119
  def bump(self, key: tuple[int, int]) -> int:
73
120
  """Return current jitter sequence counter for ``key`` and increment it."""
@@ -78,40 +125,57 @@ class JitterCache:
78
125
  class JitterCacheManager:
79
126
  """Manager exposing the jitter cache without global reassignment."""
80
127
 
81
- def __init__(self, cache: JitterCache | None = None) -> None:
82
- self.cache = cache or JitterCache()
128
+ def __init__(
129
+ self,
130
+ cache: JitterCache | None = None,
131
+ *,
132
+ manager: CacheManager | None = None,
133
+ ) -> None:
134
+ if cache is not None:
135
+ self.cache = cache
136
+ self._manager = cache.manager
137
+ else:
138
+ self._manager = manager or build_cache_manager()
139
+ self.cache = JitterCache(manager=self._manager)
83
140
 
84
141
  # Convenience passthrough properties
85
142
  @property
86
- def seq(self) -> LRUCache[tuple[int, int], int]:
143
+ def seq(self) -> InstrumentedLRUCache[tuple[int, int], int]:
144
+ """Expose the underlying instrumented jitter sequence cache."""
145
+
87
146
  return self.cache.seq
88
147
 
89
148
  @property
90
149
  def settings(self) -> dict[str, Any]:
150
+ """Return persisted jitter cache configuration."""
151
+
91
152
  return self.cache.settings
92
153
 
93
154
  @property
94
- def lock(self):
155
+ def lock(self) -> threading.Lock | threading.RLock:
156
+ """Return the lock associated with the jitter cache."""
157
+
95
158
  return self.cache.lock
96
159
 
97
160
  @property
98
161
  def max_entries(self) -> int:
99
162
  """Return the maximum number of cached jitter entries."""
163
+
100
164
  return self.cache.max_entries
101
165
 
102
166
  @max_entries.setter
103
167
  def max_entries(self, value: int) -> None:
104
168
  """Set the maximum number of cached jitter entries."""
169
+
105
170
  self.cache.max_entries = value
106
171
 
107
- def setup(
108
- self, force: bool = False, max_entries: int | None = None
109
- ) -> None:
172
+ def setup(self, force: bool = False, max_entries: int | None = None) -> None:
110
173
  """Ensure jitter cache matches the configured size.
111
174
 
112
175
  ``max_entries`` may be provided to explicitly resize the cache.
113
176
  When omitted the existing ``cache.max_entries`` is preserved.
114
177
  """
178
+
115
179
  if max_entries is not None:
116
180
  self.cache.setup(force=True, max_entries=max_entries)
117
181
  else:
@@ -119,6 +183,7 @@ class JitterCacheManager:
119
183
 
120
184
  def clear(self) -> None:
121
185
  """Clear cached RNGs and jitter state."""
186
+
122
187
  self.cache.clear()
123
188
 
124
189
  def bump(self, key: tuple[int, int]) -> int:
@@ -148,27 +213,31 @@ def reset_jitter_manager() -> None:
148
213
  _JITTER_MANAGER = None
149
214
 
150
215
 
151
- def _node_offset(G, n) -> int:
216
+ def _node_offset(G: TNFRGraph, n: NodeId) -> int:
152
217
  """Deterministic node index used for jitter seeds."""
153
218
  mapping = ensure_node_offset_map(G)
154
219
  return int(mapping.get(n, 0))
155
220
 
156
221
 
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)
222
+ def _resolve_jitter_seed(node: NodeProtocol) -> tuple[int, int]:
223
+ node_nx_type = get_nodenx()
224
+ if node_nx_type is None:
225
+ raise ImportError("NodeNX is unavailable")
226
+ if isinstance(node, node_nx_type):
227
+ graph = cast(TNFRGraph, getattr(node, "G"))
228
+ node_id = cast(NodeId, getattr(node, "n"))
229
+ return _node_offset(graph, node_id), id(graph)
163
230
  uid = getattr(node, "_noise_uid", None)
164
231
  if uid is None:
165
232
  uid = id(node)
166
233
  setattr(node, "_noise_uid", uid)
167
- return int(uid), id(node)
234
+ graph = cast(TNFRGraph | None, getattr(node, "G", None))
235
+ scope = graph if graph is not None else node
236
+ return int(uid), id(scope)
168
237
 
169
238
 
170
239
  def random_jitter(
171
- node: NodoProtocol,
240
+ node: NodeProtocol,
172
241
  amplitude: float,
173
242
  ) -> float:
174
243
  """Return deterministic noise in ``[-amplitude, amplitude]`` for ``node``.
@@ -184,7 +253,7 @@ def random_jitter(
184
253
  seed_root = base_seed(node.G)
185
254
  seed_key, scope_id = _resolve_jitter_seed(node)
186
255
 
187
- cache_key = (seed_root, scope_id)
256
+ cache_key = (seed_root, scope_id, seed_key)
188
257
  seq = 0
189
258
  if cache_enabled(node.G):
190
259
  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,75 @@
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 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__ = (
71
+ "OPERATORS",
72
+ "register_operator",
73
+ "discover_operators",
74
+ "get_operator_class",
75
+ )
@@ -0,0 +1,13 @@
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
+ def register_operator(cls: type[Operator]) -> type[Operator]: ...
13
+ def get_operator_class(name: str) -> type[Operator]: ...