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
tnfr/alias.py ADDED
@@ -0,0 +1,631 @@
1
+ """Attribute helpers supporting alias keys.
2
+
3
+ ``AliasAccessor`` provides the main implementation for dealing with
4
+ alias-based attribute access. Legacy wrappers ``alias_get`` and
5
+ ``alias_set`` have been removed; use :func:`get_attr` and
6
+ :func:`set_attr` instead.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ from collections import defaultdict
11
+ from collections.abc import Iterable, Mapping, MutableMapping, Sized
12
+ from dataclasses import dataclass
13
+ from functools import lru_cache, partial
14
+ from threading import Lock
15
+ from types import ModuleType
16
+ from typing import (
17
+ TYPE_CHECKING,
18
+ Any,
19
+ Callable,
20
+ Generic,
21
+ Hashable,
22
+ Optional,
23
+ TypeVar,
24
+ cast,
25
+ )
26
+
27
+ from .constants import get_aliases
28
+ from .types import FloatArray, NodeId
29
+ from .utils import convert_value
30
+
31
+ ALIAS_VF = get_aliases("VF")
32
+ ALIAS_DNFR = get_aliases("DNFR")
33
+ ALIAS_THETA = get_aliases("THETA")
34
+
35
+ if TYPE_CHECKING: # pragma: no cover
36
+ import networkx
37
+
38
+ T = TypeVar("T")
39
+
40
+
41
+ @lru_cache(maxsize=128)
42
+ def _alias_cache(alias_tuple: tuple[str, ...]) -> tuple[str, ...]:
43
+ """Validate and cache alias tuples.
44
+
45
+ ``functools.lru_cache`` protects this function with an internal lock,
46
+ which is sufficient for thread-safe access; no explicit locking is
47
+ required.
48
+ """
49
+ if not alias_tuple:
50
+ raise ValueError("'aliases' must contain at least one key")
51
+ if not all(isinstance(a, str) for a in alias_tuple):
52
+ raise TypeError("'aliases' elements must be strings")
53
+ return alias_tuple
54
+
55
+
56
+ class AliasAccessor(Generic[T]):
57
+ """Helper providing ``get`` and ``set`` for alias-based attributes.
58
+
59
+ This class implements all logic for resolving and assigning values
60
+ using alias keys. Helper functions :func:`get_attr` and
61
+ :func:`set_attr` delegate to a module-level instance of this class.
62
+ """
63
+
64
+ def __init__(
65
+ self, conv: Callable[[Any], T] | None = None, default: T | None = None
66
+ ) -> None:
67
+ self._conv = conv
68
+ self._default = default
69
+ # expose cache for testing and manual control
70
+ self._alias_cache = _alias_cache
71
+ self._key_cache: dict[tuple[int, tuple[str, ...]], tuple[str, int]] = {}
72
+ self._lock = Lock()
73
+
74
+ def _prepare(
75
+ self,
76
+ aliases: Iterable[str],
77
+ conv: Callable[[Any], T] | None,
78
+ default: Optional[T] = None,
79
+ ) -> tuple[tuple[str, ...], Callable[[Any], T], Optional[T]]:
80
+ """Validate ``aliases`` and resolve ``conv`` and ``default``.
81
+
82
+ Parameters
83
+ ----------
84
+ aliases:
85
+ Iterable of alias strings. Must not be a single string.
86
+ conv:
87
+ Conversion callable. If ``None``, the accessor's default
88
+ converter is used.
89
+ default:
90
+ Default value to use if no alias is found. If ``None``, the
91
+ accessor's default is used.
92
+ """
93
+
94
+ if isinstance(aliases, str) or not isinstance(aliases, Iterable):
95
+ raise TypeError("'aliases' must be a non-string iterable")
96
+ aliases = _alias_cache(tuple(aliases))
97
+ if conv is None:
98
+ conv = self._conv
99
+ if conv is None:
100
+ raise TypeError("'conv' must be provided")
101
+ if default is None:
102
+ default = self._default
103
+ return aliases, conv, default
104
+
105
+ def _resolve_cache_key(
106
+ self, d: dict[str, Any], aliases: tuple[str, ...]
107
+ ) -> tuple[tuple[int, tuple[str, ...]], str | None]:
108
+ """Return cache entry for ``d`` and ``aliases`` if still valid.
109
+
110
+ The mapping remains coherent only when the cached key exists in
111
+ ``d`` and the dictionary size has not changed. Invalid entries are
112
+ removed to preserve structural consistency.
113
+ """
114
+
115
+ cache_key = (id(d), aliases)
116
+ with self._lock:
117
+ cached = self._key_cache.get(cache_key)
118
+ if cached is not None:
119
+ key, size = cached
120
+ if size == len(d) and key in d:
121
+ return cache_key, key
122
+ with self._lock:
123
+ self._key_cache.pop(cache_key, None)
124
+ return cache_key, None
125
+
126
+ def get(
127
+ self,
128
+ d: dict[str, Any],
129
+ aliases: Iterable[str],
130
+ default: Optional[T] = None,
131
+ *,
132
+ strict: bool = False,
133
+ log_level: int | None = None,
134
+ conv: Callable[[Any], T] | None = None,
135
+ ) -> Optional[T]:
136
+ aliases, conv, default = self._prepare(aliases, conv, default)
137
+ cache_key, key = self._resolve_cache_key(d, aliases)
138
+ if key is not None:
139
+ ok, value = convert_value(
140
+ d[key], conv, strict=strict, key=key, log_level=log_level
141
+ )
142
+ if ok:
143
+ return value
144
+ for key in aliases:
145
+ if key in d:
146
+ ok, value = convert_value(
147
+ d[key], conv, strict=strict, key=key, log_level=log_level
148
+ )
149
+ if ok:
150
+ with self._lock:
151
+ self._key_cache[cache_key] = (key, len(d))
152
+ return value
153
+ if default is not None:
154
+ ok, value = convert_value(
155
+ default,
156
+ conv,
157
+ strict=strict,
158
+ key="default",
159
+ log_level=log_level,
160
+ )
161
+ if ok:
162
+ return value
163
+ return None
164
+
165
+ def set(
166
+ self,
167
+ d: dict[str, Any],
168
+ aliases: Iterable[str],
169
+ value: Any,
170
+ conv: Callable[[Any], T] | None = None,
171
+ ) -> T:
172
+ aliases, conv, _ = self._prepare(aliases, conv)
173
+ cache_key, key = self._resolve_cache_key(d, aliases)
174
+ if key is not None:
175
+ d[key] = conv(value)
176
+ return d[key]
177
+ key = next((k for k in aliases if k in d), aliases[0])
178
+ val = conv(value)
179
+ d[key] = val
180
+ with self._lock:
181
+ self._key_cache[cache_key] = (key, len(d))
182
+ return val
183
+
184
+
185
+ _generic_accessor: AliasAccessor[Any] = AliasAccessor()
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
+
207
+ def get_attr(
208
+ d: dict[str, Any],
209
+ aliases: Iterable[str],
210
+ default: T | None = None,
211
+ *,
212
+ strict: bool = False,
213
+ log_level: int | None = None,
214
+ conv: Callable[[Any], T] = float,
215
+ ) -> T | None:
216
+ """Return the value for the first key in ``aliases`` found in ``d``."""
217
+
218
+ return _generic_accessor.get(
219
+ d,
220
+ aliases,
221
+ default=default,
222
+ strict=strict,
223
+ log_level=log_level,
224
+ conv=conv,
225
+ )
226
+
227
+
228
+ def collect_attr(
229
+ G: "networkx.Graph",
230
+ nodes: Iterable[NodeId],
231
+ aliases: Iterable[str],
232
+ default: float = 0.0,
233
+ *,
234
+ np: ModuleType | None = None,
235
+ ) -> FloatArray | list[float]:
236
+ """Collect attribute values for ``nodes`` from ``G`` using ``aliases``.
237
+
238
+ Parameters
239
+ ----------
240
+ G:
241
+ Graph containing node attribute mappings.
242
+ nodes:
243
+ Iterable of node identifiers to query.
244
+ aliases:
245
+ Sequence of alias keys passed to :func:`get_attr`.
246
+ default:
247
+ Fallback value when no alias is found for a node.
248
+ np:
249
+ Optional NumPy module. When provided, the result is returned as a
250
+ NumPy array of ``float``; otherwise a Python ``list`` is returned.
251
+
252
+ Returns
253
+ -------
254
+ list or numpy.ndarray
255
+ Collected attribute values in the same order as ``nodes``.
256
+ """
257
+
258
+ def _nodes_iter_and_size(nodes: Iterable[NodeId]) -> tuple[Iterable[NodeId], int]:
259
+ if nodes is G.nodes:
260
+ return G.nodes, G.number_of_nodes()
261
+ if isinstance(nodes, Sized):
262
+ return nodes, len(nodes) # type: ignore[arg-type]
263
+ nodes_list = list(nodes)
264
+ return nodes_list, len(nodes_list)
265
+
266
+ nodes_iter, size = _nodes_iter_and_size(nodes)
267
+
268
+ def _value(node: NodeId) -> float:
269
+ return float(get_attr(G.nodes[node], aliases, default))
270
+
271
+ if np is not None:
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]
304
+
305
+
306
+ def set_attr_generic(
307
+ d: dict[str, Any],
308
+ aliases: Iterable[str],
309
+ value: Any,
310
+ *,
311
+ conv: Callable[[Any], T],
312
+ ) -> T:
313
+ """Assign ``value`` to the first alias key found in ``d``."""
314
+
315
+ return _generic_accessor.set(d, aliases, value, conv=conv)
316
+
317
+
318
+ set_attr = partial(set_attr_generic, conv=float)
319
+
320
+
321
+ get_attr_str = partial(get_attr, conv=str)
322
+ set_attr_str = partial(set_attr_generic, conv=str)
323
+
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
+
333
+ # -------------------------
334
+ # Cached global maxima
335
+ # -------------------------
336
+
337
+
338
+ @dataclass(slots=True)
339
+ class AbsMaxResult:
340
+ """Absolute maximum value and the node where it occurs."""
341
+
342
+ max_value: float
343
+ node: Hashable | None
344
+
345
+
346
+ def _coerce_abs_value(value: Any) -> float:
347
+ """Return ``value`` as ``float`` treating ``None`` as ``0.0``."""
348
+
349
+ if value is None:
350
+ return 0.0
351
+ try:
352
+ return float(value)
353
+ except (TypeError, ValueError):
354
+ return 0.0
355
+
356
+
357
+ def _compute_abs_max_result(
358
+ G: "networkx.Graph",
359
+ aliases: tuple[str, ...],
360
+ *,
361
+ key: str | None = None,
362
+ candidate: tuple[Hashable, float] | None = None,
363
+ ) -> AbsMaxResult:
364
+ """Return the absolute maximum (and node) for ``aliases``.
365
+
366
+ Parameters
367
+ ----------
368
+ G:
369
+ Graph containing nodal data.
370
+ aliases:
371
+ Attribute aliases to inspect.
372
+ key:
373
+ Cache key to update. When ``None``, the graph cache is untouched.
374
+ candidate:
375
+ Optional ``(node, value)`` pair representing a candidate maximum.
376
+
377
+ Returns
378
+ -------
379
+ AbsMaxResult
380
+ Structure holding the absolute maximum and the node where it
381
+ occurs. When ``candidate`` is provided, its value is treated as the
382
+ current maximum and no recomputation is performed.
383
+ """
384
+
385
+ if candidate is not None:
386
+ node, value = candidate
387
+ max_val = abs(float(value))
388
+ else:
389
+ node, max_val = max(
390
+ ((n, abs(get_attr(G.nodes[n], aliases, 0.0))) for n in G.nodes()),
391
+ key=lambda item: item[1],
392
+ default=(None, 0.0),
393
+ )
394
+ max_val = float(max_val)
395
+
396
+ if key is not None:
397
+ G.graph[key] = max_val
398
+ G.graph[f"{key}_node"] = node
399
+
400
+ return AbsMaxResult(max_value=max_val, node=node)
401
+
402
+
403
+ def multi_recompute_abs_max(
404
+ G: "networkx.Graph", alias_map: dict[str, tuple[str, ...]]
405
+ ) -> dict[str, float]:
406
+ """Return absolute maxima for each entry in ``alias_map``.
407
+
408
+ ``G`` is a :class:`networkx.Graph`. ``alias_map`` maps result keys to
409
+ alias tuples. The graph is traversed once and the absolute maximum for
410
+ each alias tuple is recorded. The returned dictionary uses the same
411
+ keys as ``alias_map``.
412
+ """
413
+
414
+ maxima: defaultdict[str, float] = defaultdict(float)
415
+ items = list(alias_map.items())
416
+ for _, nd in G.nodes(data=True):
417
+ maxima.update(
418
+ {
419
+ key: max(maxima[key], abs(get_attr(nd, aliases, 0.0)))
420
+ for key, aliases in items
421
+ }
422
+ )
423
+ return {k: float(v) for k, v in maxima.items()}
424
+
425
+
426
+ def _update_cached_abs_max(
427
+ G: "networkx.Graph",
428
+ aliases: tuple[str, ...],
429
+ n: Hashable,
430
+ value: float,
431
+ *,
432
+ key: str,
433
+ ) -> AbsMaxResult:
434
+ """Update cached absolute maxima for ``aliases``.
435
+
436
+ The current cached value is updated when ``value`` becomes the new
437
+ maximum or when the stored node matches ``n`` but its magnitude
438
+ decreases. The returned :class:`AbsMaxResult` always reflects the
439
+ cached maximum after applying the update.
440
+ """
441
+
442
+ node_key = f"{key}_node"
443
+ val = abs(float(value))
444
+ cur = _coerce_abs_value(G.graph.get(key))
445
+ cur_node = cast(Hashable | None, G.graph.get(node_key))
446
+
447
+ if val >= cur:
448
+ return _compute_abs_max_result(
449
+ G, aliases, key=key, candidate=(n, val)
450
+ )
451
+ if cur_node == n:
452
+ return _compute_abs_max_result(G, aliases, key=key)
453
+ return AbsMaxResult(max_value=cur, node=cur_node)
454
+
455
+
456
+ def set_attr_and_cache(
457
+ G: "networkx.Graph",
458
+ n: Hashable,
459
+ aliases: tuple[str, ...],
460
+ value: float,
461
+ *,
462
+ cache: str | None = None,
463
+ extra: Callable[["networkx.Graph", Hashable, float], None] | None = None,
464
+ ) -> AbsMaxResult | None:
465
+ """Assign ``value`` to node ``n`` and optionally update cached maxima.
466
+
467
+ Returns
468
+ -------
469
+ AbsMaxResult | None
470
+ Absolute maximum information when ``cache`` is provided; otherwise
471
+ ``None``.
472
+ """
473
+
474
+ val = set_attr(G.nodes[n], aliases, value)
475
+ result: AbsMaxResult | None = None
476
+ if cache is not None:
477
+ result = _update_cached_abs_max(G, aliases, n, val, key=cache)
478
+ if extra is not None:
479
+ extra(G, n, val)
480
+ return result
481
+
482
+
483
+ def set_attr_with_max(
484
+ G: "networkx.Graph",
485
+ n: Hashable,
486
+ aliases: tuple[str, ...],
487
+ value: float,
488
+ *,
489
+ cache: str,
490
+ ) -> AbsMaxResult:
491
+ """Assign ``value`` to node ``n`` and update the global maximum.
492
+
493
+ This is a convenience wrapper around :func:`set_attr_and_cache`.
494
+ """
495
+ return cast(
496
+ AbsMaxResult,
497
+ set_attr_and_cache(G, n, aliases, value, cache=cache),
498
+ )
499
+
500
+
501
+ def set_scalar(
502
+ G: "networkx.Graph",
503
+ n: Hashable,
504
+ alias: tuple[str, ...],
505
+ value: float,
506
+ *,
507
+ cache: str | None = None,
508
+ extra: Callable[["networkx.Graph", Hashable, float], None] | None = None,
509
+ ) -> AbsMaxResult | None:
510
+ """Assign ``value`` to ``alias`` for node ``n`` and update caches.
511
+
512
+ Returns
513
+ -------
514
+ AbsMaxResult | None
515
+ Updated absolute maximum details when ``cache`` is provided.
516
+ """
517
+
518
+ return set_attr_and_cache(G, n, alias, value, cache=cache, extra=extra)
519
+
520
+
521
+ def _increment_trig_version(
522
+ G: "networkx.Graph", _: Hashable, __: float
523
+ ) -> None:
524
+ """Increment cached trig version to invalidate trig caches."""
525
+ g = G.graph
526
+ g["_trig_version"] = int(g.get("_trig_version", 0)) + 1
527
+
528
+
529
+ SCALAR_SETTERS: dict[str, dict[str, Any]] = {
530
+ "vf": {
531
+ "alias": ALIAS_VF,
532
+ "cache": "_vfmax",
533
+ "doc": "Set ``νf`` for node ``n`` and optionally update the global maximum.",
534
+ "update_max_param": True,
535
+ },
536
+ "dnfr": {
537
+ "alias": ALIAS_DNFR,
538
+ "cache": "_dnfrmax",
539
+ "doc": "Set ``ΔNFR`` for node ``n`` and update the global maximum.",
540
+ },
541
+ "theta": {
542
+ "alias": ALIAS_THETA,
543
+ "extra": _increment_trig_version,
544
+ "doc": "Set ``theta`` for node ``n`` and invalidate trig caches.",
545
+ },
546
+ }
547
+
548
+
549
+ def _make_scalar_setter(
550
+ name: str, spec: dict[str, Any]
551
+ ) -> Callable[..., AbsMaxResult | None]:
552
+ alias = spec["alias"]
553
+ cache = spec.get("cache")
554
+ extra = spec.get("extra")
555
+ doc = spec.get("doc")
556
+ has_update = spec.get("update_max_param", False)
557
+
558
+ if has_update:
559
+
560
+ def setter(
561
+ G: "networkx.Graph",
562
+ n: Hashable,
563
+ value: float,
564
+ *,
565
+ update_max: bool = True,
566
+ ) -> AbsMaxResult | None:
567
+ cache_key = cache if update_max else None
568
+ return set_scalar(G, n, alias, value, cache=cache_key, extra=extra)
569
+
570
+ else:
571
+
572
+ def setter(
573
+ G: "networkx.Graph", n: Hashable, value: float
574
+ ) -> AbsMaxResult | None:
575
+ return set_scalar(G, n, alias, value, cache=cache, extra=extra)
576
+
577
+ setter.__name__ = f"set_{name}"
578
+ setter.__qualname__ = f"set_{name}"
579
+ setter.__doc__ = doc
580
+ return setter
581
+
582
+
583
+ for _name, _spec in SCALAR_SETTERS.items():
584
+ globals()[f"set_{_name}"] = _make_scalar_setter(_name, _spec)
585
+
586
+ del _name, _spec, _make_scalar_setter
587
+
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
+
614
+ __all__ = [
615
+ "AbsMaxResult",
616
+ "set_attr_generic",
617
+ "get_attr",
618
+ "get_theta_attr",
619
+ "collect_attr",
620
+ "collect_theta_attr",
621
+ "set_attr",
622
+ "get_attr_str",
623
+ "set_attr_str",
624
+ "set_theta_attr",
625
+ "set_attr_and_cache",
626
+ "set_attr_with_max",
627
+ "set_scalar",
628
+ "SCALAR_SETTERS",
629
+ *[f"set_{name}" for name in SCALAR_SETTERS],
630
+ "multi_recompute_abs_max",
631
+ ]