tnfr 4.5.1__py3-none-any.whl → 4.5.2__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 (78) hide show
  1. tnfr/__init__.py +91 -90
  2. tnfr/alias.py +546 -0
  3. tnfr/cache.py +578 -0
  4. tnfr/callback_utils.py +388 -0
  5. tnfr/cli/__init__.py +75 -0
  6. tnfr/cli/arguments.py +177 -0
  7. tnfr/cli/execution.py +288 -0
  8. tnfr/cli/utils.py +36 -0
  9. tnfr/collections_utils.py +300 -0
  10. tnfr/config.py +19 -28
  11. tnfr/constants/__init__.py +174 -0
  12. tnfr/constants/core.py +159 -0
  13. tnfr/constants/init.py +31 -0
  14. tnfr/constants/metric.py +110 -0
  15. tnfr/constants_glyphs.py +98 -0
  16. tnfr/dynamics/__init__.py +658 -0
  17. tnfr/dynamics/dnfr.py +733 -0
  18. tnfr/dynamics/integrators.py +267 -0
  19. tnfr/dynamics/sampling.py +31 -0
  20. tnfr/execution.py +201 -0
  21. tnfr/flatten.py +283 -0
  22. tnfr/gamma.py +302 -88
  23. tnfr/glyph_history.py +290 -0
  24. tnfr/grammar.py +285 -96
  25. tnfr/graph_utils.py +84 -0
  26. tnfr/helpers/__init__.py +71 -0
  27. tnfr/helpers/numeric.py +87 -0
  28. tnfr/immutable.py +178 -0
  29. tnfr/import_utils.py +228 -0
  30. tnfr/initialization.py +197 -0
  31. tnfr/io.py +246 -0
  32. tnfr/json_utils.py +162 -0
  33. tnfr/locking.py +37 -0
  34. tnfr/logging_utils.py +116 -0
  35. tnfr/metrics/__init__.py +41 -0
  36. tnfr/metrics/coherence.py +829 -0
  37. tnfr/metrics/common.py +151 -0
  38. tnfr/metrics/core.py +101 -0
  39. tnfr/metrics/diagnosis.py +234 -0
  40. tnfr/metrics/export.py +137 -0
  41. tnfr/metrics/glyph_timing.py +189 -0
  42. tnfr/metrics/reporting.py +148 -0
  43. tnfr/metrics/sense_index.py +120 -0
  44. tnfr/metrics/trig.py +181 -0
  45. tnfr/metrics/trig_cache.py +109 -0
  46. tnfr/node.py +214 -159
  47. tnfr/observers.py +126 -136
  48. tnfr/ontosim.py +134 -134
  49. tnfr/operators/__init__.py +420 -0
  50. tnfr/operators/jitter.py +203 -0
  51. tnfr/operators/remesh.py +485 -0
  52. tnfr/presets.py +46 -14
  53. tnfr/rng.py +254 -0
  54. tnfr/selector.py +210 -0
  55. tnfr/sense.py +284 -131
  56. tnfr/structural.py +207 -79
  57. tnfr/tokens.py +60 -0
  58. tnfr/trace.py +329 -94
  59. tnfr/types.py +43 -17
  60. tnfr/validators.py +70 -24
  61. tnfr/value_utils.py +59 -0
  62. tnfr-4.5.2.dist-info/METADATA +379 -0
  63. tnfr-4.5.2.dist-info/RECORD +67 -0
  64. tnfr/cli.py +0 -322
  65. tnfr/constants.py +0 -277
  66. tnfr/dynamics.py +0 -814
  67. tnfr/helpers.py +0 -264
  68. tnfr/main.py +0 -47
  69. tnfr/metrics.py +0 -597
  70. tnfr/operators.py +0 -525
  71. tnfr/program.py +0 -176
  72. tnfr/scenarios.py +0 -34
  73. tnfr-4.5.1.dist-info/METADATA +0 -221
  74. tnfr-4.5.1.dist-info/RECORD +0 -28
  75. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
  76. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
  77. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
  78. {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,109 @@
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_attr
14
+ from ..constants import get_aliases
15
+ from ..cache import edge_version_cache
16
+ from ..import_utils import get_numpy
17
+ from ..types import GraphLike
18
+
19
+ ALIAS_THETA = get_aliases("THETA")
20
+
21
+ __all__ = ("TrigCache", "compute_theta_trig", "get_trig_cache", "_compute_trig_python")
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class TrigCache:
26
+ """Container for cached trigonometric values per node."""
27
+
28
+ cos: dict[Any, float]
29
+ sin: dict[Any, float]
30
+ theta: dict[Any, float]
31
+
32
+
33
+ def _iter_theta_pairs(
34
+ nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
35
+ ) -> Iterable[tuple[Any, float]]:
36
+ """Yield ``(node, θ)`` pairs from ``nodes``."""
37
+
38
+ for n, data in nodes:
39
+ if isinstance(data, Mapping):
40
+ yield n, get_attr(data, ALIAS_THETA, 0.0)
41
+ else:
42
+ yield n, float(data)
43
+
44
+
45
+ def _compute_trig_python(
46
+ nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
47
+ ) -> TrigCache:
48
+ """Compute trigonometric mappings using pure Python."""
49
+
50
+ cos_th: dict[Any, float] = {}
51
+ sin_th: dict[Any, float] = {}
52
+ thetas: dict[Any, float] = {}
53
+ for n, th in _iter_theta_pairs(nodes):
54
+ thetas[n] = th
55
+ cos_th[n] = math.cos(th)
56
+ sin_th[n] = math.sin(th)
57
+ return TrigCache(cos=cos_th, sin=sin_th, theta=thetas)
58
+
59
+
60
+ def compute_theta_trig(
61
+ nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
62
+ np: Any | None = None,
63
+ ) -> TrigCache:
64
+ """Return trigonometric mappings of ``θ`` per node."""
65
+
66
+ if np is None:
67
+ np = get_numpy()
68
+ if np is None or not all(hasattr(np, attr) for attr in ("fromiter", "cos", "sin")):
69
+ return _compute_trig_python(nodes)
70
+
71
+ pairs = list(_iter_theta_pairs(nodes))
72
+ if not pairs:
73
+ return TrigCache(cos={}, sin={}, theta={})
74
+
75
+ node_list, theta_vals = zip(*pairs)
76
+ theta_arr = np.fromiter(theta_vals, dtype=float)
77
+ cos_arr = np.cos(theta_arr)
78
+ sin_arr = np.sin(theta_arr)
79
+
80
+ cos_th = dict(zip(node_list, map(float, cos_arr)))
81
+ sin_th = dict(zip(node_list, map(float, sin_arr)))
82
+ thetas = dict(zip(node_list, map(float, theta_arr)))
83
+ return TrigCache(cos=cos_th, sin=sin_th, theta=thetas)
84
+
85
+
86
+ def _build_trig_cache(G: GraphLike, np: Any | None = None) -> TrigCache:
87
+ """Construct trigonometric cache for ``G``."""
88
+
89
+ return compute_theta_trig(G.nodes(data=True), np=np)
90
+
91
+
92
+ def get_trig_cache(
93
+ G: GraphLike,
94
+ *,
95
+ np: Any | None = None,
96
+ cache_size: int | None = 128,
97
+ ) -> TrigCache:
98
+ """Return cached cosines and sines of ``θ`` per node."""
99
+
100
+ if np is None:
101
+ np = get_numpy()
102
+ version = G.graph.setdefault("_trig_version", 0)
103
+ key = ("_trig", version)
104
+ return edge_version_cache(
105
+ G,
106
+ key,
107
+ lambda: _build_trig_cache(G, np=np),
108
+ max_entries=cache_size,
109
+ )
tnfr/node.py CHANGED
@@ -1,202 +1,257 @@
1
+ """Node utilities and structures for TNFR graphs."""
2
+
1
3
  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
4
+ from typing import Iterable, MutableMapping, Optional, Protocol, TypeVar
5
+ from collections.abc import Hashable
6
+ import math
7
+
8
+ from .constants import get_aliases
9
+ from .alias import (
10
+ get_attr,
11
+ get_attr_str,
12
+ set_attr,
13
+ set_attr_str,
14
+ set_vf,
15
+ set_dnfr,
16
+ set_theta,
17
+ )
18
+ from .cache import (
19
+ cached_node_list,
20
+ ensure_node_offset_map,
21
+ increment_edge_version,
22
+ )
23
+ from .graph_utils import supports_add_edge
24
+ from .locking import get_lock
25
+
26
+ ALIAS_EPI = get_aliases("EPI")
27
+ ALIAS_VF = get_aliases("VF")
28
+ ALIAS_THETA = get_aliases("THETA")
29
+ ALIAS_SI = get_aliases("SI")
30
+ ALIAS_EPI_KIND = get_aliases("EPI_KIND")
31
+ ALIAS_DNFR = get_aliases("DNFR")
32
+ ALIAS_D2EPI = get_aliases("D2EPI")
33
+
34
+ # Mapping of NodoNX attribute specifications used to generate property
35
+ # descriptors. Each entry defines the keyword arguments passed to
36
+ # ``_nx_attr_property`` for a given attribute name.
37
+ ATTR_SPECS: dict[str, dict] = {
38
+ "EPI": {"aliases": ALIAS_EPI},
39
+ "vf": {
40
+ "aliases": ALIAS_VF,
41
+ "setter": set_vf,
42
+ "use_graph_setter": True,
43
+ },
44
+ "theta": {
45
+ "aliases": ALIAS_THETA,
46
+ "setter": set_theta,
47
+ "use_graph_setter": True,
48
+ },
49
+ "Si": {"aliases": ALIAS_SI},
50
+ "epi_kind": {
51
+ "aliases": ALIAS_EPI_KIND,
52
+ "default": "",
53
+ "getter": get_attr_str,
54
+ "setter": set_attr_str,
55
+ "to_python": str,
56
+ "to_storage": str,
57
+ },
58
+ "dnfr": {
59
+ "aliases": ALIAS_DNFR,
60
+ "setter": set_dnfr,
61
+ "use_graph_setter": True,
62
+ },
63
+ "d2EPI": {"aliases": ALIAS_D2EPI},
64
+ }
65
+
66
+ T = TypeVar("T")
67
+
68
+ __all__ = ("NodoNX", "NodoProtocol", "add_edge")
69
+
70
+
71
+ def _nx_attr_property(
72
+ aliases: tuple[str, ...],
73
+ *,
74
+ default=0.0,
75
+ getter=get_attr,
76
+ setter=set_attr,
77
+ to_python=float,
78
+ to_storage=float,
79
+ use_graph_setter=False,
80
+ ):
81
+ """Generate ``NodoNX`` property descriptors.
82
+
83
+ Parameters
84
+ ----------
85
+ aliases:
86
+ Immutable tuple of aliases used to access the attribute in the
87
+ underlying ``networkx`` node.
88
+ default:
89
+ Value returned when the attribute is missing.
90
+ getter, setter:
91
+ Helper functions used to retrieve or store the value. ``setter`` can
92
+ either accept ``(mapping, aliases, value)`` or, when
93
+ ``use_graph_setter`` is ``True``, ``(G, n, value)``.
94
+ to_python, to_storage:
95
+ Conversion helpers applied when getting or setting the value,
96
+ respectively.
97
+ use_graph_setter:
98
+ Whether ``setter`` expects ``(G, n, value)`` instead of
99
+ ``(mapping, aliases, value)``.
100
+ """
101
+
102
+ def fget(self) -> T:
103
+ return to_python(getter(self.G.nodes[self.n], aliases, default))
104
+
105
+ def fset(self, value: T) -> None:
106
+ value = to_storage(value)
107
+ if use_graph_setter:
108
+ setter(self.G, self.n, value)
109
+ else:
110
+ setter(self.G.nodes[self.n], aliases, value)
5
111
 
6
- from .constants import DEFAULTS
7
- from .helpers import push_glifo
112
+ return property(fget, fset)
8
113
 
9
114
 
10
- class NodoProtocol(Protocol):
11
- """Protocolo mínimo para nodos TNFR."""
115
+ def _add_edge_common(n1, n2, weight) -> Optional[float]:
116
+ """Validate basic edge constraints.
12
117
 
13
- EPI: float
14
- vf: float
15
- theta: float
16
- Si: float
17
- epi_kind: str
18
- dnfr: float
19
- d2EPI: float
20
- graph: Dict[str, object]
118
+ Returns the parsed weight if the edge can be added. ``None`` is returned
119
+ when the edge should be ignored (e.g. self-connections).
120
+ """
21
121
 
22
- def neighbors(self) -> Iterable["NodoProtocol"]:
23
- ...
122
+ if n1 == n2:
123
+ return None
24
124
 
25
- def push_glifo(self, glifo: str, window: int) -> None:
26
- ...
125
+ weight = float(weight)
126
+ if not math.isfinite(weight):
127
+ raise ValueError("Edge weight must be a finite number")
128
+ if weight < 0:
129
+ raise ValueError("Edge weight must be non-negative")
27
130
 
28
- def has_edge(self, other: "NodoProtocol") -> bool:
29
- ...
131
+ return weight
30
132
 
31
- def add_edge(self, other: "NodoProtocol", weight: float) -> None:
32
- ...
33
133
 
34
- def offset(self) -> int:
35
- ...
134
+ def add_edge(
135
+ graph,
136
+ n1,
137
+ n2,
138
+ weight,
139
+ overwrite: bool = False,
140
+ ):
141
+ """Add an edge between ``n1`` and ``n2`` in a ``networkx`` graph."""
36
142
 
37
- def all_nodes(self) -> Iterable["NodoProtocol"]:
38
- ...
143
+ weight = _add_edge_common(n1, n2, weight)
144
+ if weight is None:
145
+ return
39
146
 
147
+ if not supports_add_edge(graph):
148
+ raise TypeError("add_edge only supports networkx graphs")
40
149
 
41
- @dataclass
42
- class NodoTNFR:
43
- """Representa un nodo TNFR autónomo."""
150
+ if graph.has_edge(n1, n2) and not overwrite:
151
+ return
44
152
 
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)))
153
+ graph.add_edge(n1, n2, weight=weight)
154
+ increment_edge_version(graph)
55
155
 
