tnfr 4.5.2__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 (195) hide show
  1. tnfr/__init__.py +275 -51
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +117 -31
  8. tnfr/alias.pyi +108 -0
  9. tnfr/cache.py +6 -572
  10. tnfr/cache.pyi +16 -0
  11. tnfr/callback_utils.py +16 -38
  12. tnfr/callback_utils.pyi +79 -0
  13. tnfr/cli/__init__.py +34 -14
  14. tnfr/cli/__init__.pyi +26 -0
  15. tnfr/cli/arguments.py +211 -28
  16. tnfr/cli/arguments.pyi +27 -0
  17. tnfr/cli/execution.py +470 -50
  18. tnfr/cli/execution.pyi +70 -0
  19. tnfr/cli/utils.py +18 -3
  20. tnfr/cli/utils.pyi +8 -0
  21. tnfr/config/__init__.py +13 -0
  22. tnfr/config/__init__.pyi +10 -0
  23. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  24. tnfr/config/constants.pyi +12 -0
  25. tnfr/config/feature_flags.py +83 -0
  26. tnfr/{config.py → config/init.py} +11 -7
  27. tnfr/config/init.pyi +8 -0
  28. tnfr/config/operator_names.py +93 -0
  29. tnfr/config/operator_names.pyi +28 -0
  30. tnfr/config/presets.py +84 -0
  31. tnfr/config/presets.pyi +7 -0
  32. tnfr/constants/__init__.py +80 -29
  33. tnfr/constants/__init__.pyi +92 -0
  34. tnfr/constants/aliases.py +31 -0
  35. tnfr/constants/core.py +4 -4
  36. tnfr/constants/core.pyi +17 -0
  37. tnfr/constants/init.py +1 -1
  38. tnfr/constants/init.pyi +12 -0
  39. tnfr/constants/metric.py +7 -15
  40. tnfr/constants/metric.pyi +19 -0
  41. tnfr/dynamics/__init__.py +165 -633
  42. tnfr/dynamics/__init__.pyi +82 -0
  43. tnfr/dynamics/adaptation.py +267 -0
  44. tnfr/dynamics/aliases.py +23 -0
  45. tnfr/dynamics/coordination.py +385 -0
  46. tnfr/dynamics/dnfr.py +2283 -400
  47. tnfr/dynamics/dnfr.pyi +24 -0
  48. tnfr/dynamics/integrators.py +406 -98
  49. tnfr/dynamics/integrators.pyi +34 -0
  50. tnfr/dynamics/runtime.py +881 -0
  51. tnfr/dynamics/sampling.py +10 -5
  52. tnfr/dynamics/sampling.pyi +7 -0
  53. tnfr/dynamics/selectors.py +719 -0
  54. tnfr/execution.py +70 -48
  55. tnfr/execution.pyi +45 -0
  56. tnfr/flatten.py +13 -9
  57. tnfr/flatten.pyi +21 -0
  58. tnfr/gamma.py +66 -53
  59. tnfr/gamma.pyi +34 -0
  60. tnfr/glyph_history.py +110 -52
  61. tnfr/glyph_history.pyi +35 -0
  62. tnfr/glyph_runtime.py +16 -0
  63. tnfr/glyph_runtime.pyi +9 -0
  64. tnfr/immutable.py +69 -28
  65. tnfr/immutable.pyi +34 -0
  66. tnfr/initialization.py +16 -16
  67. tnfr/initialization.pyi +65 -0
  68. tnfr/io.py +6 -240
  69. tnfr/io.pyi +16 -0
  70. tnfr/locking.pyi +7 -0
  71. tnfr/mathematics/__init__.py +81 -0
  72. tnfr/mathematics/backend.py +426 -0
  73. tnfr/mathematics/dynamics.py +398 -0
  74. tnfr/mathematics/epi.py +254 -0
  75. tnfr/mathematics/generators.py +222 -0
  76. tnfr/mathematics/metrics.py +119 -0
  77. tnfr/mathematics/operators.py +233 -0
  78. tnfr/mathematics/operators_factory.py +71 -0
  79. tnfr/mathematics/projection.py +78 -0
  80. tnfr/mathematics/runtime.py +173 -0
  81. tnfr/mathematics/spaces.py +247 -0
  82. tnfr/mathematics/transforms.py +292 -0
  83. tnfr/metrics/__init__.py +10 -10
  84. tnfr/metrics/__init__.pyi +20 -0
  85. tnfr/metrics/coherence.py +993 -324
  86. tnfr/metrics/common.py +23 -16
  87. tnfr/metrics/common.pyi +46 -0
  88. tnfr/metrics/core.py +251 -35
  89. tnfr/metrics/core.pyi +13 -0
  90. tnfr/metrics/diagnosis.py +708 -111
  91. tnfr/metrics/diagnosis.pyi +85 -0
  92. tnfr/metrics/export.py +27 -15
  93. tnfr/metrics/glyph_timing.py +232 -42
  94. tnfr/metrics/reporting.py +33 -22
  95. tnfr/metrics/reporting.pyi +12 -0
  96. tnfr/metrics/sense_index.py +987 -43
  97. tnfr/metrics/sense_index.pyi +9 -0
  98. tnfr/metrics/trig.py +214 -23
  99. tnfr/metrics/trig.pyi +13 -0
  100. tnfr/metrics/trig_cache.py +115 -22
  101. tnfr/metrics/trig_cache.pyi +10 -0
  102. tnfr/node.py +542 -136
  103. tnfr/node.pyi +178 -0
  104. tnfr/observers.py +152 -35
  105. tnfr/observers.pyi +31 -0
  106. tnfr/ontosim.py +23 -19
  107. tnfr/ontosim.pyi +28 -0
  108. tnfr/operators/__init__.py +601 -82
  109. tnfr/operators/__init__.pyi +45 -0
  110. tnfr/operators/definitions.py +513 -0
  111. tnfr/operators/definitions.pyi +78 -0
  112. tnfr/operators/grammar.py +760 -0
  113. tnfr/operators/jitter.py +107 -38
  114. tnfr/operators/jitter.pyi +11 -0
  115. tnfr/operators/registry.py +75 -0
  116. tnfr/operators/registry.pyi +13 -0
  117. tnfr/operators/remesh.py +149 -88
  118. tnfr/py.typed +0 -0
  119. tnfr/rng.py +46 -143
  120. tnfr/rng.pyi +14 -0
  121. tnfr/schemas/__init__.py +8 -0
  122. tnfr/schemas/grammar.json +94 -0
  123. tnfr/selector.py +25 -19
  124. tnfr/selector.pyi +19 -0
  125. tnfr/sense.py +72 -62
  126. tnfr/sense.pyi +23 -0
  127. tnfr/structural.py +522 -262
  128. tnfr/structural.pyi +69 -0
  129. tnfr/telemetry/__init__.py +35 -0
  130. tnfr/telemetry/cache_metrics.py +226 -0
  131. tnfr/telemetry/nu_f.py +423 -0
  132. tnfr/telemetry/nu_f.pyi +123 -0
  133. tnfr/telemetry/verbosity.py +37 -0
  134. tnfr/tokens.py +1 -3
  135. tnfr/tokens.pyi +36 -0
  136. tnfr/trace.py +270 -113
  137. tnfr/trace.pyi +40 -0
  138. tnfr/types.py +574 -6
  139. tnfr/types.pyi +331 -0
  140. tnfr/units.py +69 -0
  141. tnfr/units.pyi +16 -0
  142. tnfr/utils/__init__.py +217 -0
  143. tnfr/utils/__init__.pyi +202 -0
  144. tnfr/utils/cache.py +2395 -0
  145. tnfr/utils/cache.pyi +468 -0
  146. tnfr/utils/chunks.py +104 -0
  147. tnfr/utils/chunks.pyi +21 -0
  148. tnfr/{collections_utils.py → utils/data.py} +147 -90
  149. tnfr/utils/data.pyi +64 -0
  150. tnfr/utils/graph.py +85 -0
  151. tnfr/utils/graph.pyi +10 -0
  152. tnfr/utils/init.py +770 -0
  153. tnfr/utils/init.pyi +78 -0
  154. tnfr/utils/io.py +456 -0
  155. tnfr/{helpers → utils}/numeric.py +51 -24
  156. tnfr/utils/numeric.pyi +21 -0
  157. tnfr/validation/__init__.py +113 -0
  158. tnfr/validation/__init__.pyi +77 -0
  159. tnfr/validation/compatibility.py +95 -0
  160. tnfr/validation/compatibility.pyi +6 -0
  161. tnfr/validation/grammar.py +71 -0
  162. tnfr/validation/grammar.pyi +40 -0
  163. tnfr/validation/graph.py +138 -0
  164. tnfr/validation/graph.pyi +17 -0
  165. tnfr/validation/rules.py +281 -0
  166. tnfr/validation/rules.pyi +55 -0
  167. tnfr/validation/runtime.py +263 -0
  168. tnfr/validation/runtime.pyi +31 -0
  169. tnfr/validation/soft_filters.py +170 -0
  170. tnfr/validation/soft_filters.pyi +37 -0
  171. tnfr/validation/spectral.py +159 -0
  172. tnfr/validation/spectral.pyi +46 -0
  173. tnfr/validation/syntax.py +40 -0
  174. tnfr/validation/syntax.pyi +10 -0
  175. tnfr/validation/window.py +39 -0
  176. tnfr/validation/window.pyi +1 -0
  177. tnfr/viz/__init__.py +9 -0
  178. tnfr/viz/matplotlib.py +246 -0
  179. tnfr-7.0.0.dist-info/METADATA +179 -0
  180. tnfr-7.0.0.dist-info/RECORD +185 -0
  181. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  182. tnfr/grammar.py +0 -344
  183. tnfr/graph_utils.py +0 -84
  184. tnfr/helpers/__init__.py +0 -71
  185. tnfr/import_utils.py +0 -228
  186. tnfr/json_utils.py +0 -162
  187. tnfr/logging_utils.py +0 -116
  188. tnfr/presets.py +0 -60
  189. tnfr/validators.py +0 -84
  190. tnfr/value_utils.py +0 -59
  191. tnfr-4.5.2.dist-info/METADATA +0 -379
  192. tnfr-4.5.2.dist-info/RECORD +0 -67
  193. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  194. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  195. {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
@@ -1,50 +1,171 @@
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
 
5
5
  import logging
6
+ import math
6
7
  from collections import deque
7
8
  from collections.abc import Collection, Iterable, Mapping, Sequence
9
+ from numbers import Real
8
10
  from itertools import islice
9
11
  from typing import Any, Callable, Iterator, TypeVar, cast
10
12
 
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
13
+ from .numeric import kahan_sum_nd
14
+ from .init import get_logger
15
+ from .init import warn_once as _warn_once_factory
15
16
 
16
17
  T = TypeVar("T")
17
18
 
18
- logger = get_logger(__name__)
19
+ _collections_logger = get_logger("tnfr.utils.data.collections")
20
+ _value_logger = get_logger("tnfr.utils.data")
19
21
 
20
22
  STRING_TYPES = (str, bytes, bytearray)
21
23
 
22
24
  NEGATIVE_WEIGHTS_MSG = "Negative weights detected: %s"
23
25
 
24
- _NEGATIVE_WARN_ONCE_MAXSIZE = 1024
26
+ _MAX_NEGATIVE_WARN_ONCE = 1024
25
27
 
28
+ __all__ = (
29
+ "convert_value",
30
+ "normalize_optional_int",
31
+ "MAX_MATERIALIZE_DEFAULT",
32
+ "normalize_materialize_limit",
33
+ "is_non_string_sequence",
34
+ "flatten_structure",
35
+ "STRING_TYPES",
36
+ "ensure_collection",
37
+ "normalize_weights",
38
+ "negative_weights_warn_once",
39
+ "normalize_counter",
40
+ "mix_groups",
41
+ )
26
42
 
27
- def negative_weights_warn_once(
28
- *, maxsize: int = _NEGATIVE_WARN_ONCE_MAXSIZE
29
- ) -> Callable[[Mapping[str, float]], None]:
30
- """Return a ``WarnOnce`` callable for negative weight warnings.
31
43
 
32
- The returned callable may be reused across multiple
33
- :func:`normalize_weights` invocations to suppress duplicate warnings for
34
- the same keys.
44
+ def convert_value(
45
+ value: Any,
46
+ conv: Callable[[Any], T],
47
+ *,
48
+ strict: bool = False,
49
+ key: str | None = None,
50
+ log_level: int | None = None,
51
+ ) -> tuple[bool, T | None]:
52
+ """Attempt to convert a value and report failures."""
53
+
54
+ try:
55
+ converted = conv(value)
56
+ except (ValueError, TypeError) as exc:
57
+ if strict:
58
+ raise
59
+ level = log_level if log_level is not None else logging.DEBUG
60
+ if key is not None:
61
+ _value_logger.log(level, "Could not convert value for %r: %s", key, exc)
62
+ else:
63
+ _value_logger.log(level, "Could not convert value: %s", exc)
64
+ return False, None
65
+ if isinstance(converted, float) and not math.isfinite(converted):
66
+ if strict:
67
+ target = f"{key!r}" if key is not None else "value"
68
+ raise ValueError(f"Non-finite value {converted!r} for {target}")
69
+ level = log_level if log_level is not None else logging.DEBUG
70
+ if key is not None:
71
+ _value_logger.log(level, "Non-finite value for %r: %s", key, converted)
72
+ else:
73
+ _value_logger.log(level, "Non-finite value: %s", converted)
74
+ return False, None
75
+ return True, converted
76
+
77
+
78
+ _DEFAULT_SENTINELS = frozenset({"auto", "none", "null"})
79
+
80
+
81
+ def normalize_optional_int(
82
+ value: Any,
83
+ *,
84
+ sentinels: Collection[str] | None = _DEFAULT_SENTINELS,
85
+ allow_non_positive: bool = True,
86
+ strict: bool = False,
87
+ error_message: str | None = None,
88
+ ) -> int | None:
89
+ """Normalise optional integers shared by CLI and runtime helpers.
90
+
91
+ Parameters
92
+ ----------
93
+ value:
94
+ Arbitrary object obtained from configuration, CLI options or graph
95
+ metadata.
96
+ sentinels:
97
+ Collection of case-insensitive strings that should be interpreted as
98
+ ``None``. When ``None`` or empty, no sentinel mapping is applied.
99
+ allow_non_positive:
100
+ When ``False`` values ``<= 0`` are rejected and converted to ``None``.
101
+ strict:
102
+ When ``True`` invalid inputs raise :class:`ValueError` instead of
103
+ returning ``None``.
104
+ error_message:
105
+ Optional message used when ``strict`` mode raises due to invalid input
106
+ or disallowed non-positive values.
35
107
  """
36
108
 
37
- return _warn_once_factory(logger, NEGATIVE_WEIGHTS_MSG, maxsize=maxsize)
109
+ if value is None:
110
+ return None
111
+
112
+ if isinstance(value, int):
113
+ result = value
114
+ elif isinstance(value, Real):
115
+ result = int(value)
116
+ else:
117
+ text = str(value).strip()
118
+ if not text:
119
+ if strict:
120
+ raise ValueError(
121
+ error_message
122
+ or "Empty value is not allowed for configuration options."
123
+ )
124
+ return None
125
+ sentinel_set: set[str] | None = None
126
+ if sentinels:
127
+ sentinel_set = {s.lower() for s in sentinels}
128
+ lowered = text.lower()
129
+ if lowered in sentinel_set:
130
+ return None
131
+ try:
132
+ result = int(text)
133
+ except (TypeError, ValueError) as exc:
134
+ if strict:
135
+ raise ValueError(
136
+ error_message or f"Invalid integer value: {value!r}"
137
+ ) from exc
138
+ return None
139
+
140
+ if not allow_non_positive and result <= 0:
141
+ if strict:
142
+ raise ValueError(
143
+ error_message
144
+ or "Non-positive values are not permitted for this option."
145
+ )
146
+ return None
147
+
148
+ return result
149
+
150
+
151
+ def negative_weights_warn_once(
152
+ *, maxsize: int = _MAX_NEGATIVE_WARN_ONCE
153
+ ) -> Callable[[Mapping[str, float]], None]:
154
+ """Return a ``WarnOnce`` callable for negative weight warnings."""
155
+
156
+ return _warn_once_factory(
157
+ _collections_logger, NEGATIVE_WEIGHTS_MSG, maxsize=maxsize
158
+ )
38
159
 
39
160
 
40
161
  def _log_negative_weights(negatives: Mapping[str, float]) -> None:
41
162
  """Log negative weight warnings without deduplicating keys."""
42
163
 
43
- logger.warning(NEGATIVE_WEIGHTS_MSG, negatives)
164
+ _collections_logger.warning(NEGATIVE_WEIGHTS_MSG, negatives)
44
165
 
45
166
 
46
167
  def _resolve_negative_warn_handler(
47
- warn_once: bool | Callable[[Mapping[str, float]], None]
168
+ warn_once: bool | Callable[[Mapping[str, float]], None],
48
169
  ) -> Callable[[Mapping[str, float]], None]:
49
170
  """Return a callable that logs negative weight warnings."""
50
171
 
@@ -57,6 +178,7 @@ def _resolve_negative_warn_handler(
57
178
 
58
179
  def is_non_string_sequence(obj: Any) -> bool:
59
180
  """Return ``True`` if ``obj`` is an ``Iterable`` but not string-like or a mapping."""
181
+
60
182
  return isinstance(obj, Iterable) and not isinstance(obj, (*STRING_TYPES, Mapping))
61
183
 
62
184
 
@@ -65,20 +187,7 @@ def flatten_structure(
65
187
  *,
66
188
  expand: Callable[[Any], Iterable[Any] | None] | None = None,
67
189
  ) -> 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
- """
190
+ """Yield leaf items from ``obj`` following breadth-first semantics."""
82
191
 
83
192
  stack = deque([obj])
84
193
  seen: set[int] = set()
@@ -100,30 +209,13 @@ def flatten_structure(
100
209
  yield item
101
210
 
102
211
 
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
212
  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
- """
213
+ """Default materialization limit used by :func:`ensure_collection`."""
123
214
 
124
215
 
125
216
  def normalize_materialize_limit(max_materialize: int | None) -> int | None:
126
217
  """Normalize and validate ``max_materialize`` returning a usable limit."""
218
+
127
219
  if max_materialize is None:
128
220
  return None
129
221
  limit = int(max_materialize)
@@ -138,34 +230,17 @@ def ensure_collection(
138
230
  max_materialize: int | None = MAX_MATERIALIZE_DEFAULT,
139
231
  error_msg: str | None = None,
140
232
  ) -> 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
- """
233
+ """Return ``it`` as a :class:`Collection`, materializing when needed."""
156
234
 
157
- # Step 1: early-return for collections and raw strings/bytes
158
235
  if isinstance(it, Collection):
159
236
  if isinstance(it, STRING_TYPES):
160
237
  return (cast(T, it),)
161
238
  else:
162
239
  return it
163
240
 
164
- # Step 2: ensure the input is iterable
165
241
  if not isinstance(it, Iterable):
166
242
  raise TypeError(f"{it!r} is not iterable")
167
243
 
168
- # Step 3: validate limit and materialize items once
169
244
  limit = normalize_materialize_limit(max_materialize)
170
245
  if limit is None:
171
246
  return tuple(it)
@@ -231,28 +306,8 @@ def normalize_weights(
231
306
  warn_once: bool | Callable[[Mapping[str, float]], None] = True,
232
307
  error_on_conversion: bool = False,
233
308
  ) -> 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
- """
309
+ """Normalize ``keys`` in mapping ``dict_like`` so their sum is 1."""
310
+
256
311
  weights, keys_list, total = _convert_and_validate_weights(
257
312
  dict_like,
258
313
  keys,
@@ -273,6 +328,7 @@ def normalize_counter(
273
328
  counts: Mapping[str, float | int],
274
329
  ) -> tuple[dict[str, float], float]:
275
330
  """Normalize a ``Counter`` returning proportions and total."""
331
+
276
332
  total = kahan_sum_nd(((c,) for c in counts.values()), dims=1)[0]
277
333
  if total <= 0:
278
334
  return {}, 0
@@ -287,6 +343,7 @@ def mix_groups(
287
343
  prefix: str = "_",
288
344
  ) -> dict[str, float]:
289
345
  """Aggregate values of ``dist`` according to ``groups``."""
346
+
290
347
  out: dict[str, float] = dict(dist)
291
348
  out.update(
292
349
  {
tnfr/utils/data.pyi ADDED
@@ -0,0 +1,64 @@
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
+ def convert_value(
15
+ value: Any,
16
+ conv: Callable[[Any], T],
17
+ *,
18
+ strict: bool = ...,
19
+ key: str | None = ...,
20
+ log_level: int | None = ...,
21
+ ) -> tuple[bool, T | None]: ...
22
+ def normalize_optional_int(
23
+ value: Any,
24
+ *,
25
+ sentinels: Collection[str] | None = ...,
26
+ allow_non_positive: bool = ...,
27
+ strict: bool = ...,
28
+ error_message: str | None = ...,
29
+ ) -> int | None: ...
30
+ def negative_weights_warn_once(
31
+ *,
32
+ maxsize: int = ...,
33
+ ) -> Callable[[Mapping[str, float]], None]: ...
34
+ def is_non_string_sequence(obj: Any) -> bool: ...
35
+ def flatten_structure(
36
+ obj: Any,
37
+ *,
38
+ expand: Callable[[Any], Iterable[Any] | None] | None = ...,
39
+ ) -> Iterator[Any]: ...
40
+ def normalize_materialize_limit(max_materialize: int | None) -> int | None: ...
41
+ def ensure_collection(
42
+ it: Iterable[T],
43
+ *,
44
+ max_materialize: int | None = ...,
45
+ error_msg: str | None = ...,
46
+ ) -> Collection[T]: ...
47
+ def normalize_weights(
48
+ dict_like: Mapping[str, Any],
49
+ keys: Iterable[str] | Sequence[str],
50
+ default: float = ...,
51
+ *,
52
+ error_on_negative: bool = ...,
53
+ warn_once: bool | Callable[[Mapping[str, float]], None] = ...,
54
+ error_on_conversion: bool = ...,
55
+ ) -> dict[str, float]: ...
56
+ def normalize_counter(
57
+ counts: Mapping[str, float | int],
58
+ ) -> tuple[dict[str, float], float]: ...
59
+ def mix_groups(
60
+ dist: Mapping[str, float],
61
+ groups: Mapping[str, Iterable[str]],
62
+ *,
63
+ prefix: str = ...,
64
+ ) -> dict[str, float]: ...
tnfr/utils/graph.py ADDED
@@ -0,0 +1,85 @@
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(G: GraphLike | TNFRGraph | MutableMapping[str, Any]) -> None:
66
+ """Flag ΔNFR preparation data as stale by marking ``G.graph``.
67
+
68
+ ``G`` is constrained to the :class:`~tnfr.types.GraphLike` protocol, a
69
+ concrete :class:`~tnfr.types.TNFRGraph` or an explicit metadata mapping,
70
+ ensuring the metadata storage is available for mutation.
71
+ """
72
+
73
+ graph = get_graph(G)
74
+ graph["_dnfr_prep_dirty"] = True
75
+
76
+
77
+ def supports_add_edge(graph: GraphLike | TNFRGraph) -> bool:
78
+ """Return ``True`` if ``graph`` exposes an ``add_edge`` method.
79
+
80
+ The ``graph`` parameter must implement :class:`~tnfr.types.GraphLike` or be
81
+ a :class:`~tnfr.types.TNFRGraph`, aligning runtime expectations with the
82
+ type contract enforced throughout the engine.
83
+ """
84
+
85
+ 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