tnfr 6.0.0__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 (176) hide show
  1. tnfr/__init__.py +50 -5
  2. tnfr/__init__.pyi +0 -7
  3. tnfr/_compat.py +0 -1
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +44 -2
  6. tnfr/alias.py +14 -13
  7. tnfr/alias.pyi +5 -37
  8. tnfr/cache.py +9 -729
  9. tnfr/cache.pyi +8 -224
  10. tnfr/callback_utils.py +16 -31
  11. tnfr/callback_utils.pyi +3 -29
  12. tnfr/cli/__init__.py +17 -11
  13. tnfr/cli/__init__.pyi +0 -21
  14. tnfr/cli/arguments.py +175 -14
  15. tnfr/cli/arguments.pyi +5 -11
  16. tnfr/cli/execution.py +434 -48
  17. tnfr/cli/execution.pyi +14 -24
  18. tnfr/cli/utils.py +20 -3
  19. tnfr/cli/utils.pyi +5 -5
  20. tnfr/config/__init__.py +2 -1
  21. tnfr/config/__init__.pyi +2 -0
  22. tnfr/config/feature_flags.py +83 -0
  23. tnfr/config/init.py +1 -1
  24. tnfr/config/operator_names.py +1 -14
  25. tnfr/config/presets.py +6 -26
  26. tnfr/constants/__init__.py +10 -13
  27. tnfr/constants/__init__.pyi +10 -22
  28. tnfr/constants/aliases.py +31 -0
  29. tnfr/constants/core.py +4 -3
  30. tnfr/constants/init.py +1 -1
  31. tnfr/constants/metric.py +3 -3
  32. tnfr/dynamics/__init__.py +64 -10
  33. tnfr/dynamics/__init__.pyi +3 -4
  34. tnfr/dynamics/adaptation.py +79 -13
  35. tnfr/dynamics/aliases.py +10 -9
  36. tnfr/dynamics/coordination.py +77 -35
  37. tnfr/dynamics/dnfr.py +575 -274
  38. tnfr/dynamics/dnfr.pyi +1 -10
  39. tnfr/dynamics/integrators.py +47 -33
  40. tnfr/dynamics/integrators.pyi +0 -1
  41. tnfr/dynamics/runtime.py +489 -129
  42. tnfr/dynamics/sampling.py +2 -0
  43. tnfr/dynamics/selectors.py +101 -62
  44. tnfr/execution.py +15 -8
  45. tnfr/execution.pyi +5 -25
  46. tnfr/flatten.py +7 -3
  47. tnfr/flatten.pyi +1 -8
  48. tnfr/gamma.py +22 -26
  49. tnfr/gamma.pyi +0 -6
  50. tnfr/glyph_history.py +37 -26
  51. tnfr/glyph_history.pyi +1 -19
  52. tnfr/glyph_runtime.py +16 -0
  53. tnfr/glyph_runtime.pyi +9 -0
  54. tnfr/immutable.py +20 -15
  55. tnfr/immutable.pyi +4 -7
  56. tnfr/initialization.py +5 -7
  57. tnfr/initialization.pyi +1 -9
  58. tnfr/io.py +6 -305
  59. tnfr/io.pyi +13 -8
  60. tnfr/mathematics/__init__.py +81 -0
  61. tnfr/mathematics/backend.py +426 -0
  62. tnfr/mathematics/dynamics.py +398 -0
  63. tnfr/mathematics/epi.py +254 -0
  64. tnfr/mathematics/generators.py +222 -0
  65. tnfr/mathematics/metrics.py +119 -0
  66. tnfr/mathematics/operators.py +233 -0
  67. tnfr/mathematics/operators_factory.py +71 -0
  68. tnfr/mathematics/projection.py +78 -0
  69. tnfr/mathematics/runtime.py +173 -0
  70. tnfr/mathematics/spaces.py +247 -0
  71. tnfr/mathematics/transforms.py +292 -0
  72. tnfr/metrics/__init__.py +10 -10
  73. tnfr/metrics/coherence.py +123 -94
  74. tnfr/metrics/common.py +22 -13
  75. tnfr/metrics/common.pyi +42 -11
  76. tnfr/metrics/core.py +72 -14
  77. tnfr/metrics/diagnosis.py +48 -57
  78. tnfr/metrics/diagnosis.pyi +3 -7
  79. tnfr/metrics/export.py +3 -5
  80. tnfr/metrics/glyph_timing.py +41 -31
  81. tnfr/metrics/reporting.py +13 -6
  82. tnfr/metrics/sense_index.py +884 -114
  83. tnfr/metrics/trig.py +167 -11
  84. tnfr/metrics/trig.pyi +1 -0
  85. tnfr/metrics/trig_cache.py +112 -15
  86. tnfr/node.py +400 -17
  87. tnfr/node.pyi +55 -38
  88. tnfr/observers.py +111 -8
  89. tnfr/observers.pyi +0 -15
  90. tnfr/ontosim.py +9 -6
  91. tnfr/ontosim.pyi +0 -5
  92. tnfr/operators/__init__.py +529 -42
  93. tnfr/operators/__init__.pyi +14 -0
  94. tnfr/operators/definitions.py +350 -18
  95. tnfr/operators/definitions.pyi +0 -14
  96. tnfr/operators/grammar.py +760 -0
  97. tnfr/operators/jitter.py +28 -22
  98. tnfr/operators/registry.py +7 -12
  99. tnfr/operators/registry.pyi +0 -2
  100. tnfr/operators/remesh.py +38 -61
  101. tnfr/rng.py +17 -300
  102. tnfr/schemas/__init__.py +8 -0
  103. tnfr/schemas/grammar.json +94 -0
  104. tnfr/selector.py +3 -4
  105. tnfr/selector.pyi +1 -1
  106. tnfr/sense.py +22 -24
  107. tnfr/sense.pyi +0 -7
  108. tnfr/structural.py +504 -21
  109. tnfr/structural.pyi +41 -18
  110. tnfr/telemetry/__init__.py +23 -1
  111. tnfr/telemetry/cache_metrics.py +226 -0
  112. tnfr/telemetry/nu_f.py +423 -0
  113. tnfr/telemetry/nu_f.pyi +123 -0
  114. tnfr/tokens.py +1 -4
  115. tnfr/tokens.pyi +1 -6
  116. tnfr/trace.py +20 -53
  117. tnfr/trace.pyi +9 -37
  118. tnfr/types.py +244 -15
  119. tnfr/types.pyi +200 -14
  120. tnfr/units.py +69 -0
  121. tnfr/units.pyi +16 -0
  122. tnfr/utils/__init__.py +107 -48
  123. tnfr/utils/__init__.pyi +80 -11
  124. tnfr/utils/cache.py +1705 -65
  125. tnfr/utils/cache.pyi +370 -58
  126. tnfr/utils/chunks.py +104 -0
  127. tnfr/utils/chunks.pyi +21 -0
  128. tnfr/utils/data.py +95 -5
  129. tnfr/utils/data.pyi +8 -17
  130. tnfr/utils/graph.py +2 -4
  131. tnfr/utils/init.py +31 -7
  132. tnfr/utils/init.pyi +4 -11
  133. tnfr/utils/io.py +313 -14
  134. tnfr/{helpers → utils}/numeric.py +50 -24
  135. tnfr/utils/numeric.pyi +21 -0
  136. tnfr/validation/__init__.py +92 -4
  137. tnfr/validation/__init__.pyi +77 -17
  138. tnfr/validation/compatibility.py +79 -43
  139. tnfr/validation/compatibility.pyi +4 -6
  140. tnfr/validation/grammar.py +55 -133
  141. tnfr/validation/grammar.pyi +37 -8
  142. tnfr/validation/graph.py +138 -0
  143. tnfr/validation/graph.pyi +17 -0
  144. tnfr/validation/rules.py +161 -74
  145. tnfr/validation/rules.pyi +55 -18
  146. tnfr/validation/runtime.py +263 -0
  147. tnfr/validation/runtime.pyi +31 -0
  148. tnfr/validation/soft_filters.py +170 -0
  149. tnfr/validation/soft_filters.pyi +37 -0
  150. tnfr/validation/spectral.py +159 -0
  151. tnfr/validation/spectral.pyi +46 -0
  152. tnfr/validation/syntax.py +28 -139
  153. tnfr/validation/syntax.pyi +7 -4
  154. tnfr/validation/window.py +39 -0
  155. tnfr/validation/window.pyi +1 -0
  156. tnfr/viz/__init__.py +9 -0
  157. tnfr/viz/matplotlib.py +246 -0
  158. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
  159. tnfr-7.0.0.dist-info/RECORD +185 -0
  160. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  161. tnfr/constants_glyphs.py +0 -16
  162. tnfr/constants_glyphs.pyi +0 -12
  163. tnfr/grammar.py +0 -25
  164. tnfr/grammar.pyi +0 -13
  165. tnfr/helpers/__init__.py +0 -151
  166. tnfr/helpers/__init__.pyi +0 -66
  167. tnfr/helpers/numeric.pyi +0 -12
  168. tnfr/presets.py +0 -15
  169. tnfr/presets.pyi +0 -7
  170. tnfr/utils/io.pyi +0 -10
  171. tnfr/utils/validators.py +0 -130
  172. tnfr/utils/validators.pyi +0 -19
  173. tnfr-6.0.0.dist-info/RECORD +0 -157
  174. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  175. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  176. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/metrics/trig.py CHANGED