56
- def neighbors(self) -> Iterable["NodoTNFR"]:
57
- return list(self._neighbors)
58
156
 
59
- def has_edge(self, other: "NodoTNFR") -> bool:
60
- return other in self._neighbors
157
+ class NodoProtocol(Protocol):
158
+ """Minimal protocol for TNFR nodes."""
61
159
 
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)
160
+ EPI: float
161
+ vf: float
162
+ theta: float
163
+ Si: float
164
+ epi_kind: str
165
+ dnfr: float
166
+ d2EPI: float
167
+ graph: dict[str, object]
66
168
 
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
169
+ def neighbors(self) -> Iterable[NodoProtocol | Hashable]: ...
72
170
 
73
- def offset(self) -> int:
74
- return 0
171
+ def _glyph_storage(self) -> MutableMapping[str, object]: ...
172
+
173
+ def has_edge(self, other: "NodoProtocol") -> bool: ...
75
174
 
76
- def all_nodes(self) -> Iterable["NodoTNFR"]:
77
- return list(getattr(self.graph, "_all_nodes", [self]))
175
+ def add_edge(
176
+ self, other: "NodoProtocol", weight: float, *, overwrite: bool = False
177
+ ) -> None: ...
78
178
 
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)
179
+ def offset(self) -> int: ...
82
180
 
