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