tnfr 4.5.1__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 (170) hide show
  1. tnfr/__init__.py +270 -90
  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 +631 -0
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +732 -0
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +381 -0
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +89 -0
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +199 -0
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +322 -0
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +34 -0
  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/init.py +36 -0
  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 +228 -0
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +158 -0
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.py +31 -0
  35. tnfr/constants/init.pyi +12 -0
  36. tnfr/constants/metric.py +102 -0
  37. tnfr/constants/metric.pyi +19 -0
  38. tnfr/constants_glyphs.py +16 -0
  39. tnfr/constants_glyphs.pyi +12 -0
  40. tnfr/dynamics/__init__.py +136 -0
  41. tnfr/dynamics/__init__.pyi +83 -0
  42. tnfr/dynamics/adaptation.py +201 -0
  43. tnfr/dynamics/aliases.py +22 -0
  44. tnfr/dynamics/coordination.py +343 -0
  45. tnfr/dynamics/dnfr.py +2315 -0
  46. tnfr/dynamics/dnfr.pyi +33 -0
  47. tnfr/dynamics/integrators.py +561 -0
  48. tnfr/dynamics/integrators.pyi +35 -0
  49. tnfr/dynamics/runtime.py +521 -0
  50. tnfr/dynamics/sampling.py +34 -0
  51. tnfr/dynamics/sampling.pyi +7 -0
  52. tnfr/dynamics/selectors.py +680 -0
  53. tnfr/execution.py +216 -0
  54. tnfr/execution.pyi +65 -0
  55. tnfr/flatten.py +283 -0
  56. tnfr/flatten.pyi +28 -0
  57. tnfr/gamma.py +320 -89
  58. tnfr/gamma.pyi +40 -0
  59. tnfr/glyph_history.py +337 -0
  60. tnfr/glyph_history.pyi +53 -0
  61. tnfr/grammar.py +23 -153
  62. tnfr/grammar.pyi +13 -0
  63. tnfr/helpers/__init__.py +151 -0
  64. tnfr/helpers/__init__.pyi +66 -0
  65. tnfr/helpers/numeric.py +88 -0
  66. tnfr/helpers/numeric.pyi +12 -0
  67. tnfr/immutable.py +214 -0
  68. tnfr/immutable.pyi +37 -0
  69. tnfr/initialization.py +199 -0
  70. tnfr/initialization.pyi +73 -0
  71. tnfr/io.py +311 -0
  72. tnfr/io.pyi +11 -0
  73. tnfr/locking.py +37 -0
  74. tnfr/locking.pyi +7 -0
  75. tnfr/metrics/__init__.py +41 -0
  76. tnfr/metrics/__init__.pyi +20 -0
  77. tnfr/metrics/coherence.py +1469 -0
  78. tnfr/metrics/common.py +149 -0
  79. tnfr/metrics/common.pyi +15 -0
  80. tnfr/metrics/core.py +259 -0
  81. tnfr/metrics/core.pyi +13 -0
  82. tnfr/metrics/diagnosis.py +840 -0
  83. tnfr/metrics/diagnosis.pyi +89 -0
  84. tnfr/metrics/export.py +151 -0
  85. tnfr/metrics/glyph_timing.py +369 -0
  86. tnfr/metrics/reporting.py +152 -0
  87. tnfr/metrics/reporting.pyi +12 -0
  88. tnfr/metrics/sense_index.py +294 -0
  89. tnfr/metrics/sense_index.pyi +9 -0
  90. tnfr/metrics/trig.py +216 -0
  91. tnfr/metrics/trig.pyi +12 -0
  92. tnfr/metrics/trig_cache.py +105 -0
  93. tnfr/metrics/trig_cache.pyi +10 -0
  94. tnfr/node.py +255 -177
  95. tnfr/node.pyi +161 -0
  96. tnfr/observers.py +154 -150
  97. tnfr/observers.pyi +46 -0
  98. tnfr/ontosim.py +135 -134
  99. tnfr/ontosim.pyi +33 -0
  100. tnfr/operators/__init__.py +452 -0
  101. tnfr/operators/__init__.pyi +31 -0
  102. tnfr/operators/definitions.py +181 -0
  103. tnfr/operators/definitions.pyi +92 -0
  104. tnfr/operators/jitter.py +266 -0
  105. tnfr/operators/jitter.pyi +11 -0
  106. tnfr/operators/registry.py +80 -0
  107. tnfr/operators/registry.pyi +15 -0
  108. tnfr/operators/remesh.py +569 -0
  109. tnfr/presets.py +10 -23
  110. tnfr/presets.pyi +7 -0
  111. tnfr/py.typed +0 -0
  112. tnfr/rng.py +440 -0
  113. tnfr/rng.pyi +14 -0
  114. tnfr/selector.py +217 -0
  115. tnfr/selector.pyi +19 -0
  116. tnfr/sense.py +307 -142
  117. tnfr/sense.pyi +30 -0
  118. tnfr/structural.py +69 -164
  119. tnfr/structural.pyi +46 -0
  120. tnfr/telemetry/__init__.py +13 -0
  121. tnfr/telemetry/verbosity.py +37 -0
  122. tnfr/tokens.py +61 -0
  123. tnfr/tokens.pyi +41 -0
  124. tnfr/trace.py +520 -95
  125. tnfr/trace.pyi +68 -0
  126. tnfr/types.py +382 -17
  127. tnfr/types.pyi +145 -0
  128. tnfr/utils/__init__.py +158 -0
  129. tnfr/utils/__init__.pyi +133 -0
  130. tnfr/utils/cache.py +755 -0
  131. tnfr/utils/cache.pyi +156 -0
  132. tnfr/utils/data.py +267 -0
  133. tnfr/utils/data.pyi +73 -0
  134. tnfr/utils/graph.py +87 -0
  135. tnfr/utils/graph.pyi +10 -0
  136. tnfr/utils/init.py +746 -0
  137. tnfr/utils/init.pyi +85 -0
  138. tnfr/utils/io.py +157 -0
  139. tnfr/utils/io.pyi +10 -0
  140. tnfr/utils/validators.py +130 -0
  141. tnfr/utils/validators.pyi +19 -0
  142. tnfr/validation/__init__.py +25 -0
  143. tnfr/validation/__init__.pyi +17 -0
  144. tnfr/validation/compatibility.py +59 -0
  145. tnfr/validation/compatibility.pyi +8 -0
  146. tnfr/validation/grammar.py +149 -0
  147. tnfr/validation/grammar.pyi +11 -0
  148. tnfr/validation/rules.py +194 -0
  149. tnfr/validation/rules.pyi +18 -0
  150. tnfr/validation/syntax.py +151 -0
  151. tnfr/validation/syntax.pyi +7 -0
  152. tnfr-6.0.0.dist-info/METADATA +135 -0
  153. tnfr-6.0.0.dist-info/RECORD +157 -0
  154. tnfr/cli.py +0 -322
  155. tnfr/config.py +0 -41
  156. tnfr/constants.py +0 -277
  157. tnfr/dynamics.py +0 -814
  158. tnfr/helpers.py +0 -264
  159. tnfr/main.py +0 -47
  160. tnfr/metrics.py +0 -597
  161. tnfr/operators.py +0 -525
  162. tnfr/program.py +0 -176
  163. tnfr/scenarios.py +0 -34
  164. tnfr/validators.py +0 -38
  165. tnfr-4.5.1.dist-info/METADATA +0 -221
  166. tnfr-4.5.1.dist-info/RECORD +0 -28
  167. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  168. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  169. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,105 @@