83
- def integrar(self, dt: float) -> None:
84
- self.EPI += self.dnfr * dt
181
+ def all_nodes(self) -> Iterable["NodoProtocol"]: ...
85
182
 
86
183
 
87
184
  class NodoNX(NodoProtocol):
88
- """Adaptador para nodos ``networkx``."""
185
+ """Adapter for ``networkx`` nodes."""
186
+
187
+ # Statically defined property descriptors for ``NodoNX`` attributes.
188
+ # Declaring them here makes the attributes discoverable by type checkers
189
+ # and IDEs, avoiding the previous runtime ``setattr`` loop.
190
+ EPI: float = _nx_attr_property(**ATTR_SPECS["EPI"])
191
+ vf: float = _nx_attr_property(**ATTR_SPECS["vf"])
192
+ theta: float = _nx_attr_property(**ATTR_SPECS["theta"])
193
+ Si: float = _nx_attr_property(**ATTR_SPECS["Si"])
194
+ epi_kind: str = _nx_attr_property(**ATTR_SPECS["epi_kind"])
195
+ dnfr: float = _nx_attr_property(**ATTR_SPECS["dnfr"])
196
+ d2EPI: float = _nx_attr_property(**ATTR_SPECS["d2EPI"])
89
197
 
90
198
  def __init__(self, G, n):
