tnfr 4.5.2__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 (161) hide show
  1. tnfr/__init__.py +228 -49
  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 +106 -21
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +666 -512
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +2 -9
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +21 -7
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +42 -20
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +54 -20
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +0 -2
  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.py → config/init.py} +11 -7
  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 +78 -24
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +1 -2
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.pyi +12 -0
  35. tnfr/constants/metric.py +4 -12
  36. tnfr/constants/metric.pyi +19 -0
  37. tnfr/constants_glyphs.py +9 -91
  38. tnfr/constants_glyphs.pyi +12 -0
  39. tnfr/dynamics/__init__.py +112 -634
  40. tnfr/dynamics/__init__.pyi +83 -0
  41. tnfr/dynamics/adaptation.py +201 -0
  42. tnfr/dynamics/aliases.py +22 -0
  43. tnfr/dynamics/coordination.py +343 -0
  44. tnfr/dynamics/dnfr.py +1936 -354
  45. tnfr/dynamics/dnfr.pyi +33 -0
  46. tnfr/dynamics/integrators.py +369 -75
  47. tnfr/dynamics/integrators.pyi +35 -0
  48. tnfr/dynamics/runtime.py +521 -0
  49. tnfr/dynamics/sampling.py +8 -5
  50. tnfr/dynamics/sampling.pyi +7 -0
  51. tnfr/dynamics/selectors.py +680 -0
  52. tnfr/execution.py +56 -41
  53. tnfr/execution.pyi +65 -0
  54. tnfr/flatten.py +7 -7
  55. tnfr/flatten.pyi +28 -0
  56. tnfr/gamma.py +54 -37
  57. tnfr/gamma.pyi +40 -0
  58. tnfr/glyph_history.py +85 -38
  59. tnfr/glyph_history.pyi +53 -0
  60. tnfr/grammar.py +19 -338
  61. tnfr/grammar.pyi +13 -0
  62. tnfr/helpers/__init__.py +110 -30
  63. tnfr/helpers/__init__.pyi +66 -0
  64. tnfr/helpers/numeric.py +1 -0
  65. tnfr/helpers/numeric.pyi +12 -0
  66. tnfr/immutable.py +55 -19
  67. tnfr/immutable.pyi +37 -0
  68. tnfr/initialization.py +12 -10
  69. tnfr/initialization.pyi +73 -0
  70. tnfr/io.py +99 -34
  71. tnfr/io.pyi +11 -0
  72. tnfr/locking.pyi +7 -0
  73. tnfr/metrics/__init__.pyi +20 -0
  74. tnfr/metrics/coherence.py +934 -294
  75. tnfr/metrics/common.py +1 -3
  76. tnfr/metrics/common.pyi +15 -0
  77. tnfr/metrics/core.py +192 -34
  78. tnfr/metrics/core.pyi +13 -0
  79. tnfr/metrics/diagnosis.py +707 -101
  80. tnfr/metrics/diagnosis.pyi +89 -0
  81. tnfr/metrics/export.py +27 -13
  82. tnfr/metrics/glyph_timing.py +218 -38
  83. tnfr/metrics/reporting.py +22 -18
  84. tnfr/metrics/reporting.pyi +12 -0
  85. tnfr/metrics/sense_index.py +199 -25
  86. tnfr/metrics/sense_index.pyi +9 -0
  87. tnfr/metrics/trig.py +53 -18
  88. tnfr/metrics/trig.pyi +12 -0
  89. tnfr/metrics/trig_cache.py +3 -7
  90. tnfr/metrics/trig_cache.pyi +10 -0
  91. tnfr/node.py +148 -125
  92. tnfr/node.pyi +161 -0
  93. tnfr/observers.py +44 -30
  94. tnfr/observers.pyi +46 -0
  95. tnfr/ontosim.py +14 -13
  96. tnfr/ontosim.pyi +33 -0
  97. tnfr/operators/__init__.py +84 -52
  98. tnfr/operators/__init__.pyi +31 -0
  99. tnfr/operators/definitions.py +181 -0
  100. tnfr/operators/definitions.pyi +92 -0
  101. tnfr/operators/jitter.py +86 -23
  102. tnfr/operators/jitter.pyi +11 -0
  103. tnfr/operators/registry.py +80 -0
  104. tnfr/operators/registry.pyi +15 -0
  105. tnfr/operators/remesh.py +141 -57
  106. tnfr/presets.py +9 -54
  107. tnfr/presets.pyi +7 -0
  108. tnfr/py.typed +0 -0
  109. tnfr/rng.py +259 -73
  110. tnfr/rng.pyi +14 -0
  111. tnfr/selector.py +24 -17
  112. tnfr/selector.pyi +19 -0
  113. tnfr/sense.py +55 -43
  114. tnfr/sense.pyi +30 -0
  115. tnfr/structural.py +44 -267
  116. tnfr/structural.pyi +46 -0
  117. tnfr/telemetry/__init__.py +13 -0
  118. tnfr/telemetry/verbosity.py +37 -0
  119. tnfr/tokens.py +3 -2
  120. tnfr/tokens.pyi +41 -0
  121. tnfr/trace.py +272 -82
  122. tnfr/trace.pyi +68 -0
  123. tnfr/types.py +345 -6
  124. tnfr/types.pyi +145 -0
  125. tnfr/utils/__init__.py +158 -0
  126. tnfr/utils/__init__.pyi +133 -0
  127. tnfr/utils/cache.py +755 -0
  128. tnfr/utils/cache.pyi +156 -0
  129. tnfr/{collections_utils.py → utils/data.py} +57 -90
  130. tnfr/utils/data.pyi +73 -0
  131. tnfr/utils/graph.py +87 -0
  132. tnfr/utils/graph.pyi +10 -0
  133. tnfr/utils/init.py +746 -0
  134. tnfr/utils/init.pyi +85 -0
  135. tnfr/{json_utils.py → utils/io.py} +13 -18
  136. tnfr/utils/io.pyi +10 -0
  137. tnfr/utils/validators.py +130 -0
  138. tnfr/utils/validators.pyi +19 -0
  139. tnfr/validation/__init__.py +25 -0
  140. tnfr/validation/__init__.pyi +17 -0
  141. tnfr/validation/compatibility.py +59 -0
  142. tnfr/validation/compatibility.pyi +8 -0
  143. tnfr/validation/grammar.py +149 -0
  144. tnfr/validation/grammar.pyi +11 -0
  145. tnfr/validation/rules.py +194 -0
  146. tnfr/validation/rules.pyi +18 -0
  147. tnfr/validation/syntax.py +151 -0
  148. tnfr/validation/syntax.pyi +7 -0
  149. tnfr-6.0.0.dist-info/METADATA +135 -0
  150. tnfr-6.0.0.dist-info/RECORD +157 -0
  151. tnfr/graph_utils.py +0 -84
  152. tnfr/import_utils.py +0 -228
  153. tnfr/logging_utils.py +0 -116
  154. tnfr/validators.py +0 -84
  155. tnfr/value_utils.py +0 -59
  156. tnfr-4.5.2.dist-info/METADATA +0 -379
  157. tnfr-4.5.2.dist-info/RECORD +0 -67
  158. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  159. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  160. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  161. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/utils/cache.pyi ADDED
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, MutableMapping
5
+ from typing import Any, ContextManager, Generic, TypeVar
6
+
7
+ import networkx as nx
8
+
9
+ from ..cache import CacheCapacityConfig, CacheManager
10
+ from ..types import GraphLike, NodeId, TNFRGraph, TimingContext
11
+
12
+ K = TypeVar("K", bound=Hashable)
13
+ V = TypeVar("V")
14
+ T = TypeVar("T")
15
+
16
+ __all__ = (
17
+ "EdgeCacheManager",
18
+ "NODE_SET_CHECKSUM_KEY",
19
+ "cached_node_list",
20
+ "cached_nodes_and_A",
21
+ "clear_node_repr_cache",
22
+ "configure_graph_cache_limits",
23
+ "edge_version_cache",
24
+ "edge_version_update",
25
+ "ensure_node_index_map",
26
+ "ensure_node_offset_map",
27
+ "get_graph_version",
28
+ "increment_edge_version",
29
+ "increment_graph_version",
30
+ "node_set_checksum",
31
+ "stable_json",
32
+ )
33
+
34
+ NODE_SET_CHECKSUM_KEY: str
35
+
36
+
37
+ class LRUCache(MutableMapping[K, V], Generic[K, V]):
38
+ def __init__(self, maxsize: int = ...) -> None: ...
39
+
40
+ def __getitem__(self, __key: K) -> V: ...
41
+
42
+ def __setitem__(self, __key: K, __value: V) -> None: ...
43
+
44
+ def __delitem__(self, __key: K) -> None: ...
45
+
46
+ def __iter__(self) -> Iterator[K]: ...
47
+
48
+ def __len__(self) -> int: ...
49
+
50
+
51
+ class EdgeCacheState:
52
+ cache: MutableMapping[Hashable, Any]
53
+ locks: MutableMapping[Hashable, threading.RLock]
54
+ max_entries: int | None
55
+
56
+
57
+ class EdgeCacheManager:
58
+ _STATE_KEY: str
59
+
60
+ def __init__(self, graph: MutableMapping[str, Any]) -> None: ...
61
+
62
+ def record_hit(self) -> None: ...
63
+
64
+ def record_miss(self, *, track_metrics: bool = ...) -> None: ...
65
+
66
+ def record_eviction(self, *, track_metrics: bool = ...) -> None: ...
67
+
68
+ def timer(self) -> TimingContext: ...
69
+
70
+ def _default_state(self) -> EdgeCacheState: ...
71
+
72
+ def resolve_max_entries(self, max_entries: int | None | object) -> int | None: ...
73
+
74
+ def _build_state(self, max_entries: int | None) -> EdgeCacheState: ...
75
+
76
+ def _ensure_state(
77
+ self, state: EdgeCacheState | None, max_entries: int | None | object
78
+ ) -> EdgeCacheState: ...
79
+
80
+ def _reset_state(self, state: EdgeCacheState | None) -> EdgeCacheState: ...
81
+
82
+ def get_cache(
83
+ self,
84
+ max_entries: int | None | object,
85
+ *,
86
+ create: bool = ...,
87
+ ) -> tuple[
88
+ MutableMapping[Hashable, Any] | None,
89
+ MutableMapping[Hashable, threading.RLock] | None,
90
+ ]: ...
91
+
92
+ def clear(self) -> None: ...
93
+
94
+
95
+ def get_graph_version(graph: Any, key: str, default: int = ...) -> int: ...
96
+
97
+
98
+ def increment_graph_version(graph: Any, key: str) -> int: ...
99
+
100
+
101
+ def stable_json(obj: Any) -> str: ...
102
+
103
+
104
+ def clear_node_repr_cache() -> None: ...
105
+
106
+
107
+ def node_set_checksum(
108
+ G: nx.Graph,
109
+ nodes: Iterable[Any] | None = ...,
110
+ *,
111
+ presorted: bool = ...,
112
+ store: bool = ...,
113
+ ) -> str: ...
114
+
115
+
116
+ def cached_node_list(G: nx.Graph) -> tuple[Any, ...]: ...
117
+
118
+
119
+ def ensure_node_index_map(G: TNFRGraph) -> dict[NodeId, int]: ...
120
+
121
+
122
+ def ensure_node_offset_map(G: TNFRGraph) -> dict[NodeId, int]: ...
123
+
124
+
125
+ def configure_graph_cache_limits(
126
+ G: GraphLike | TNFRGraph | MutableMapping[str, Any],
127
+ *,
128
+ default_capacity: int | None | object = CacheManager._MISSING,
129
+ overrides: Mapping[str, int | None] | None = ...,
130
+ replace_overrides: bool = ...,
131
+ ) -> CacheCapacityConfig: ...
132
+
133
+
134
+ def increment_edge_version(G: Any) -> None: ...
135
+
136
+
137
+ def edge_version_cache(
138
+ G: Any,
139
+ key: Hashable,
140
+ builder: Callable[[], T],
141
+ *,
142
+ max_entries: int | None | object = CacheManager._MISSING,
143
+ ) -> T: ...
144
+
145
+
146
+ def cached_nodes_and_A(
147
+ G: nx.Graph,
148
+ *,
149
+ cache_size: int | None = ...,
150
+ require_numpy: bool = ...,
151
+ prefer_sparse: bool = ...,
152
+ nodes: tuple[Any, ...] | None = ...,
153
+ ) -> tuple[tuple[Any, ...], Any]: ...
154
+
155
+
156
+ def edge_version_update(G: TNFRGraph) -> ContextManager[None]: ...
@@ -1,4 +1,4 @@
1
- """Utilities for working with generic collections and weight mappings."""
1
+ """Utilities for manipulating collections and scalar values within TNFR."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -8,39 +8,70 @@ from collections.abc import Collection, Iterable, Mapping, Sequence
8
8
  from itertools import islice
9
9
  from typing import Any, Callable, Iterator, TypeVar, cast
10
10
 
11
- from .logging_utils import get_logger
12
- from .logging_utils import warn_once as _warn_once_factory
13
- from .value_utils import convert_value
14
- from .helpers.numeric import kahan_sum_nd
11
+ from ..helpers.numeric import kahan_sum_nd
12
+ from .init import get_logger, warn_once as _warn_once_factory
15
13
 
16
14
  T = TypeVar("T")
17
15
 
18
- logger = get_logger(__name__)
16
+ _collections_logger = get_logger("tnfr.utils.data.collections")
17
+ _value_logger = get_logger("tnfr.utils.data")
19
18
 
20
19
  STRING_TYPES = (str, bytes, bytearray)
21
20
 
22
21
  NEGATIVE_WEIGHTS_MSG = "Negative weights detected: %s"
23
22
 
24
- _NEGATIVE_WARN_ONCE_MAXSIZE = 1024
23
+ _MAX_NEGATIVE_WARN_ONCE = 1024
24
+
25
+ __all__ = (
26
+ "convert_value",
27
+ "MAX_MATERIALIZE_DEFAULT",
28
+ "normalize_materialize_limit",
29
+ "is_non_string_sequence",
30
+ "flatten_structure",
31
+ "STRING_TYPES",
32
+ "ensure_collection",
33
+ "normalize_weights",
34
+ "negative_weights_warn_once",
35
+ "normalize_counter",
36
+ "mix_groups",
37
+ )
38
+
39
+
40
+ def convert_value(
41
+ value: Any,
42
+ conv: Callable[[Any], T],
43
+ *,
44
+ strict: bool = False,
45
+ key: str | None = None,
46
+ log_level: int | None = None,
47
+ ) -> tuple[bool, T | None]:
48
+ """Attempt to convert a value and report failures."""
49
+
50
+ try:
51
+ return True, conv(value)
52
+ except (ValueError, TypeError) as exc:
53
+ if strict:
54
+ raise
55
+ level = log_level if log_level is not None else logging.DEBUG
56
+ if key is not None:
57
+ _value_logger.log(level, "Could not convert value for %r: %s", key, exc)
58
+ else:
59
+ _value_logger.log(level, "Could not convert value: %s", exc)
60
+ return False, None
25
61
 
26
62
 
27
63
  def negative_weights_warn_once(
28
- *, maxsize: int = _NEGATIVE_WARN_ONCE_MAXSIZE
64
+ *, maxsize: int = _MAX_NEGATIVE_WARN_ONCE
29
65
  ) -> Callable[[Mapping[str, float]], None]:
30
- """Return a ``WarnOnce`` callable for negative weight warnings.
31
-
32
- The returned callable may be reused across multiple
33
- :func:`normalize_weights` invocations to suppress duplicate warnings for
34
- the same keys.
35
- """
66
+ """Return a ``WarnOnce`` callable for negative weight warnings."""
36
67
 
37
- return _warn_once_factory(logger, NEGATIVE_WEIGHTS_MSG, maxsize=maxsize)
68
+ return _warn_once_factory(_collections_logger, NEGATIVE_WEIGHTS_MSG, maxsize=maxsize)
38
69
 
39
70
 
40
71
  def _log_negative_weights(negatives: Mapping[str, float]) -> None:
41
72
  """Log negative weight warnings without deduplicating keys."""
42
73
 
43
- logger.warning(NEGATIVE_WEIGHTS_MSG, negatives)
74
+ _collections_logger.warning(NEGATIVE_WEIGHTS_MSG, negatives)
44
75
 
45
76
 
46
77
  def _resolve_negative_warn_handler(
@@ -57,6 +88,7 @@ def _resolve_negative_warn_handler(
57
88
 
58
89
  def is_non_string_sequence(obj: Any) -> bool:
59
90
  """Return ``True`` if ``obj`` is an ``Iterable`` but not string-like or a mapping."""
91
+
60
92
  return isinstance(obj, Iterable) and not isinstance(obj, (*STRING_TYPES, Mapping))
61
93
 
62
94
 
@@ -65,20 +97,7 @@ def flatten_structure(
65
97
  *,
66
98
  expand: Callable[[Any], Iterable[Any] | None] | None = None,
67
99
  ) -> Iterator[Any]:
68
- """Yield leaf items from ``obj``.
69
-
70
- The order of yielded items follows the order of the input iterable when it
71
- is defined. For unordered iterables like :class:`set` the resulting order is
72
- arbitrary. Mappings are treated as atomic items and not expanded.
73
-
74
- Parameters
75
- ----------
76
- obj:
77
- Object that may contain nested iterables.
78
- expand:
79
- Optional callable returning a replacement iterable for ``item``. When
80
- it returns ``None`` the ``item`` is processed normally.
81
- """
100
+ """Yield leaf items from ``obj`` following breadth-first semantics."""
82
101
 
83
102
  stack = deque([obj])
84
103
  seen: set[int] = set()
@@ -100,30 +119,13 @@ def flatten_structure(
100
119
  yield item
101
120
 
102
121
 
103
- __all__ = (
104
- "MAX_MATERIALIZE_DEFAULT",
105
- "normalize_materialize_limit",
106
- "is_non_string_sequence",
107
- "flatten_structure",
108
- "STRING_TYPES",
109
- "ensure_collection",
110
- "normalize_weights",
111
- "negative_weights_warn_once",
112
- "normalize_counter",
113
- "mix_groups",
114
- )
115
-
116
122
  MAX_MATERIALIZE_DEFAULT: int = 1000
117
- """Default materialization limit used by :func:`ensure_collection`.
118
-
119
- This guard prevents accidentally consuming huge or infinite iterables when a
120
- limit is not explicitly provided. Pass ``max_materialize=None`` to disable the
121
- limit.
122
- """
123
+ """Default materialization limit used by :func:`ensure_collection`."""
123
124
 
124
125
 
125
126
  def normalize_materialize_limit(max_materialize: int | None) -> int | None:
126
127
  """Normalize and validate ``max_materialize`` returning a usable limit."""
128
+
127
129
  if max_materialize is None:
128
130
  return None
129
131
  limit = int(max_materialize)
@@ -138,34 +140,17 @@ def ensure_collection(
138
140
  max_materialize: int | None = MAX_MATERIALIZE_DEFAULT,
139
141
  error_msg: str | None = None,
140
142
  ) -> Collection[T]:
141
- """Return ``it`` as a :class:`Collection`, materializing when needed.
142
-
143
- Checks are executed in the following order:
144
-
145
- 1. Existing collections are returned directly. String-like inputs
146
- (``str``, ``bytes`` and ``bytearray``) are wrapped as a single item
147
- tuple.
148
- 2. The object must be an :class:`Iterable`; otherwise ``TypeError`` is
149
- raised.
150
- 3. Remaining iterables are materialized up to ``max_materialize`` items.
151
- ``None`` disables the limit. ``error_msg`` customizes the
152
- :class:`ValueError` raised when the iterable yields more items than
153
- allowed. The input is consumed at most once and no extra items beyond the
154
- limit are stored in memory.
155
- """
156
-
157
- # Step 1: early-return for collections and raw strings/bytes
143
+ """Return ``it`` as a :class:`Collection`, materializing when needed."""
144
+
158
145
  if isinstance(it, Collection):
159
146
  if isinstance(it, STRING_TYPES):
160
147
  return (cast(T, it),)
161
148
  else:
162
149
  return it
163
150
 
164
- # Step 2: ensure the input is iterable
165
151
  if not isinstance(it, Iterable):
166
152
  raise TypeError(f"{it!r} is not iterable")
167
153
 
168
- # Step 3: validate limit and materialize items once
169
154
  limit = normalize_materialize_limit(max_materialize)
170
155
  if limit is None:
171
156
  return tuple(it)
@@ -231,28 +216,8 @@ def normalize_weights(
231
216
  warn_once: bool | Callable[[Mapping[str, float]], None] = True,
232
217
  error_on_conversion: bool = False,
233
218
  ) -> dict[str, float]:
234
- """Normalize ``keys`` in mapping ``dict_like`` so their sum is 1.
235
-
236
- ``keys`` may be any iterable of strings and is materialized once while
237
- collapsing repeated entries preserving their first occurrence.
238
-
239
- Negative weights are handled according to ``error_on_negative``. When
240
- ``True`` a :class:`ValueError` is raised. Otherwise negatives are logged,
241
- replaced with ``0`` and the remaining weights are renormalized. If all
242
- weights are non-positive a uniform distribution is returned.
243
-
244
- Conversion errors are controlled separately by ``error_on_conversion``. When
245
- ``True`` any :class:`TypeError` or :class:`ValueError` while converting a
246
- value to ``float`` is propagated. Otherwise the error is logged and the
247
- ``default`` value is used.
248
-
249
- ``warn_once`` accepts either a boolean or a callable. ``False`` logs all
250
- negative weights using :func:`logging.Logger.warning`. ``True`` (the
251
- default) creates a fresh :class:`~tnfr.logging_utils.WarnOnce` instance for
252
- the call, emitting a single warning containing all negative keys. To reuse
253
- deduplication state across calls, pass a callable such as
254
- :func:`negative_weights_warn_once`.
255
- """
219
+ """Normalize ``keys`` in mapping ``dict_like`` so their sum is 1."""
220
+
256
221
  weights, keys_list, total = _convert_and_validate_weights(
257
222
  dict_like,
258
223
  keys,
@@ -273,6 +238,7 @@ def normalize_counter(
273
238
  counts: Mapping[str, float | int],
274
239
  ) -> tuple[dict[str, float], float]:
275
240
  """Normalize a ``Counter`` returning proportions and total."""
241
+
276
242
  total = kahan_sum_nd(((c,) for c in counts.values()), dims=1)[0]
277
243
  if total <= 0:
278
244
  return {}, 0
@@ -287,6 +253,7 @@ def mix_groups(
287
253
  prefix: str = "_",
288
254
  ) -> dict[str, float]:
289
255
  """Aggregate values of ``dist`` according to ``groups``."""
256
+
290
257
  out: dict[str, float] = dict(dist)
291
258
  out.update(
292
259
  {
tnfr/utils/data.pyi ADDED
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence
4
+ from typing import Any, Callable, TypeVar
5
+
6
+ T = TypeVar("T")
7
+
8
+ STRING_TYPES: tuple[type[str] | type[bytes] | type[bytearray], ...]
9
+ MAX_MATERIALIZE_DEFAULT: int
10
+ NEGATIVE_WEIGHTS_MSG: str
11
+
12
+ __all__: tuple[str, ...]
13
+
14
+
15
+ def convert_value(
16
+ value: Any,
17
+ conv: Callable[[Any], T],
18
+ *,
19
+ strict: bool = ...,
20
+ key: str | None = ...,
21
+ log_level: int | None = ...,
22
+ ) -> tuple[bool, T | None]: ...
23
+
24
+
25
+ def negative_weights_warn_once(
26
+ *,
27
+ maxsize: int = ...,
28
+ ) -> Callable[[Mapping[str, float]], None]: ...
29
+
30
+
31
+ def is_non_string_sequence(obj: Any) -> bool: ...
32
+
33
+
34
+ def flatten_structure(
35
+ obj: Any,
36
+ *,
37
+ expand: Callable[[Any], Iterable[Any] | None] | None = ...,
38
+ ) -> Iterator[Any]: ...
39
+
40
+
41
+ def normalize_materialize_limit(max_materialize: int | None) -> int | None: ...
42
+
43
+
44
+ def ensure_collection(
45
+ it: Iterable[T],
46
+ *,
47
+ max_materialize: int | None = ...,
48
+ error_msg: str | None = ...,
49
+ ) -> Collection[T]: ...
50
+
51
+
52
+ def normalize_weights(
53
+ dict_like: Mapping[str, Any],
54
+ keys: Iterable[str] | Sequence[str],
55
+ default: float = ...,
56
+ *,
57
+ error_on_negative: bool = ...,
58
+ warn_once: bool | Callable[[Mapping[str, float]], None] = ...,
59
+ error_on_conversion: bool = ...,
60
+ ) -> dict[str, float]: ...
61
+
62
+
63
+ def normalize_counter(
64
+ counts: Mapping[str, float | int],
65
+ ) -> tuple[dict[str, float], float]: ...
66
+
67
+
68
+ def mix_groups(
69
+ dist: Mapping[str, float],
70
+ groups: Mapping[str, Iterable[str]],
71
+ *,
72
+ prefix: str = ...,
73
+ ) -> dict[str, float]: ...
tnfr/utils/graph.py ADDED
@@ -0,0 +1,87 @@
1
+ """Utilities for graph-level bookkeeping shared by TNFR components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from types import MappingProxyType
7
+ from typing import Any, Mapping, MutableMapping
8
+
9
+ from ..types import GraphLike, TNFRGraph
10
+
11
+ __all__ = (
12
+ "get_graph",
13
+ "get_graph_mapping",
14
+ "mark_dnfr_prep_dirty",
15
+ "supports_add_edge",
16
+ "GraphLike",
17
+ )
18
+
19
+
20
+ def get_graph(
21
+ obj: GraphLike | TNFRGraph | MutableMapping[str, Any]
22
+ ) -> MutableMapping[str, Any]:
23
+ """Return the graph-level metadata mapping for ``obj``.
24
+
25
+ ``obj`` must be a :class:`~tnfr.types.TNFRGraph` instance or fulfil the
26
+ :class:`~tnfr.types.GraphLike` protocol. The function normalises access to
27
+ the ``graph`` attribute exposed by ``networkx``-style graphs and wrappers,
28
+ always returning the underlying metadata mapping. A pre-extracted mapping
29
+ is also accepted for legacy call sites.
30
+ """
31
+
32
+ graph = getattr(obj, "graph", None)
33
+ if graph is not None:
34
+ return graph
35
+ if isinstance(obj, MutableMapping):
36
+ return obj
37
+ raise TypeError("Unsupported graph object: metadata mapping not accessible")
38
+
39
+
40
+ def get_graph_mapping(
41
+ G: GraphLike | TNFRGraph | MutableMapping[str, Any], key: str, warn_msg: str
42
+ ) -> Mapping[str, Any] | None:
43
+ """Return an immutable view of ``G``'s stored mapping for ``key``.
44
+
45
+ The ``G`` argument follows the :class:`~tnfr.types.GraphLike` protocol, is
46
+ a concrete :class:`~tnfr.types.TNFRGraph` or provides the metadata mapping
47
+ directly. The helper validates that the stored value is a mapping before
48
+ returning a read-only proxy.
49
+ """
50
+
51
+ graph = get_graph(G)
52
+ getter = getattr(graph, "get", None)
53
+ if getter is None:
54
+ return None
55
+
56
+ data = getter(key)
57
+ if data is None:
58
+ return None
59
+ if not isinstance(data, Mapping):
60
+ warnings.warn(warn_msg, UserWarning, stacklevel=2)
61
+ return None
62
+ return MappingProxyType(data)
63
+
64
+
65
+ def mark_dnfr_prep_dirty(
66
+ G: GraphLike | TNFRGraph | MutableMapping[str, Any]
67
+ ) -> None:
68
+ """Flag ΔNFR preparation data as stale by marking ``G.graph``.
69
+
70
+ ``G`` is constrained to the :class:`~tnfr.types.GraphLike` protocol, a
71
+ concrete :class:`~tnfr.types.TNFRGraph` or an explicit metadata mapping,
72
+ ensuring the metadata storage is available for mutation.
73
+ """
74
+
75
+ graph = get_graph(G)
76
+ graph["_dnfr_prep_dirty"] = True
77
+
78
+
79
+ def supports_add_edge(graph: GraphLike | TNFRGraph) -> bool:
80
+ """Return ``True`` if ``graph`` exposes an ``add_edge`` method.
81
+
82
+ The ``graph`` parameter must implement :class:`~tnfr.types.GraphLike` or be
83
+ a :class:`~tnfr.types.TNFRGraph`, aligning runtime expectations with the
84
+ type contract enforced throughout the engine.
85
+ """
86
+
87
+ return hasattr(graph, "add_edge")
tnfr/utils/graph.pyi ADDED
@@ -0,0 +1,10 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ get_graph: Any
8
+ get_graph_mapping: Any
9
+ mark_dnfr_prep_dirty: Any
10
+ supports_add_edge: Any