@@ -9,11 +9,11 @@ from __future__ import annotations
9
9
  import math
10
10
  from collections.abc import Iterable, Iterator, Sequence
11
11
  from itertools import tee
12
- from typing import TYPE_CHECKING, Any, overload, cast
12
+ from typing import TYPE_CHECKING, Any, cast, overload
13
13
 
14
- from ..helpers.numeric import kahan_sum_nd
15
- from ..utils import cached_import, get_numpy
14
+ from ..utils import kahan_sum_nd
16
15
  from ..types import NodeId, Phase, TNFRGraph
16
+ from ..utils import cached_import, get_numpy
17
17
 
18
18
  if TYPE_CHECKING: # pragma: no cover - typing only
19
19
  from ..node import NodeProtocol
@@ -23,6 +23,7 @@ __all__ = (
23
23
  "_phase_mean_from_iter",
24
24
  "_neighbor_phase_mean_core",
25
25
  "_neighbor_phase_mean_generic",
26
+ "neighbor_phase_mean_bulk",
26
27
  "neighbor_phase_mean_list",
27
28
  "neighbor_phase_mean",
28
29
  )
@@ -123,7 +124,7 @@ def _neighbor_phase_mean_generic(
123
124
  np: Any | None = None,
124
125
  fallback: float = 0.0,
125
126
  ) -> float:
126
- """Internal helper delegating to :func:`_neighbor_phase_mean_core`.
127
+ """Compute the neighbour phase mean via :func:`_neighbor_phase_mean_core`.
127
128
 
128
129
  ``obj`` may be either a node bound to a graph or a sequence of neighbours.
129
130
  When ``cos_map`` and ``sin_map`` are ``None`` the function assumes ``obj`` is
@@ -138,9 +139,7 @@ def _neighbor_phase_mean_generic(
138
139
  if cos_map is None or sin_map is None:
139
140
  node = cast("NodeProtocol", obj)
140
141
  if getattr(node, "G", None) is None:
141
- raise TypeError(
142
- "neighbor_phase_mean requires nodes bound to a graph"
143
- )
142
+ raise TypeError("neighbor_phase_mean requires nodes bound to a graph")
144
143
  from .trig_cache import get_trig_cache
145
144
 
146
145
  trig = get_trig_cache(node.G)
@@ -172,14 +171,171 @@ def neighbor_phase_mean_list(
172
171
  )
173
172
 
174
173
 
174
+ def neighbor_phase_mean_bulk(
175
+ edge_src: Any,
176
+ edge_dst: Any,
177
+ *,
178
+ cos_values: Any,
179
+ sin_values: Any,
180
+ theta_values: Any,
181
+ node_count: int,
182
+ np: Any,
183
+ neighbor_cos_sum: Any | None = None,
184
+ neighbor_sin_sum: Any | None = None,
185
+ neighbor_counts: Any | None = None,
186
+ mean_cos: Any | None = None,
187
+ mean_sin: Any | None = None,
188
+ ) -> tuple[Any, Any]:
189
+ """Vectorised neighbour phase means for all nodes in a graph.
190
+
191
+ Parameters
192
+ ----------
193
+ edge_src, edge_dst:
194
+ Arrays describing the source (neighbour) and destination (node) indices
195
+ for each edge contribution. They must have matching shapes.
196
+ cos_values, sin_values:
197
+ Arrays containing the cosine and sine values of each node's phase. The
198
+ arrays must be indexed using the same positional indices referenced by
199
+ ``edge_src``.
200
+ theta_values:
201
+ Array with the baseline phase for each node. Positions that do not have
202
+ neighbours reuse this baseline as their mean phase.
203
+ node_count:
204
+ Total number of nodes represented in ``theta_values``.
205
+ np:
206
+ Numpy module used to materialise the vectorised operations.
207
+
208
+ Optional buffers
209
+ -----------------
210
+ neighbor_cos_sum, neighbor_sin_sum, neighbor_counts, mean_cos, mean_sin:
211
+ Preallocated arrays sized ``node_count`` reused to accumulate the
212
+ neighbour cosine/sine sums, neighbour sample counts, and the averaged
213
+ cosine/sine vectors. When omitted, the helper materialises fresh
214
+ buffers that match the previous semantics.
215
+
216
+ Returns
217
+ -------
218
+ tuple[Any, Any]
219
+ Tuple ``(mean_theta, has_neighbors)`` where ``mean_theta`` contains the
220
+ circular mean of neighbour phases for every node and ``has_neighbors``
221
+ is a boolean mask identifying which nodes contributed at least one
222
+ neighbour sample.
223
+ """
224
+
225
+ if node_count <= 0:
226
+ empty_mean = np.zeros(0, dtype=float)
227
+ return empty_mean, empty_mean.astype(bool)
228
+
229
+ edge_src_arr = np.asarray(edge_src, dtype=np.intp)
230
+ edge_dst_arr = np.asarray(edge_dst, dtype=np.intp)
231
+
232
+ if edge_src_arr.shape != edge_dst_arr.shape:
233
+ raise ValueError("edge_src and edge_dst must share the same shape")
234
+
235
+ theta_arr = np.asarray(theta_values, dtype=float)
236
+ if theta_arr.ndim != 1 or theta_arr.size != node_count:
237
+ raise ValueError("theta_values must be a 1-D array matching node_count")
238
+
239
+ cos_arr = np.asarray(cos_values, dtype=float)
240
+ sin_arr = np.asarray(sin_values, dtype=float)
241
+ if cos_arr.ndim != 1 or cos_arr.size != node_count:
242
+ raise ValueError("cos_values must be a 1-D array matching node_count")
243
+ if sin_arr.ndim != 1 or sin_arr.size != node_count:
244
+ raise ValueError("sin_values must be a 1-D array matching node_count")
245
+
246
+ edge_count = edge_dst_arr.size
247
+ def _coerce_buffer(buffer: Any | None, *, name: str) -> tuple[Any, bool]:
248
+ if buffer is None:
249
+ return None, False
250
+ arr = np.array(buffer, dtype=float, copy=False)
251
+ if arr.ndim != 1 or arr.size != node_count:
252
+ raise ValueError(f"{name} must be a 1-D array sized node_count")
253
+ arr.fill(0.0)
254
+ return arr, True
255
+
256
+ neighbor_cos_sum, has_cos_buffer = _coerce_buffer(
257
+ neighbor_cos_sum, name="neighbor_cos_sum"
258
+ )
259
+ neighbor_sin_sum, has_sin_buffer = _coerce_buffer(
260
+ neighbor_sin_sum, name="neighbor_sin_sum"
261
+ )
262
+ neighbor_counts, has_count_buffer = _coerce_buffer(
263
+ neighbor_counts, name="neighbor_counts"
264
+ )
265
+
266
+ if edge_count:
267
+ cos_bincount = np.bincount(
268
+ edge_dst_arr,
269
+ weights=cos_arr[edge_src_arr],
270
+ minlength=node_count,
271
+ )
272
+ sin_bincount = np.bincount(
273
+ edge_dst_arr,
274
+ weights=sin_arr[edge_src_arr],
275
+ minlength=node_count,
276
+ )
277
+ count_bincount = np.bincount(
278
+ edge_dst_arr,
279
+ minlength=node_count,
280
+ ).astype(float, copy=False)
281
+
282
+ if not has_cos_buffer:
283
+ neighbor_cos_sum = cos_bincount
284
+ else:
285
+ np.copyto(neighbor_cos_sum, cos_bincount)
286
+
287
+ if not has_sin_buffer:
288
+ neighbor_sin_sum = sin_bincount
289
+ else:
290
+ np.copyto(neighbor_sin_sum, sin_bincount)
291
+
292
+ if not has_count_buffer:
293
+ neighbor_counts = count_bincount
294
+ else:
295
+ np.copyto(neighbor_counts, count_bincount)
296
+ else:
297
+ if neighbor_cos_sum is None:
298
+ neighbor_cos_sum = np.zeros(node_count, dtype=float)
299
+ if neighbor_sin_sum is None:
300
+ neighbor_sin_sum = np.zeros(node_count, dtype=float)
301
+ if neighbor_counts is None:
302
+ neighbor_counts = np.zeros(node_count, dtype=float)
303
+
304
+ has_neighbors = neighbor_counts > 0.0
305
+
306
+ mean_cos, _ = _coerce_buffer(mean_cos, name="mean_cos")
307
+ mean_sin, _ = _coerce_buffer(mean_sin, name="mean_sin")
308
+
309
+ if mean_cos is None:
310
+ mean_cos = np.zeros(node_count, dtype=float)
311
+ if mean_sin is None:
312
+ mean_sin = np.zeros(node_count, dtype=float)
313
+
314
+ if edge_count:
315
+ with np.errstate(divide="ignore", invalid="ignore"):
316
+ np.divide(
317
+ neighbor_cos_sum,
318
+ neighbor_counts,
319
+ out=mean_cos,
320
+ where=has_neighbors,
321
+ )
322
+ np.divide(
323
+ neighbor_sin_sum,
324
+ neighbor_counts,
325
+ out=mean_sin,
326
+ where=has_neighbors,
327
+ )
328
+
329
+ mean_theta = np.where(has_neighbors, np.arctan2(mean_sin, mean_cos), theta_arr)
330
+ return mean_theta, has_neighbors
331
+
332
+
175
333
  @overload
176
- def neighbor_phase_mean(obj: "NodeProtocol", n: None = ...) -> Phase:
177
- ...
334
+ def neighbor_phase_mean(obj: "NodeProtocol", n: None = ...) -> Phase: ...
178
335
 
179
336
 
180
337
  @overload
181
- def neighbor_phase_mean(obj: TNFRGraph, n: NodeId) -> Phase:
182
- ...
338
+ def neighbor_phase_mean(obj: TNFRGraph, n: NodeId) -> Phase: ...
183
339
 
184
340
 
185
341
  def neighbor_phase_mean(
tnfr/metrics/trig.pyi CHANGED
@@ -9,4 +9,5 @@ _neighbor_phase_mean_generic: Any
9
9
  _phase_mean_from_iter: Any
10
10
  accumulate_cos_sin: Any
11
11
  neighbor_phase_mean: Any
12
+ neighbor_phase_mean_bulk: Any
12
13
  neighbor_phase_mean_list: Any
@@ -6,12 +6,14 @@ focused on pure mathematical utilities (phase means, compensated sums, etc.).
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import hashlib
9
10
  import math
11
+ import struct
10
12
  from dataclasses import dataclass
11
13
  from typing import Any, Iterable, Mapping
12
14
 
13
15
  from ..alias import get_theta_attr
14
- from ..types import GraphLike
16
+ from ..types import GraphLike, NodeAttrMap
15
17
  from ..utils import edge_version_cache, get_numpy
16
18
 
17
19
  __all__ = ("TrigCache", "compute_theta_trig", "get_trig_cache", "_compute_trig_python")
@@ -24,10 +26,18 @@ class TrigCache:
24
26
  cos: dict[Any, float]
25
27
  sin: dict[Any, float]
26
28
  theta: dict[Any, float]
29
+ theta_checksums: dict[Any, bytes]
30
+ order: tuple[Any, ...]
31
+ cos_values: Any
32
+ sin_values: Any
33
+ theta_values: Any
34
+ index: dict[Any, int]
35
+ edge_src: Any | None = None
36
+ edge_dst: Any | None = None
27
37
 
28
38
 
29
39
  def _iter_theta_pairs(
30
- nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
40
+ nodes: Iterable[tuple[Any, NodeAttrMap | float]],
31
41
  ) -> Iterable[tuple[Any, float]]:
32
42
  """Yield ``(node, θ)`` pairs from ``nodes``."""
33
43
 
@@ -39,22 +49,48 @@ def _iter_theta_pairs(
39
49
 
40
50
 
41
51
  def _compute_trig_python(
42
- nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
52
+ nodes: Iterable[tuple[Any, NodeAttrMap | float]],
43
53
  ) -> TrigCache:
44
54
  """Compute trigonometric mappings using pure Python."""
45
55
 
56
+ pairs = list(_iter_theta_pairs(nodes))
57
+
46
58
  cos_th: dict[Any, float] = {}
47
59
  sin_th: dict[Any, float] = {}
48
60
  thetas: dict[Any, float] = {}
49
- for n, th in _iter_theta_pairs(nodes):
61
+ theta_checksums: dict[Any, bytes] = {}
62
+ order_list: list[Any] = []
63
+
64
+ for n, th in pairs:
65
+ order_list.append(n)
50
66
  thetas[n] = th
51
67
  cos_th[n] = math.cos(th)
52
68
  sin_th[n] = math.sin(th)
53
- return TrigCache(cos=cos_th, sin=sin_th, theta=thetas)
69
+ theta_checksums[n] = _theta_checksum(th)
70
+
71
+ order = tuple(order_list)
72
+ cos_values = tuple(cos_th[n] for n in order)
73
+ sin_values = tuple(sin_th[n] for n in order)
74
+ theta_values = tuple(thetas[n] for n in order)
75
+ index = {n: i for i, n in enumerate(order)}
76
+
77
+ return TrigCache(
78
+ cos=cos_th,
79
+ sin=sin_th,
80
+ theta=thetas,
81
+ theta_checksums=theta_checksums,
82
+ order=order,
83
+ cos_values=cos_values,
84
+ sin_values=sin_values,
85
+ theta_values=theta_values,
86
+ index=index,
87
+ edge_src=None,
88
+ edge_dst=None,
89
+ )
54
90
 
55
91
 
56
92
  def compute_theta_trig(
57
- nodes: Iterable[tuple[Any, Mapping[str, Any] | float]],
93
+ nodes: Iterable[tuple[Any, NodeAttrMap | float]],
58
94
  np: Any | None = None,
59
95
  ) -> TrigCache:
60
96
  """Return trigonometric mappings of ``θ`` per node."""
@@ -66,9 +102,22 @@ def compute_theta_trig(
66
102
 
67
103
  pairs = list(_iter_theta_pairs(nodes))
68
104
  if not pairs:
69
- return TrigCache(cos={}, sin={}, theta={})
105
+ return TrigCache(
106
+ cos={},
107
+ sin={},
108
+ theta={},
109
+ theta_checksums={},
110
+ order=(),
111
+ cos_values=(),
112
+ sin_values=(),
113
+ theta_values=(),
114
+ index={},
115
+ edge_src=None,
116
+ edge_dst=None,
117
+ )
70
118
 
71
119
  node_list, theta_vals = zip(*pairs)
120
+ node_list = tuple(node_list)
72
121
  theta_arr = np.fromiter(theta_vals, dtype=float)
73
122
  cos_arr = np.cos(theta_arr)
74
123
  sin_arr = np.sin(theta_arr)
@@ -76,7 +125,21 @@ def compute_theta_trig(
76
125
  cos_th = dict(zip(node_list, map(float, cos_arr)))
77
126
  sin_th = dict(zip(node_list, map(float, sin_arr)))
78
127
  thetas = dict(zip(node_list, map(float, theta_arr)))
79
- return TrigCache(cos=cos_th, sin=sin_th, theta=thetas)
128
+ theta_checksums = {node: _theta_checksum(float(theta)) for node, theta in pairs}
129
+ index = {n: i for i, n in enumerate(node_list)}
130
+ return TrigCache(
131
+ cos=cos_th,
132
+ sin=sin_th,
133
+ theta=thetas,
134
+ theta_checksums=theta_checksums,
135
+ order=node_list,
136
+ cos_values=cos_arr,
137
+ sin_values=sin_arr,
138
+ theta_values=theta_arr,
139
+ index=index,
140
+ edge_src=None,
141
+ edge_dst=None,
142
+ )
80
143
 
81
144
 
82
145
  def _build_trig_cache(G: GraphLike, np: Any | None = None) -> TrigCache:
@@ -95,11 +158,45 @@ def get_trig_cache(
95
158
 
96
159
  if np is None:
97
160
  np = get_numpy()
98
- version = G.graph.setdefault("_trig_version", 0)
161
+ graph = G.graph
162
+ version = graph.setdefault("_trig_version", 0)
99
163
  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
- )
164
+
165
+ def builder() -> TrigCache:
166
+ return _build_trig_cache(G, np=np)
167
+
168
+ trig = edge_version_cache(G, key, builder, max_entries=cache_size)
169
+ current_checksums = _graph_theta_checksums(G)
170
+ trig_checksums = getattr(trig, "theta_checksums", None)
171
+ if trig_checksums is None:
172
+ trig_checksums = {}
173
+
174
+ if trig_checksums != current_checksums:
175
+ version = version + 1
176
+ graph["_trig_version"] = version
177
+ key = ("_trig", version)
178
+ trig = edge_version_cache(G, key, builder, max_entries=cache_size)
179
+ trig_checksums = getattr(trig, "theta_checksums", None)
180
+ if trig_checksums is None:
181
+ trig_checksums = {}
182
+ if trig_checksums != current_checksums:
183
+ current_checksums = _graph_theta_checksums(G)
184
+ if trig_checksums != current_checksums:
185
+ return trig
186
+ return trig
187
+
188
+
189
+ def _theta_checksum(theta: float) -> bytes:
190
+ """Return a deterministic checksum for ``theta``."""
191
+
192
+ packed = struct.pack("!d", float(theta))
193
+ return hashlib.blake2b(packed, digest_size=8).digest()
194
+
195
+
196
+ def _graph_theta_checksums(G: GraphLike) -> dict[Any, bytes]:
197
+ """Return checksum snapshot of the graph's current ``θ`` values."""
198
+
199
+ checksums: dict[Any, bytes] = {}
200
+ for node, theta in _iter_theta_pairs(G.nodes(data=True)):
201
+ checksums[node] = _theta_checksum(theta)
202
+ return checksums