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
tnfr/__init__.py CHANGED
@@ -4,88 +4,267 @@ This package only re-exports a handful of high level helpers. Most
4
4
  functionality lives in submodules that should be imported directly, for
5
5
  example :mod:`tnfr.metrics`, :mod:`tnfr.observers` or the DSL utilities
6
6
  in :mod:`tnfr.tokens`, :mod:`tnfr.flatten` and :mod:`tnfr.execution`.
7
- Recommended entry points are:
8
7
 
9
- - ``step`` and ``run`` in :mod:`tnfr.dynamics`
10
- - ``preparar_red`` in :mod:`tnfr.ontosim`
11
- - ``create_nfr`` and ``run_sequence`` in :mod:`tnfr.structural`
12
- - ``cached_import`` and ``prune_failed_imports`` for optional dependencies
8
+ Exported helpers and their dependencies
9
+ ---------------------------------------
10
+ The :data:`EXPORT_DEPENDENCIES` mapping enumerates which internal
11
+ submodules and third-party packages are required to load each helper.
12
+ The imports are grouped as follows:
13
+
14
+ ``step`` / ``run``
15
+ Provided by :mod:`tnfr.dynamics`. These helpers rely on the
16
+ machinery defined within the :mod:`tnfr.dynamics` package (operator
17
+ orchestration, validation hooks and metrics integration) and require
18
+ the ``networkx`` package for graph handling.
19
+
20
+ ``prepare_network``
21
+ Defined in :mod:`tnfr.ontosim`. Besides :mod:`tnfr.ontosim`
22
+ itself, the helper imports :mod:`tnfr.callback_utils`,
23
+ :mod:`tnfr.constants`, :mod:`tnfr.dynamics`, :mod:`tnfr.glyph_history`,
24
+ :mod:`tnfr.initialization` and :mod:`tnfr.utils` to assemble the
25
+ graph preparation pipeline. It also requires ``networkx`` at import
26
+ time.
27
+
28
+ ``create_nfr`` / ``run_sequence``
29
+ Re-exported from :mod:`tnfr.structural`. They depend on
30
+ :mod:`tnfr.structural`, :mod:`tnfr.constants`, :mod:`tnfr.dynamics`,
31
+ :mod:`tnfr.operators.definitions`, :mod:`tnfr.operators.registry` and
32
+ :mod:`tnfr.validation`, and additionally require the ``networkx``
33
+ package.
34
+
35
+ ``cached_import`` and ``prune_failed_imports`` remain available from
36
+ ``tnfr.utils`` for optional dependency management.
13
37
  """
14
38
 
15
39
  from __future__ import annotations
16
40
 
17
- from .import_utils import cached_import, prune_failed_imports
18
- from .ontosim import preparar_red
41
+ import warnings
42
+ from importlib import import_module, metadata
43
+ from importlib.metadata import PackageNotFoundError
44
+ from typing import Any, Callable, NoReturn
45
+
46
+
47
+ EXPORT_DEPENDENCIES: dict[str, dict[str, tuple[str, ...]]] = {
48
+ "step": {
49
+ "submodules": ("tnfr.dynamics",),
50
+ "third_party": ("networkx",),
51
+ },
52
+ "run": {
53
+ "submodules": ("tnfr.dynamics",),
54
+ "third_party": ("networkx",),
55
+ },
56
+ "prepare_network": {
57
+ "submodules": (
58
+ "tnfr.ontosim",
59
+ "tnfr.callback_utils",
60
+ "tnfr.constants",
61
+ "tnfr.dynamics",
62
+ "tnfr.glyph_history",
63
+ "tnfr.initialization",
64
+ "tnfr.utils",
65
+ ),
66
+ "third_party": ("networkx",),
67
+ },
68
+ "create_nfr": {
69
+ "submodules": (
70
+ "tnfr.structural",
71
+ "tnfr.constants",
72
+ "tnfr.dynamics",
73
+ "tnfr.operators.definitions",
74
+ "tnfr.operators.registry",
75
+ "tnfr.validation",
76
+ ),
77
+ "third_party": ("networkx",),
78
+ },
79
+ "run_sequence": {
80
+ "submodules": (
81
+ "tnfr.structural",
82
+ "tnfr.constants",
83
+ "tnfr.dynamics",
84
+ "tnfr.operators.definitions",
85
+ "tnfr.operators.registry",
86
+ "tnfr.validation",
87
+ ),
88
+ "third_party": ("networkx",),
89
+ },
90
+ }
91
+
92
+
93
+ try: # pragma: no cover - exercised in version resolution tests
94
+ __version__ = metadata.version("tnfr")
95
+ except PackageNotFoundError: # pragma: no cover - fallback tested explicitly
96
+ from ._version import __version__ as _fallback_version
97
+
98
+ __version__ = _fallback_version
19
99
 
20
100
 
21
- def _missing_dependency(name: str, exc: ImportError):
22
- def _stub(*args, **kwargs):
101
+ def _is_internal_import_error(exc: ImportError) -> bool:
102
+ missing_name = getattr(exc, "name", None) or ""
103
+ if missing_name.startswith("tnfr"):
104
+ return True
105
+
106
+ module_name = getattr(exc, "module", None) or ""
107
+ if module_name.startswith("tnfr"):
108
+ return True
109
+
110
+ missing_path = getattr(exc, "path", None) or ""
111
+ if missing_path:
112
+ normalized = missing_path.replace("\\", "/")
113
+ if "/tnfr/" in normalized or normalized.endswith("/tnfr"):
114
+ return True
115
+
116
+ message = str(exc)
117
+ lowered = message.lower()
118
+ mentions_base_package = "module 'tnfr'" in lowered or 'module "tnfr"' in lowered
119
+ if ("tnfr." in message or mentions_base_package) and (
120
+ "circular import" in lowered or "partially initialized module" in lowered
121
+ ):
122
+ return True
123
+
124
+ return False
125
+
126
+
127
+ def _missing_dependency(
128
+ name: str, exc: ImportError, *, module: str | None = None
129
+ ) -> Callable[..., NoReturn]:
130
+ missing_name = getattr(exc, "name", None)
131
+
132
+ def _stub(*args: Any, **kwargs: Any) -> NoReturn:
23
133
  raise ImportError(
24
134
  f"{name} is unavailable because required dependencies could not be imported. "
25
135
  f"Original error ({exc.__class__.__name__}): {exc}. "
26
136
  "Install the missing packages (e.g. 'networkx' or grammar modules)."
27
137
  ) from exc
28
138
 
139
+ _stub.__tnfr_missing_dependency__ = {
140
+ "export": name,
141
+ "module": module,
142
+ "missing": missing_name,
143
+ }
29
144
  return _stub
30
145
 
31
146
 
32
- try: # pragma: no cover - exercised in import tests
33
- from .dynamics import step, run
34
- except ImportError as exc: # pragma: no cover - no missing deps in CI
35
- step = _missing_dependency("step", exc)
36
- run = _missing_dependency("run", exc)
147
+ _MISSING_EXPORTS: dict[str, dict[str, Any]] = {}
148
+
37
149
 
150
+ class ExportDependencyError(RuntimeError):
151
+ """Raised when the export dependency manifest is inconsistent."""
38
152
 
39
- _HAS_RUN_SEQUENCE = False
40
- try: # pragma: no cover - exercised in import tests
41
- from .structural import create_nfr, run_sequence
42
- except ImportError as exc: # pragma: no cover - no missing deps in CI
43
- create_nfr = _missing_dependency("create_nfr", exc)
44
- run_sequence = _missing_dependency("run_sequence", exc)
45
- else:
46
- _HAS_RUN_SEQUENCE = True
47
153
 
154
+ def _validate_export_dependencies() -> None:
155
+ """Ensure exported helpers and their manifest entries stay in sync."""
48
156
 
49
- _metadata = cached_import("importlib.metadata")
50
- if _metadata is None: # pragma: no cover
51
- _metadata = cached_import("importlib_metadata")
157
+ if "__all__" not in globals():
158
+ # Defensive guard for unusual import orders (should never trigger).
159
+ return
52
160
 
53
- if _metadata is not None: # pragma: no cover
54
- version = _metadata.version # type: ignore[attr-defined]
55
- PackageNotFoundError = _metadata.PackageNotFoundError # type: ignore[attr-defined]
56
- else: # pragma: no cover
161
+ issues: list[str] = []
162
+ manifest = EXPORT_DEPENDENCIES
163
+ export_names = [name for name in __all__ if name != "__version__"]
164
+ manifest_names = set(manifest)
57
165
 
58
- class PackageNotFoundError(Exception):
59
- pass
166
+ for export_name in export_names:
167
+ if export_name not in manifest:
168
+ issues.append(
169
+ f"helper '{export_name}' is exported via __all__ but missing from EXPORT_DEPENDENCIES"
170
+ )
171
+ continue
60
172
 
61
- def version(_: str) -> str:
62
- raise PackageNotFoundError
173
+ entry = manifest[export_name]
174
+ if not isinstance(entry, dict):
175
+ issues.append(
176
+ f"helper '{export_name}' has a malformed manifest entry (expected mapping, got {type(entry)!r})"
177
+ )
178
+ continue
63
179
 
180
+ for key in ("submodules", "third_party"):
181
+ value = entry.get(key)
182
+ if not value:
183
+ issues.append(
184
+ f"helper '{export_name}' is missing '{key}' dependencies in EXPORT_DEPENDENCIES"
185
+ )
64
186
 
65
- try:
66
- __version__ = version("tnfr")
67
- except PackageNotFoundError: # pragma: no cover
68
- tomllib = cached_import("tomllib")
69
- if tomllib is not None:
70
- from pathlib import Path
187
+ missing_exports = manifest_names.difference(export_names).difference(_MISSING_EXPORTS)
188
+ for manifest_only in sorted(missing_exports):
189
+ entry = manifest[manifest_only]
190
+ if not isinstance(entry, dict):
191
+ issues.append(
192
+ f"helper '{manifest_only}' has a malformed manifest entry (expected mapping, got {type(entry)!r})"
193
+ )
194
+ continue
195
+
196
+ for key in ("submodules", "third_party"):
197
+ value = entry.get(key)
198
+ if not value:
199
+ issues.append(
200
+ f"helper '{manifest_only}' is missing '{key}' dependencies in EXPORT_DEPENDENCIES"
201
+ )
202
+
203
+ issues.append(
204
+ f"helper '{manifest_only}' is listed in EXPORT_DEPENDENCIES but not exported via __all__"
205
+ )
206
+
207
+ if issues:
208
+ raise ExportDependencyError(
209
+ "Invalid TNFR export dependency manifest:\n- " + "\n- ".join(issues)
210
+ )
211
+
212
+
213
+ def _assign_exports(module: str, names: tuple[str, ...]) -> bool:
214
+ try: # pragma: no cover - exercised in import tests
215
+ mod = import_module(f".{module}", __name__)
216
+ except ImportError as exc: # pragma: no cover - no missing deps in CI
217
+ if _is_internal_import_error(exc):
218
+ raise
219
+ for export_name in names:
220
+ stub = _missing_dependency(export_name, exc, module=module)
221
+ globals()[export_name] = stub
222
+ _MISSING_EXPORTS[export_name] = getattr(
223
+ stub, "__tnfr_missing_dependency__", {}
224
+ )
225
+ return False
226
+ else:
227
+ for export_name in names:
228
+ globals()[export_name] = getattr(mod, export_name)
229
+ return True
230
+
231
+
232
+ _assign_exports("dynamics", ("step", "run"))
233
+
234
+
235
+ _HAS_PREPARE_NETWORK = _assign_exports("ontosim", ("prepare_network",))
236
+
237
+
238
+ _HAS_RUN_SEQUENCE = _assign_exports("structural", ("create_nfr", "run_sequence"))
239
+
240
+
241
+ def _emit_missing_dependency_warning() -> None:
242
+ if not _MISSING_EXPORTS:
243
+ return
244
+ details = ", ".join(
245
+ f"{name} (missing: {info.get('missing') or 'unknown'})"
246
+ for name, info in sorted(_MISSING_EXPORTS.items())
247
+ )
248
+ warnings.warn(
249
+ "TNFR helpers disabled because dependencies are missing: " + details,
250
+ ImportWarning,
251
+ stacklevel=2,
252
+ )
253
+
254
+
255
+ _emit_missing_dependency_warning()
71
256
 
72
- try:
73
- with (Path(__file__).resolve().parents[2] / "pyproject.toml").open(
74
- "rb",
75
- ) as f:
76
- __version__ = tomllib.load(f)["project"]["version"]
77
- except (OSError, KeyError, ValueError): # pragma: no cover
78
- __version__ = "0+unknown"
79
- else: # pragma: no cover
80
- __version__ = "0+unknown"
81
257
 
82
258
  __all__ = [
83
259
  "__version__",
84
260
  "step",
85
261
  "run",
86
- "preparar_red",
262
+ "prepare_network",
87
263
  "create_nfr",
88
264
  ]
89
265
 
90
266
  if _HAS_RUN_SEQUENCE:
91
267
  __all__.append("run_sequence")
268
+
269
+
270
+ _validate_export_dependencies()
tnfr/__init__.pyi ADDED
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any, NoReturn
5
+
6
+ from .dynamics import run, step
7
+ from .ontosim import prepare_network
8
+ from .structural import create_nfr, run_sequence
9
+
10
+ EXPORT_DEPENDENCIES: dict[str, dict[str, tuple[str, ...]]]
11
+ """Manifest describing required submodules and third-party packages."""
12
+
13
+ _MISSING_EXPORTS: dict[str, dict[str, Any]]
14
+
15
+ __version__: str
16
+ __all__: list[str]
17
+
18
+
19
+ class ExportDependencyError(RuntimeError):
20
+ """Raised when the export dependency manifest is inconsistent."""
21
+
22
+
23
+ def _is_internal_import_error(exc: ImportError) -> bool: ...
24
+
25
+ def _missing_dependency(
26
+ name: str,
27
+ exc: ImportError,
28
+ *,
29
+ module: str | None = ...,
30
+ ) -> Callable[..., NoReturn]: ...
31
+
32
+
33
+ def _validate_export_dependencies() -> None: ...
34
+
35
+ def _assign_exports(module: str, names: tuple[str, ...]) -> bool: ...
36
+
37
+ def _emit_missing_dependency_warning() -> None: ...
38
+
39
+ _HAS_PREPARE_NETWORK: bool
40
+ _HAS_RUN_SEQUENCE: bool
tnfr/_compat.py ADDED
@@ -0,0 +1,11 @@
1
+ """Compatibility helpers for bridging typing features across Python versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try: # pragma: no cover - exercised implicitly by importers
6
+ from typing import TypeAlias # type: ignore[attr-defined]
7
+ except (ImportError, AttributeError): # pragma: no cover - Python < 3.10
8
+ from typing_extensions import TypeAlias # type: ignore[assignment]
9
+
10
+ __all__ = ["TypeAlias"]
11
+
tnfr/_version.py ADDED
@@ -0,0 +1,7 @@
1
+ """Package version for :mod:`tnfr`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["__version__"]
6
+
7
+ __version__ = "6.0.0"
tnfr/_version.pyi ADDED
@@ -0,0 +1,7 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ __version__: Any
tnfr/alias.py CHANGED
@@ -8,31 +8,32 @@ alias-based attribute access. Legacy wrappers ``alias_get`` and
8
8
 
9
9
  from __future__ import annotations
10
10
  from collections import defaultdict
11
- from collections.abc import Iterable, Sized
11
+ from collections.abc import Iterable, Mapping, MutableMapping, Sized
12
12
  from dataclasses import dataclass
13
+ from functools import lru_cache, partial
14
+ from threading import Lock
15
+ from types import ModuleType
13
16
  from typing import (
17
+ TYPE_CHECKING,
14
18
  Any,
15
19
  Callable,
16
- TypeVar,
17
- Optional,
18
20
  Generic,
19
21
  Hashable,
20
- TYPE_CHECKING,
22
+ Optional,
23
+ TypeVar,
21
24
  cast,
22
25
  )
23
26
 
24
- from functools import lru_cache, partial
25
- from threading import Lock
26
-
27
27
  from .constants import get_aliases
28
- from .value_utils import convert_value
28
+ from .types import FloatArray, NodeId
29
+ from .utils import convert_value
29
30
 
30
31
  ALIAS_VF = get_aliases("VF")
31
32
  ALIAS_DNFR = get_aliases("DNFR")
32
33
  ALIAS_THETA = get_aliases("THETA")
33
34
 
34
35
  if TYPE_CHECKING: # pragma: no cover
35
- import networkx # type: ignore[import-untyped]
36
+ import networkx
36
37
 
37
38
  T = TypeVar("T")
38
39
 
@@ -184,6 +185,25 @@ class AliasAccessor(Generic[T]):
184
185
  _generic_accessor: AliasAccessor[Any] = AliasAccessor()
185
186
 
186
187
 
188
+ def get_theta_attr(
189
+ d: Mapping[str, Any],
190
+ default: T | None = None,
191
+ *,
192
+ strict: bool = False,
193
+ log_level: int | None = None,
194
+ conv: Callable[[Any], T] = float,
195
+ ) -> T | None:
196
+ """Return ``theta``/``phase`` using the English alias set."""
197
+ return _generic_accessor.get(
198
+ cast(dict[str, Any], d),
199
+ ALIAS_THETA,
200
+ default,
201
+ strict=strict,
202
+ log_level=log_level,
203
+ conv=conv,
204
+ )
205
+
206
+
187
207
  def get_attr(
188
208
  d: dict[str, Any],
189
209
  aliases: Iterable[str],
@@ -207,12 +227,12 @@ def get_attr(
207
227
 
208
228
  def collect_attr(
209
229
  G: "networkx.Graph",
210
- nodes: Iterable[Any],
230
+ nodes: Iterable[NodeId],
211
231
  aliases: Iterable[str],
212
232
  default: float = 0.0,
213
233
  *,
214
- np=None,
215
- ):
234
+ np: ModuleType | None = None,
235
+ ) -> FloatArray | list[float]:
216
236
  """Collect attribute values for ``nodes`` from ``G`` using ``aliases``.