1
+ """Trigonometric caches for TNFR metrics.
2
+
3
+ The cosine/sine storage helpers live here to keep :mod:`tnfr.metrics.trig`
4
+ focused on pure mathematical utilities (phase means, compensated sums, etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ from dataclasses import dataclass
11
+ from typing import Any, Iterable, Mapping
12
+
13
+ from ..alias import get_theta_attr
14
+ from ..types import GraphLike
15
+ from ..utils import edge_version_cache, get_numpy
16
+
17
+ __all__ = ("TrigCache", "compute_theta_trig", "get_trig_cache", "_compute_trig_python")
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class TrigCache:
22
+ """Container for cached trigonometric values per node."""
23
+
24
+ cos: dict[Any, float]
25
+ sin: dict[Any, float]
26
+ theta: dict[Any, float]
27
+
28
+
29
+ def _iter_theta_pairs(
30
+ nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
31
+ ) -> Iterable[tuple[Any, float]]:
32
+ """Yield ``(node, θ)`` pairs from ``nodes``."""
33
+
34
+ for n, data in nodes:
35
+ if isinstance(data, Mapping):
36
+ yield n, get_theta_attr(data, 0.0) or 0.0
37
+ else:
38
+ yield n, float(data)
39
+
40
+
41
+ def _compute_trig_python(
42
+ nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
43
+ ) -> TrigCache:
44
+ """Compute trigonometric mappings using pure Python."""
45
+
46
+ cos_th: dict[Any, float] = {}
47
+ sin_th: dict[Any, float] = {}
48
+ thetas: dict[Any, float] = {}
49
+ for n, th in _iter_theta_pairs(nodes):
50
+ thetas[n] = th
51
+ cos_th[n] = math.cos(th)
52
+ sin_th[n] = math.sin(th)
53
+ return TrigCache(cos=cos_th, sin=sin_th, theta=thetas)
54
+
55
+
56
+ def compute_theta_trig(
57
+ nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
58
+ np: Any | None = None,
59
+ ) -> TrigCache:
60
+ """Return trigonometric mappings of ``θ`` per node."""
61
+
62
+ if np is None:
63
+ np = get_numpy()
64
+ if np is None or not all(hasattr(np, attr) for attr in ("fromiter", "cos", "sin")):
65
+ return _compute_trig_python(nodes)
66
+
67
+ pairs = list(_iter_theta_pairs(nodes))
68
+ if not pairs:
69
+ return TrigCache(cos={}, sin={}, theta={})
70
+
71
+ node_list, theta_vals = zip(*pairs)
72
+ theta_arr = np.fromiter(theta_vals, dtype=float)
73
+ cos_arr = np.cos(theta_arr)
74
+ sin_arr = np.sin(theta_arr)
75
+
76
+ cos_th = dict(zip(node_list, map(float, cos_arr)))
77
+ sin_th = dict(zip(node_list, map(float, sin_arr)))
78
+ thetas = dict(zip(node_list, map(float, theta_arr)))
79
+ return TrigCache(cos=cos_th, sin=sin_th, theta=thetas)
80
+
81
+
82
+ def _build_trig_cache(G: GraphLike, np: Any | None = None) -> TrigCache:
83
+ """Construct trigonometric cache for ``G``."""
84
+
85
+ return compute_theta_trig(G.nodes(data=True), np=np)
86
+
87
+
88
+ def get_trig_cache(
89
+ G: GraphLike,
90
+ *,
91
+ np: Any | None = None,
92
+ cache_size: int | None = 128,
93
+ ) -> TrigCache:
94
+ """Return cached cosines and sines of ``θ`` per node."""
95
+
96
+ if np is None:
97
+ np = get_numpy()
98
+ version = G.graph.setdefault("_trig_version", 0)
99
+ key = ("_trig", version)
100
+ return edge_version_cache(
101
+ G,
102
+ key,
103
+ lambda: _build_trig_cache(G, np=np),
104
+ max_entries=cache_size,
105
+ )
@@ -0,0 +1,10 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ TrigCache: Any
8
+ _compute_trig_python: Any
9
+ compute_theta_trig: Any
10
+ get_trig_cache: Any
tnfr/node.py CHANGED
@@ -1,202 +1,280 @@
1
- from __future__ import annotations
2
- from dataclasses import dataclass, field
3
- from typing import Deque, Dict, Iterable, List, Optional, Protocol
4
- from collections import deque
5
-
6
- from .constants import DEFAULTS
7
- from .helpers import push_glifo
8
-
9
-
10
- class NodoProtocol(Protocol):
11
- """Protocolo mínimo para nodos TNFR."""
1
+ """Node utilities and structures for TNFR graphs."""
12
2
 
13
- EPI: float
14
- vf: float
15
- theta: float
16
- Si: float
3
+ from __future__ import annotations
4
+ from typing import (
5
+ Any,
6
+ Callable,
7
+ Iterable,
8
+ MutableMapping,
9
+ Optional,
10
+ Protocol,
11
+ SupportsFloat,
12
+ TypeVar,
13
+ )
14
+ from collections.abc import Hashable
15
+ import math
16
+ from dataclasses import dataclass
17
+
18
+ from .constants import get_aliases
19
+ from .alias import (
20
+ get_attr,
21
+ get_theta_attr,
22
+ get_attr_str,
23
+ set_attr,
24
+ set_attr_str,
25
+ set_vf,
26
+ set_dnfr,
27
+ set_theta,
28
+ )
29
+ from .types import (
30
+ CouplingWeight,
31
+ DeltaNFR,
32
+ EPIValue,
33
+ NodeId,
34
+ Phase,
35
+ SecondDerivativeEPI,
36
+ SenseIndex,
37
+ StructuralFrequency,
38
+ TNFRGraph,
39
+ )
40
+ from .utils import (
41
+ cached_node_list,
42
+ ensure_node_offset_map,
43
+ increment_edge_version,
44
+ supports_add_edge,
45
+ )
46
+ from .locking import get_lock
47
+
48
+ ALIAS_EPI = get_aliases("EPI")
49
+ ALIAS_VF = get_aliases("VF")
50
+ ALIAS_THETA = get_aliases("THETA")
51
+ ALIAS_SI = get_aliases("SI")
52
+ ALIAS_EPI_KIND = get_aliases("EPI_KIND")
53
+ ALIAS_DNFR = get_aliases("DNFR")
54
+ ALIAS_D2EPI = get_aliases("D2EPI")
55
+
56
+ T = TypeVar("T")
57
+
58
+ __all__ = ("NodeNX", "NodeProtocol", "add_edge")
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class AttrSpec:
63
+ """Configuration required to expose a ``networkx`` node attribute.
64
+
65
+ ``AttrSpec`` mirrors the defaults previously used by
66
+ :func:`_nx_attr_property` and centralises the descriptor generation
67
+ logic to keep a single source of truth for NodeNX attribute access.
68
+ """
69
+
70
+ aliases: tuple[str, ...]
71
+ default: Any = 0.0
72
+ getter: Callable[[MutableMapping[str, Any], tuple[str, ...], Any], Any] = get_attr
73
+ setter: Callable[..., None] = set_attr
74
+ to_python: Callable[[Any], Any] = float
75
+ to_storage: Callable[[Any], Any] = float
76
+ use_graph_setter: bool = False
77
+
78
+ def build_property(self) -> property:
79
+ """Create the property descriptor for ``NodeNX`` attributes."""
80
+
81
+ def fget(instance: "NodeNX") -> T:
82
+ return self.to_python(
83
+ self.getter(instance.G.nodes[instance.n], self.aliases, self.default)
84
+ )
85
+
86
+ def fset(instance: "NodeNX", value: T) -> None:
87
+ value = self.to_storage(value)
88
+ if self.use_graph_setter:
89
+ self.setter(instance.G, instance.n, value)
90
+ else:
91
+ self.setter(instance.G.nodes[instance.n], self.aliases, value)
92
+
93
+ return property(fget, fset)
94
+
95
+
96
+ # Mapping of NodeNX attribute specifications used to generate property
97
+ # descriptors. Each entry defines the keyword arguments passed to
98
+ # ``AttrSpec.build_property`` for a given attribute name.
99
+ ATTR_SPECS: dict[str, AttrSpec] = {
100
+ "EPI": AttrSpec(aliases=ALIAS_EPI),
101
+ "vf": AttrSpec(aliases=ALIAS_VF, setter=set_vf, use_graph_setter=True),
102
+ "theta": AttrSpec(
103
+ aliases=ALIAS_THETA,
104
+ getter=lambda mapping, _aliases, default: get_theta_attr(mapping, default),
105
+ setter=set_theta,
106
+ use_graph_setter=True,
107
+ ),
108
+ "Si": AttrSpec(aliases=ALIAS_SI),
109
+ "epi_kind": AttrSpec(
110
+ aliases=ALIAS_EPI_KIND,
111
+ default="",
112
+ getter=get_attr_str,
113
+ setter=set_attr_str,
114
+ to_python=str,
115
+ to_storage=str,
116
+ ),
117
+ "dnfr": AttrSpec(aliases=ALIAS_DNFR, setter=set_dnfr, use_graph_setter=True),
118
+ "d2EPI": AttrSpec(aliases=ALIAS_D2EPI),
119
+ }
120
+
121
+
122
+ def _add_edge_common(
123
+ n1: NodeId,
124
+ n2: NodeId,
125
+ weight: CouplingWeight | SupportsFloat | str,
126
+ ) -> Optional[CouplingWeight]:
127
+ """Validate basic edge constraints.
128
+
129
+ Returns the parsed weight if the edge can be added. ``None`` is returned
130
+ when the edge should be ignored (e.g. self-connections).
131
+ """
132
+
133
+ if n1 == n2:
134
+ return None
135
+
136
+ weight = float(weight)
137
+ if not math.isfinite(weight):
138
+ raise ValueError("Edge weight must be a finite number")
139
+ if weight < 0:
140
+ raise ValueError("Edge weight must be non-negative")
141
+
142
+ return weight
143
+
144
+
145
+ def add_edge(
146
+ graph: TNFRGraph,
147
+ n1: NodeId,
148
+ n2: NodeId,
149
+ weight: CouplingWeight | SupportsFloat | str,
150
+ overwrite: bool = False,
151
+ ) -> None:
152
+ """Add an edge between ``n1`` and ``n2`` in a ``networkx`` graph."""
153
+
154
+ weight = _add_edge_common(n1, n2, weight)
155
+ if weight is None:
156
+ return
157
+
158
+ if not supports_add_edge(graph):
159
+ raise TypeError("add_edge only supports networkx graphs")
160
+
161
+ if graph.has_edge(n1, n2) and not overwrite:
162
+ return
163
+
164
+ graph.add_edge(n1, n2, weight=weight)
165
+ increment_edge_version(graph)
166
+
167
+
168
+ class NodeProtocol(Protocol):
169
+ """Minimal protocol for TNFR nodes."""
170
+
171
+ EPI: EPIValue
172
+ vf: StructuralFrequency
173
+ theta: Phase
174
+ Si: SenseIndex
17
175
  epi_kind: str
18
- dnfr: float
19
- d2EPI: float
20
- graph: Dict[str, object]
176
+ dnfr: DeltaNFR
177
+ d2EPI: SecondDerivativeEPI
178
+ graph: MutableMapping[str, Any]
21
179
 
22
- def neighbors(self) -> Iterable["NodoProtocol"]:
180
+ def neighbors(self) -> Iterable[NodeProtocol | Hashable]:
23
181
  ...
24
182
 
25
- def push_glifo(self, glifo: str, window: int) -> None:
183
+ def _glyph_storage(self) -> MutableMapping[str, object]:
26
184
  ...
27
185
 
28
- def has_edge(self, other: "NodoProtocol") -> bool:
186
+ def has_edge(self, other: "NodeProtocol") -> bool:
29
187
  ...
30
188
 
31
- def add_edge(self, other: "NodoProtocol", weight: float) -> None:
189
+ def add_edge(
190
+ self,
191
+ other: NodeProtocol,
192
+ weight: CouplingWeight,
193
+ *,
194
+ overwrite: bool = False,
195
+ ) -> None:
32
196
  ...
33
197
 
34
198
  def offset(self) -> int:
35
199
  ...
36
200
 
37
- def all_nodes(self) -> Iterable["NodoProtocol"]:
201
+ def all_nodes(self) -> Iterable[NodeProtocol]:
38
202
  ...
39
203
 
40
204
 
41
- @dataclass
42
- class NodoTNFR:
43
- """Representa un nodo TNFR autónomo."""
44
-
45
- EPI: float = 0.0
46
- vf: float = 0.0
47
- theta: float = 0.0
48
- Si: float = 0.0
49
- epi_kind: str = ""
50
- dnfr: float = 0.0
51
- d2EPI: float = 0.0
52
- graph: Dict[str, object] = field(default_factory=dict)
53
- _neighbors: List["NodoTNFR"] = field(default_factory=list)
54
- _hist_glifos: Deque[str] = field(default_factory=lambda: deque(maxlen=DEFAULTS.get("GLYPH_HYSTERESIS_WINDOW", 7)))
55
-
56
- def neighbors(self) -> Iterable["NodoTNFR"]:
57
- return list(self._neighbors)
58
-
59
- def has_edge(self, other: "NodoTNFR") -> bool:
60
- return other in self._neighbors
61
-
62
- def add_edge(self, other: "NodoTNFR", weight: float = 1.0) -> None:
63
- if other not in self._neighbors:
64
- self._neighbors.append(other)
65
- other._neighbors.append(self)
66
-
67
- def push_glifo(self, glifo: str, window: int) -> None:
68
- self._hist_glifos.append(glifo)
69
- while len(self._hist_glifos) > window:
70
- self._hist_glifos.popleft()
71
- self.epi_kind = glifo
72
-
73
- def offset(self) -> int:
74
- return 0
75
-
76
- def all_nodes(self) -> Iterable["NodoTNFR"]:
77
- return list(getattr(self.graph, "_all_nodes", [self]))
78
-
79
- def aplicar_glifo(self, glifo: str, window: Optional[int] = None) -> None:
80
- from .operators import aplicar_glifo_obj
81
- aplicar_glifo_obj(self, glifo, window=window)
82
-
83
- def integrar(self, dt: float) -> None:
84
- self.EPI += self.dnfr * dt
85
-
86
-
87
- class NodoNX(NodoProtocol):
88
- """Adaptador para nodos ``networkx``."""
89
-
90
- def __init__(self, G, n):
91
- self.G = G
92
- self.n = n
93
- self.graph = G.graph
94
-
95
- @property
96
- def EPI(self) -> float:
97
- from .helpers import _get_attr
98
- from .constants import ALIAS_EPI
99
- return float(_get_attr(self.G.nodes[self.n], ALIAS_EPI, 0.0))
100
-
101
- @EPI.setter
102
- def EPI(self, v: float) -> None:
103
- from .helpers import _set_attr
104
- from .constants import ALIAS_EPI
105
- _set_attr(self.G.nodes[self.n], ALIAS_EPI, float(v))
106
-
107
- @property
108
- def vf(self) -> float:
109
- from .helpers import _get_attr
110
- from .constants import ALIAS_VF
111
- return float(_get_attr(self.G.nodes[self.n], ALIAS_VF, 0.0))
112
-
113
- @vf.setter
114
- def vf(self, v: float) -> None:
115
- from .helpers import _set_attr
116
- from .constants import ALIAS_VF
117
- _set_attr(self.G.nodes[self.n], ALIAS_VF, float(v))
118
-
119
- @property
120
- def theta(self) -> float:
121
- from .helpers import _get_attr
122
- from .constants import ALIAS_THETA
123
- return float(_get_attr(self.G.nodes[self.n], ALIAS_THETA, 0.0))
124
-
125
- @theta.setter
126
- def theta(self, v: float) -> None:
127
- from .helpers import _set_attr
128
- from .constants import ALIAS_THETA
129
- _set_attr(self.G.nodes[self.n], ALIAS_THETA, float(v))
130
-
131
- @property
132
- def Si(self) -> float:
133
- from .helpers import _get_attr
134
- from .constants import ALIAS_SI
135
- return float(_get_attr(self.G.nodes[self.n], ALIAS_SI, 0.0))
136
-
137
- @Si.setter
138
- def Si(self, v: float) -> None:
139
- from .helpers import _set_attr
140
- from .constants import ALIAS_SI
141
- _set_attr(self.G.nodes[self.n], ALIAS_SI, float(v))
142
-
143
- @property
144
- def epi_kind(self) -> str:
145
- from .helpers import _get_attr_str
146
- from .constants import ALIAS_EPI_KIND
147
- return _get_attr_str(self.G.nodes[self.n], ALIAS_EPI_KIND, "")
148
-
149
- @epi_kind.setter
150
- def epi_kind(self, v: str) -> None:
151
- from .helpers import _set_attr_str
152
- from .constants import ALIAS_EPI_KIND
153
- _set_attr_str(self.G.nodes[self.n], ALIAS_EPI_KIND, str(v))
154
-
155
- @property
156
- def dnfr(self) -> float:
157
- from .helpers import _get_attr
158
- from .constants import ALIAS_DNFR
159
- return float(_get_attr(self.G.nodes[self.n], ALIAS_DNFR, 0.0))
160
-
161
- @dnfr.setter
162
- def dnfr(self, v: float) -> None:
163
- from .helpers import _set_attr
164
- from .constants import ALIAS_DNFR
165
- _set_attr(self.G.nodes[self.n], ALIAS_DNFR, float(v))
166
-
167
- @property
168
- def d2EPI(self) -> float:
169
- from .helpers import _get_attr
170
- from .constants import ALIAS_D2EPI
171
- return float(_get_attr(self.G.nodes[self.n], ALIAS_D2EPI, 0.0))
172
-
173
- @d2EPI.setter
174
- def d2EPI(self, v: float) -> None:
175
- from .helpers import _set_attr
176
- from .constants import ALIAS_D2EPI
177
- _set_attr(self.G.nodes[self.n], ALIAS_D2EPI, float(v))
178
-
179
- def neighbors(self) -> Iterable[NodoProtocol]:
180
- return [NodoNX(self.G, v) for v in self.G.neighbors(self.n)]
181
-
182
- def push_glifo(self, glifo: str, window: int) -> None:
183
- push_glifo(self.G.nodes[self.n], glifo, window)
184
- self.epi_kind = glifo
185
-
186
- def has_edge(self, other: NodoProtocol) -> bool:
187
- if isinstance(other, NodoNX):
205
+ class NodeNX(NodeProtocol):
206
+ """Adapter for ``networkx`` nodes."""
207
+
208
+ # Statically defined property descriptors for ``NodeNX`` attributes.
209
+ # Declaring them here makes the attributes discoverable by type checkers
210
+ # and IDEs, avoiding the previous runtime ``setattr`` loop.
211
+ EPI: EPIValue = ATTR_SPECS["EPI"].build_property()
212
+ vf: StructuralFrequency = ATTR_SPECS["vf"].build_property()
213
+ theta: Phase = ATTR_SPECS["theta"].build_property()
214
+ Si: SenseIndex = ATTR_SPECS["Si"].build_property()
215
+ epi_kind: str = ATTR_SPECS["epi_kind"].build_property()
216
+ dnfr: DeltaNFR = ATTR_SPECS["dnfr"].build_property()
217
+ d2EPI: SecondDerivativeEPI = ATTR_SPECS["d2EPI"].build_property()
218
+
219
+ def __init__(self, G: TNFRGraph, n: NodeId) -> None:
220
+ self.G: TNFRGraph = G
221
+ self.n: NodeId = n
222
+ self.graph: MutableMapping[str, Any] = G.graph
223
+ G.graph.setdefault("_node_cache", {})[n] = self
224
+
225
+ def _glyph_storage(self) -> MutableMapping[str, Any]:
226
+ return self.G.nodes[self.n]
227
+
228
+ @classmethod
229
+ def from_graph(cls, G: TNFRGraph, n: NodeId) -> "NodeNX":
230
+ """Return cached ``NodeNX`` for ``(G, n)`` with thread safety."""
231
+ lock = get_lock(f"node_nx_cache_{id(G)}")
232
+ with lock:
233
+ cache = G.graph.setdefault("_node_cache", {})
234
+ node = cache.get(n)
235
+ if node is None:
236
+ node = cls(G, n)
237
+ return node
238
+
239
+ def neighbors(self) -> Iterable[NodeId]:
240
+ """Iterate neighbour identifiers (IDs).
241
+
242
+ Wrap each resulting ID with :meth:`from_graph` to obtain the cached
243
+ ``NodeNX`` instance when actual node objects are required.
244
+ """
245
+ return self.G.neighbors(self.n)
246
+
247
+ def has_edge(self, other: NodeProtocol) -> bool:
248
+ if isinstance(other, NodeNX):
188
249
  return self.G.has_edge(self.n, other.n)
189
250
  raise NotImplementedError
190
251
 
191
- def add_edge(self, other: NodoProtocol, weight: float) -> None:
192
- if isinstance(other, NodoNX):
193
- self.G.add_edge(self.n, other.n, weight=float(weight))
252
+ def add_edge(
253
+ self,
254
+ other: NodeProtocol,
255
+ weight: CouplingWeight,
256
+ *,
257
+ overwrite: bool = False,
258
+ ) -> None:
259
+ if isinstance(other, NodeNX):
260
+ add_edge(
261
+ self.G,
262
+ self.n,
263
+ other.n,
264
+ weight,
265
+ overwrite,
266
+ )
194
267
  else:
195
268
  raise NotImplementedError
196
269
 
197
270
  def offset(self) -> int:
198
- from .operators import _node_offset
199
- return _node_offset(self.G, self.n)
271
+ mapping = ensure_node_offset_map(self.G)
272
+ return mapping.get(self.n, 0)
273
+
274
+ def all_nodes(self) -> Iterable[NodeProtocol]:
275
+ override = self.graph.get("_all_nodes")
276
+ if override is not None:
277
+ return override
200
278
 
201
- def all_nodes(self) -> Iterable[NodoProtocol]:
202
- return [NodoNX(self.G, v) for v in self.G.nodes()]
279
+ nodes = cached_node_list(self.G)
280
+ return tuple(NodeNX.from_graph(self.G, v) for v in nodes)