91
199
  self.G = G
92
200
  self.n = n
93
201
  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
202
+ G.graph.setdefault("_node_cache", {})[n] = self
203
+
204
+ def _glyph_storage(self):
205
+ return self.G.nodes[self.n]
206
+
207
+ @classmethod
208
+ def from_graph(cls, G, n):
209
+ """Return cached ``NodoNX`` for ``(G, n)`` with thread safety."""
210
+ lock = get_lock(f"nodonx_cache_{id(G)}")
211
+ with lock:
212
+ cache = G.graph.setdefault("_node_cache", {})
213
+ node = cache.get(n)
214
+ if node is None:
215
+ node = cls(G, n)
216
+ return node
217
+
218
+ def neighbors(self) -> Iterable[Hashable]:
219
+ """Iterate neighbour identifiers (IDs).
220
+
221
+ Wrap each resulting ID with :meth:`from_graph` to obtain the cached
222
+ ``NodoNX`` instance when actual node objects are required.
223
+ """
224
+ return self.G.neighbors(self.n)
185
225
 
186
226
  def has_edge(self, other: NodoProtocol) -> bool:
187
227
  if isinstance(other, NodoNX):
188
228
  return self.G.has_edge(self.n, other.n)
189
229
  raise NotImplementedError
190
230
 
191
- def add_edge(self, other: NodoProtocol, weight: float) -> None:
231
+ def add_edge(
232
+ self, other: NodoProtocol, weight: float, *, overwrite: bool = False
233
+ ) -> None:
192
234
  if isinstance(other, NodoNX):
193
- self.G.add_edge(self.n, other.n, weight=float(weight))
235
+ add_edge(
236
+ self.G,
237
+ self.n,
238
+ other.n,
239
+ weight,
240
+ overwrite,
241
+ )
194
242
  else:
195
243
  raise NotImplementedError
196
244
 
197
245
  def offset(self) -> int:
198
- from .operators import _node_offset
199
- return _node_offset(self.G, self.n)
246
+ mapping = ensure_node_offset_map(self.G)
247
+ return mapping.get(self.n, 0)
200
248
 
201
249
  def all_nodes(self) -> Iterable[NodoProtocol]:
202
- return [NodoNX(self.G, v) for v in self.G.nodes()]
250
+ override = self.graph.get("_all_nodes")
251
+ if override is not None:
252
+ return override
253
+
254
+ nodes = cached_node_list(self.G)
255
+ return tuple(NodoNX.from_graph(self.G, v) for v in nodes)
256
+
257
+