217
237
 
218
238
  Parameters
@@ -235,7 +255,7 @@ def collect_attr(
235
255
  Collected attribute values in the same order as ``nodes``.
236
256
  """
237
257
 
238
- def _nodes_iter_and_size(nodes: Iterable[Any]) -> tuple[Iterable[Any], int]:
258
+ def _nodes_iter_and_size(nodes: Iterable[NodeId]) -> tuple[Iterable[NodeId], int]:
239
259
  if nodes is G.nodes:
240
260
  return G.nodes, G.number_of_nodes()
241
261
  if isinstance(nodes, Sized):
@@ -245,13 +265,42 @@ def collect_attr(
245
265
 
246
266
  nodes_iter, size = _nodes_iter_and_size(nodes)
247
267
 
268
+ def _value(node: NodeId) -> float:
269
+ return float(get_attr(G.nodes[node], aliases, default))
270
+
248
271
  if np is not None:
249
- return np.fromiter(
250
- (get_attr(G.nodes[n], aliases, default) for n in nodes_iter),
251
- float,
252
- count=size,
253
- )
254
- return [get_attr(G.nodes[n], aliases, default) for n in nodes_iter]
272
+ values: FloatArray = np.fromiter((_value(n) for n in nodes_iter), float, count=size)
273
+ return values
274
+ return [_value(n) for n in nodes_iter]
275
+
276
+
277
+ def collect_theta_attr(
278
+ G: "networkx.Graph",
279
+ nodes: Iterable[NodeId],
280
+ default: float = 0.0,
281
+ *,
282
+ np: ModuleType | None = None,
283
+ ) -> FloatArray | list[float]:
284
+ """Collect ``theta`` values honouring the English-only attribute contract."""
285
+
286
+ def _nodes_iter_and_size(nodes: Iterable[NodeId]) -> tuple[Iterable[NodeId], int]:
287
+ if nodes is G.nodes:
288
+ return G.nodes, G.number_of_nodes()
289
+ if isinstance(nodes, Sized):
290
+ return nodes, len(nodes) # type: ignore[arg-type]
291
+ nodes_list = list(nodes)
292
+ return nodes_list, len(nodes_list)
293
+
294
+ nodes_iter, size = _nodes_iter_and_size(nodes)
295
+
296
+ def _value(node: NodeId) -> float:
297
+ return float(get_theta_attr(G.nodes[node], default))
298
+
299
+ if np is not None:
300
+ values: FloatArray = np.fromiter((_value(n) for n in nodes_iter), float, count=size)
301
+ return values
302
+
303
+ return [_value(n) for n in nodes_iter]
255
304
 
256
305
 
257
306
  def set_attr_generic(
@@ -273,8 +322,16 @@ get_attr_str = partial(get_attr, conv=str)
273
322
  set_attr_str = partial(set_attr_generic, conv=str)
274
323
 
275
324
 
325
+ def set_theta_attr(d: MutableMapping[str, Any], value: Any) -> float:
326
+ """Assign ``theta``/``phase`` using the English attribute names."""
327
+ result = float(value)
328
+ d["theta"] = result
329
+ d["phase"] = result
330
+ return result
331
+
332
+
276
333
  # -------------------------
277
- # Máximos globales con caché
334
+ # Cached global maxima
278
335
  # -------------------------
279
336
 
280
337
 
@@ -484,7 +541,7 @@ SCALAR_SETTERS: dict[str, dict[str, Any]] = {
484
541
  "theta": {
485
542
  "alias": ALIAS_THETA,
486
543
  "extra": _increment_trig_version,
487
- "doc": "Set ``θ`` for node ``n`` and invalidate trig caches.",
544
+ "doc": "Set ``theta`` for node ``n`` and invalidate trig caches.",
488
545
  },
489
546
  }
490
547
 
@@ -529,14 +586,42 @@ for _name, _spec in SCALAR_SETTERS.items():
529
586
  del _name, _spec, _make_scalar_setter
530
587
 
531
588
 
589
+ _set_theta_impl = cast(
590
+ Callable[["networkx.Graph", Hashable, float], AbsMaxResult | None],
591
+ globals()["set_theta"],
592
+ )
593
+
594
+
595
+ def _set_theta_with_compat(
596
+ G: "networkx.Graph", n: Hashable, value: float
597
+ ) -> AbsMaxResult | None:
598
+ nd = cast(MutableMapping[str, Any], G.nodes[n])
599
+ result = _set_theta_impl(G, n, value)
600
+ theta_val = get_theta_attr(nd, value)
601
+ if theta_val is not None:
602
+ float_theta = float(theta_val)
603
+ nd["theta"] = float_theta
604
+ nd["phase"] = float_theta
605
+ return result
606
+
607
+
608
+ _set_theta_with_compat.__name__ = "set_theta"
609
+ _set_theta_with_compat.__qualname__ = "set_theta"
610
+ _set_theta_with_compat.__doc__ = _set_theta_impl.__doc__
611
+ globals()["set_theta"] = _set_theta_with_compat
612
+
613
+
532
614
  __all__ = [
533
615
  "AbsMaxResult",
534
616
  "set_attr_generic",
535
617
  "get_attr",
618
+ "get_theta_attr",
536
619
  "collect_attr",
620
+ "collect_theta_attr",
537
621
  "set_attr",
538
622
  "get_attr_str",
539
623
  "set_attr_str",
624
+ "set_theta_attr",
540
625
  "set_attr_and_cache",
541
626
  "set_attr_with_max",
542
627
  "set_scalar",