tnfr 3.0.3__py3-none-any.whl → 8.5.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 (360) hide show
  1. tnfr/__init__.py +375 -56
  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 +723 -0
  8. tnfr/alias.pyi +108 -0
  9. tnfr/backends/__init__.py +354 -0
  10. tnfr/backends/jax_backend.py +173 -0
  11. tnfr/backends/numpy_backend.py +238 -0
  12. tnfr/backends/optimized_numpy.py +420 -0
  13. tnfr/backends/torch_backend.py +408 -0
  14. tnfr/cache.py +171 -0
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +110 -0
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +489 -0
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +914 -0
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +51 -0
  24. tnfr/cli/utils.pyi +7 -0
  25. tnfr/cli/validate.py +236 -0
  26. tnfr/compat/__init__.py +85 -0
  27. tnfr/compat/dataclass.py +136 -0
  28. tnfr/compat/jsonschema_stub.py +61 -0
  29. tnfr/compat/matplotlib_stub.py +73 -0
  30. tnfr/compat/numpy_stub.py +155 -0
  31. tnfr/config/__init__.py +224 -0
  32. tnfr/config/__init__.pyi +10 -0
  33. tnfr/config/constants.py +104 -0
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/config/defaults_core.py +212 -0
  37. tnfr/config/defaults_init.py +33 -0
  38. tnfr/config/defaults_metric.py +104 -0
  39. tnfr/config/feature_flags.py +81 -0
  40. tnfr/config/feature_flags.pyi +16 -0
  41. tnfr/config/glyph_constants.py +31 -0
  42. tnfr/config/init.py +77 -0
  43. tnfr/config/init.pyi +8 -0
  44. tnfr/config/operator_names.py +254 -0
  45. tnfr/config/operator_names.pyi +36 -0
  46. tnfr/config/physics_derivation.py +354 -0
  47. tnfr/config/presets.py +83 -0
  48. tnfr/config/presets.pyi +7 -0
  49. tnfr/config/security.py +927 -0
  50. tnfr/config/thresholds.py +114 -0
  51. tnfr/config/tnfr_config.py +498 -0
  52. tnfr/constants/__init__.py +92 -0
  53. tnfr/constants/__init__.pyi +92 -0
  54. tnfr/constants/aliases.py +33 -0
  55. tnfr/constants/aliases.pyi +27 -0
  56. tnfr/constants/init.py +33 -0
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +104 -0
  59. tnfr/constants/metric.pyi +19 -0
  60. tnfr/core/__init__.py +33 -0
  61. tnfr/core/container.py +226 -0
  62. tnfr/core/default_implementations.py +329 -0
  63. tnfr/core/interfaces.py +279 -0
  64. tnfr/dynamics/__init__.py +238 -0
  65. tnfr/dynamics/__init__.pyi +83 -0
  66. tnfr/dynamics/adaptation.py +267 -0
  67. tnfr/dynamics/adaptation.pyi +7 -0
  68. tnfr/dynamics/adaptive_sequences.py +189 -0
  69. tnfr/dynamics/adaptive_sequences.pyi +14 -0
  70. tnfr/dynamics/aliases.py +23 -0
  71. tnfr/dynamics/aliases.pyi +19 -0
  72. tnfr/dynamics/bifurcation.py +232 -0
  73. tnfr/dynamics/canonical.py +229 -0
  74. tnfr/dynamics/canonical.pyi +48 -0
  75. tnfr/dynamics/coordination.py +385 -0
  76. tnfr/dynamics/coordination.pyi +25 -0
  77. tnfr/dynamics/dnfr.py +3034 -0
  78. tnfr/dynamics/dnfr.pyi +26 -0
  79. tnfr/dynamics/dynamic_limits.py +225 -0
  80. tnfr/dynamics/feedback.py +252 -0
  81. tnfr/dynamics/feedback.pyi +24 -0
  82. tnfr/dynamics/fused_dnfr.py +454 -0
  83. tnfr/dynamics/homeostasis.py +157 -0
  84. tnfr/dynamics/homeostasis.pyi +14 -0
  85. tnfr/dynamics/integrators.py +661 -0
  86. tnfr/dynamics/integrators.pyi +36 -0
  87. tnfr/dynamics/learning.py +310 -0
  88. tnfr/dynamics/learning.pyi +33 -0
  89. tnfr/dynamics/metabolism.py +254 -0
  90. tnfr/dynamics/nbody.py +796 -0
  91. tnfr/dynamics/nbody_tnfr.py +783 -0
  92. tnfr/dynamics/propagation.py +326 -0
  93. tnfr/dynamics/runtime.py +908 -0
  94. tnfr/dynamics/runtime.pyi +77 -0
  95. tnfr/dynamics/sampling.py +36 -0
  96. tnfr/dynamics/sampling.pyi +7 -0
  97. tnfr/dynamics/selectors.py +711 -0
  98. tnfr/dynamics/selectors.pyi +85 -0
  99. tnfr/dynamics/structural_clip.py +207 -0
  100. tnfr/errors/__init__.py +37 -0
  101. tnfr/errors/contextual.py +492 -0
  102. tnfr/execution.py +223 -0
  103. tnfr/execution.pyi +45 -0
  104. tnfr/extensions/__init__.py +205 -0
  105. tnfr/extensions/__init__.pyi +18 -0
  106. tnfr/extensions/base.py +173 -0
  107. tnfr/extensions/base.pyi +35 -0
  108. tnfr/extensions/business/__init__.py +71 -0
  109. tnfr/extensions/business/__init__.pyi +11 -0
  110. tnfr/extensions/business/cookbook.py +88 -0
  111. tnfr/extensions/business/cookbook.pyi +8 -0
  112. tnfr/extensions/business/health_analyzers.py +202 -0
  113. tnfr/extensions/business/health_analyzers.pyi +9 -0
  114. tnfr/extensions/business/patterns.py +183 -0
  115. tnfr/extensions/business/patterns.pyi +8 -0
  116. tnfr/extensions/medical/__init__.py +73 -0
  117. tnfr/extensions/medical/__init__.pyi +11 -0
  118. tnfr/extensions/medical/cookbook.py +88 -0
  119. tnfr/extensions/medical/cookbook.pyi +8 -0
  120. tnfr/extensions/medical/health_analyzers.py +181 -0
  121. tnfr/extensions/medical/health_analyzers.pyi +9 -0
  122. tnfr/extensions/medical/patterns.py +163 -0
  123. tnfr/extensions/medical/patterns.pyi +8 -0
  124. tnfr/flatten.py +262 -0
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +354 -0
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +377 -0
  129. tnfr/glyph_history.pyi +35 -0
  130. tnfr/glyph_runtime.py +19 -0
  131. tnfr/glyph_runtime.pyi +8 -0
  132. tnfr/immutable.py +218 -0
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +203 -0
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +10 -0
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.py +37 -0
  139. tnfr/locking.pyi +7 -0
  140. tnfr/mathematics/__init__.py +79 -0
  141. tnfr/mathematics/backend.py +453 -0
  142. tnfr/mathematics/backend.pyi +99 -0
  143. tnfr/mathematics/dynamics.py +408 -0
  144. tnfr/mathematics/dynamics.pyi +90 -0
  145. tnfr/mathematics/epi.py +391 -0
  146. tnfr/mathematics/epi.pyi +65 -0
  147. tnfr/mathematics/generators.py +242 -0
  148. tnfr/mathematics/generators.pyi +29 -0
  149. tnfr/mathematics/metrics.py +119 -0
  150. tnfr/mathematics/metrics.pyi +16 -0
  151. tnfr/mathematics/operators.py +239 -0
  152. tnfr/mathematics/operators.pyi +59 -0
  153. tnfr/mathematics/operators_factory.py +124 -0
  154. tnfr/mathematics/operators_factory.pyi +11 -0
  155. tnfr/mathematics/projection.py +87 -0
  156. tnfr/mathematics/projection.pyi +33 -0
  157. tnfr/mathematics/runtime.py +182 -0
  158. tnfr/mathematics/runtime.pyi +64 -0
  159. tnfr/mathematics/spaces.py +256 -0
  160. tnfr/mathematics/spaces.pyi +83 -0
  161. tnfr/mathematics/transforms.py +305 -0
  162. tnfr/mathematics/transforms.pyi +62 -0
  163. tnfr/metrics/__init__.py +79 -0
  164. tnfr/metrics/__init__.pyi +20 -0
  165. tnfr/metrics/buffer_cache.py +163 -0
  166. tnfr/metrics/buffer_cache.pyi +24 -0
  167. tnfr/metrics/cache_utils.py +214 -0
  168. tnfr/metrics/coherence.py +2009 -0
  169. tnfr/metrics/coherence.pyi +129 -0
  170. tnfr/metrics/common.py +158 -0
  171. tnfr/metrics/common.pyi +35 -0
  172. tnfr/metrics/core.py +316 -0
  173. tnfr/metrics/core.pyi +13 -0
  174. tnfr/metrics/diagnosis.py +833 -0
  175. tnfr/metrics/diagnosis.pyi +86 -0
  176. tnfr/metrics/emergence.py +245 -0
  177. tnfr/metrics/export.py +179 -0
  178. tnfr/metrics/export.pyi +7 -0
  179. tnfr/metrics/glyph_timing.py +379 -0
  180. tnfr/metrics/glyph_timing.pyi +81 -0
  181. tnfr/metrics/learning_metrics.py +280 -0
  182. tnfr/metrics/learning_metrics.pyi +21 -0
  183. tnfr/metrics/phase_coherence.py +351 -0
  184. tnfr/metrics/phase_compatibility.py +349 -0
  185. tnfr/metrics/reporting.py +183 -0
  186. tnfr/metrics/reporting.pyi +25 -0
  187. tnfr/metrics/sense_index.py +1203 -0
  188. tnfr/metrics/sense_index.pyi +9 -0
  189. tnfr/metrics/trig.py +373 -0
  190. tnfr/metrics/trig.pyi +13 -0
  191. tnfr/metrics/trig_cache.py +233 -0
  192. tnfr/metrics/trig_cache.pyi +10 -0
  193. tnfr/multiscale/__init__.py +32 -0
  194. tnfr/multiscale/hierarchical.py +517 -0
  195. tnfr/node.py +763 -0
  196. tnfr/node.pyi +139 -0
  197. tnfr/observers.py +255 -130
  198. tnfr/observers.pyi +31 -0
  199. tnfr/ontosim.py +144 -137
  200. tnfr/ontosim.pyi +28 -0
  201. tnfr/operators/__init__.py +1672 -0
  202. tnfr/operators/__init__.pyi +31 -0
  203. tnfr/operators/algebra.py +277 -0
  204. tnfr/operators/canonical_patterns.py +420 -0
  205. tnfr/operators/cascade.py +267 -0
  206. tnfr/operators/cycle_detection.py +358 -0
  207. tnfr/operators/definitions.py +4108 -0
  208. tnfr/operators/definitions.pyi +78 -0
  209. tnfr/operators/grammar.py +1164 -0
  210. tnfr/operators/grammar.pyi +140 -0
  211. tnfr/operators/hamiltonian.py +710 -0
  212. tnfr/operators/health_analyzer.py +809 -0
  213. tnfr/operators/jitter.py +272 -0
  214. tnfr/operators/jitter.pyi +11 -0
  215. tnfr/operators/lifecycle.py +314 -0
  216. tnfr/operators/metabolism.py +618 -0
  217. tnfr/operators/metrics.py +2138 -0
  218. tnfr/operators/network_analysis/__init__.py +27 -0
  219. tnfr/operators/network_analysis/source_detection.py +186 -0
  220. tnfr/operators/nodal_equation.py +395 -0
  221. tnfr/operators/pattern_detection.py +660 -0
  222. tnfr/operators/patterns.py +669 -0
  223. tnfr/operators/postconditions/__init__.py +38 -0
  224. tnfr/operators/postconditions/mutation.py +236 -0
  225. tnfr/operators/preconditions/__init__.py +1226 -0
  226. tnfr/operators/preconditions/coherence.py +305 -0
  227. tnfr/operators/preconditions/dissonance.py +236 -0
  228. tnfr/operators/preconditions/emission.py +128 -0
  229. tnfr/operators/preconditions/mutation.py +580 -0
  230. tnfr/operators/preconditions/reception.py +125 -0
  231. tnfr/operators/preconditions/resonance.py +364 -0
  232. tnfr/operators/registry.py +74 -0
  233. tnfr/operators/registry.pyi +9 -0
  234. tnfr/operators/remesh.py +1809 -0
  235. tnfr/operators/remesh.pyi +26 -0
  236. tnfr/operators/structural_units.py +268 -0
  237. tnfr/operators/unified_grammar.py +105 -0
  238. tnfr/parallel/__init__.py +54 -0
  239. tnfr/parallel/auto_scaler.py +234 -0
  240. tnfr/parallel/distributed.py +384 -0
  241. tnfr/parallel/engine.py +238 -0
  242. tnfr/parallel/gpu_engine.py +420 -0
  243. tnfr/parallel/monitoring.py +248 -0
  244. tnfr/parallel/partitioner.py +459 -0
  245. tnfr/py.typed +0 -0
  246. tnfr/recipes/__init__.py +22 -0
  247. tnfr/recipes/cookbook.py +743 -0
  248. tnfr/rng.py +178 -0
  249. tnfr/rng.pyi +26 -0
  250. tnfr/schemas/__init__.py +8 -0
  251. tnfr/schemas/grammar.json +94 -0
  252. tnfr/sdk/__init__.py +107 -0
  253. tnfr/sdk/__init__.pyi +19 -0
  254. tnfr/sdk/adaptive_system.py +173 -0
  255. tnfr/sdk/adaptive_system.pyi +21 -0
  256. tnfr/sdk/builders.py +370 -0
  257. tnfr/sdk/builders.pyi +51 -0
  258. tnfr/sdk/fluent.py +1121 -0
  259. tnfr/sdk/fluent.pyi +74 -0
  260. tnfr/sdk/templates.py +342 -0
  261. tnfr/sdk/templates.pyi +41 -0
  262. tnfr/sdk/utils.py +341 -0
  263. tnfr/secure_config.py +46 -0
  264. tnfr/security/__init__.py +70 -0
  265. tnfr/security/database.py +514 -0
  266. tnfr/security/subprocess.py +503 -0
  267. tnfr/security/validation.py +290 -0
  268. tnfr/selector.py +247 -0
  269. tnfr/selector.pyi +19 -0
  270. tnfr/sense.py +378 -0
  271. tnfr/sense.pyi +23 -0
  272. tnfr/services/__init__.py +17 -0
  273. tnfr/services/orchestrator.py +325 -0
  274. tnfr/sparse/__init__.py +39 -0
  275. tnfr/sparse/representations.py +492 -0
  276. tnfr/structural.py +705 -0
  277. tnfr/structural.pyi +83 -0
  278. tnfr/telemetry/__init__.py +35 -0
  279. tnfr/telemetry/cache_metrics.py +226 -0
  280. tnfr/telemetry/cache_metrics.pyi +64 -0
  281. tnfr/telemetry/nu_f.py +422 -0
  282. tnfr/telemetry/nu_f.pyi +108 -0
  283. tnfr/telemetry/verbosity.py +36 -0
  284. tnfr/telemetry/verbosity.pyi +15 -0
  285. tnfr/tokens.py +58 -0
  286. tnfr/tokens.pyi +36 -0
  287. tnfr/tools/__init__.py +20 -0
  288. tnfr/tools/domain_templates.py +478 -0
  289. tnfr/tools/sequence_generator.py +846 -0
  290. tnfr/topology/__init__.py +13 -0
  291. tnfr/topology/asymmetry.py +151 -0
  292. tnfr/trace.py +543 -0
  293. tnfr/trace.pyi +42 -0
  294. tnfr/tutorials/__init__.py +38 -0
  295. tnfr/tutorials/autonomous_evolution.py +285 -0
  296. tnfr/tutorials/interactive.py +1576 -0
  297. tnfr/tutorials/structural_metabolism.py +238 -0
  298. tnfr/types.py +775 -0
  299. tnfr/types.pyi +357 -0
  300. tnfr/units.py +68 -0
  301. tnfr/units.pyi +13 -0
  302. tnfr/utils/__init__.py +282 -0
  303. tnfr/utils/__init__.pyi +215 -0
  304. tnfr/utils/cache.py +4223 -0
  305. tnfr/utils/cache.pyi +470 -0
  306. tnfr/utils/callbacks.py +375 -0
  307. tnfr/utils/callbacks.pyi +49 -0
  308. tnfr/utils/chunks.py +108 -0
  309. tnfr/utils/chunks.pyi +22 -0
  310. tnfr/utils/data.py +428 -0
  311. tnfr/utils/data.pyi +74 -0
  312. tnfr/utils/graph.py +85 -0
  313. tnfr/utils/graph.pyi +10 -0
  314. tnfr/utils/init.py +821 -0
  315. tnfr/utils/init.pyi +80 -0
  316. tnfr/utils/io.py +559 -0
  317. tnfr/utils/io.pyi +66 -0
  318. tnfr/utils/numeric.py +114 -0
  319. tnfr/utils/numeric.pyi +21 -0
  320. tnfr/validation/__init__.py +257 -0
  321. tnfr/validation/__init__.pyi +85 -0
  322. tnfr/validation/compatibility.py +460 -0
  323. tnfr/validation/compatibility.pyi +6 -0
  324. tnfr/validation/config.py +73 -0
  325. tnfr/validation/graph.py +139 -0
  326. tnfr/validation/graph.pyi +18 -0
  327. tnfr/validation/input_validation.py +755 -0
  328. tnfr/validation/invariants.py +712 -0
  329. tnfr/validation/rules.py +253 -0
  330. tnfr/validation/rules.pyi +44 -0
  331. tnfr/validation/runtime.py +279 -0
  332. tnfr/validation/runtime.pyi +28 -0
  333. tnfr/validation/sequence_validator.py +162 -0
  334. tnfr/validation/soft_filters.py +170 -0
  335. tnfr/validation/soft_filters.pyi +32 -0
  336. tnfr/validation/spectral.py +164 -0
  337. tnfr/validation/spectral.pyi +42 -0
  338. tnfr/validation/validator.py +1266 -0
  339. tnfr/validation/window.py +39 -0
  340. tnfr/validation/window.pyi +1 -0
  341. tnfr/visualization/__init__.py +98 -0
  342. tnfr/visualization/cascade_viz.py +256 -0
  343. tnfr/visualization/hierarchy.py +284 -0
  344. tnfr/visualization/sequence_plotter.py +784 -0
  345. tnfr/viz/__init__.py +60 -0
  346. tnfr/viz/matplotlib.py +278 -0
  347. tnfr/viz/matplotlib.pyi +35 -0
  348. tnfr-8.5.0.dist-info/METADATA +573 -0
  349. tnfr-8.5.0.dist-info/RECORD +353 -0
  350. tnfr-8.5.0.dist-info/entry_points.txt +3 -0
  351. tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
  352. tnfr/constants.py +0 -183
  353. tnfr/dynamics.py +0 -543
  354. tnfr/helpers.py +0 -198
  355. tnfr/main.py +0 -37
  356. tnfr/operators.py +0 -296
  357. tnfr-3.0.3.dist-info/METADATA +0 -35
  358. tnfr-3.0.3.dist-info/RECORD +0 -13
  359. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  360. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/utils/data.py ADDED
@@ -0,0 +1,428 @@
1
+ """Utilities for manipulating collections and scalar values within TNFR."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import math
7
+ from collections import deque
8
+ from collections.abc import Collection, Iterable, Mapping, Sequence
9
+ from numbers import Real
10
+ from itertools import chain, islice
11
+ from typing import (
12
+ Any,
13
+ Callable,
14
+ Iterable as TypingIterable,
15
+ Iterator,
16
+ Literal,
17
+ TypeVar,
18
+ cast,
19
+ overload,
20
+ )
21
+
22
+ from .numeric import kahan_sum_nd
23
+ from .init import get_logger
24
+ from .init import warn_once as _warn_once_factory
25
+
26
+ T = TypeVar("T")
27
+
28
+ _collections_logger = get_logger("tnfr.utils.data.collections")
29
+ _value_logger = get_logger("tnfr.utils.data")
30
+
31
+ STRING_TYPES = (str, bytes, bytearray)
32
+
33
+ NEGATIVE_WEIGHTS_MSG = "Negative weights detected: %s"
34
+
35
+ _MAX_NEGATIVE_WARN_ONCE = 1024
36
+
37
+ __all__ = (
38
+ "convert_value",
39
+ "normalize_optional_int",
40
+ "MAX_MATERIALIZE_DEFAULT",
41
+ "normalize_materialize_limit",
42
+ "is_non_string_sequence",
43
+ "flatten_structure",
44
+ "STRING_TYPES",
45
+ "ensure_collection",
46
+ "normalize_weights",
47
+ "negative_weights_warn_once",
48
+ "normalize_counter",
49
+ "mix_groups",
50
+ )
51
+
52
+
53
+ def convert_value(
54
+ value: Any,
55
+ conv: Callable[[Any], T],
56
+ *,
57
+ strict: bool = False,
58
+ key: str | None = None,
59
+ log_level: int | None = None,
60
+ ) -> tuple[bool, T | None]:
61
+ """Attempt to convert a value and report failures."""
62
+
63
+ try:
64
+ converted = conv(value)
65
+ except (ValueError, TypeError) as exc:
66
+ if strict:
67
+ raise
68
+ level = log_level if log_level is not None else logging.DEBUG
69
+ if key is not None:
70
+ _value_logger.log(level, "Could not convert value for %r: %s", key, exc)
71
+ else:
72
+ _value_logger.log(level, "Could not convert value: %s", exc)
73
+ return False, None
74
+ if isinstance(converted, float) and not math.isfinite(converted):
75
+ if strict:
76
+ target = f"{key!r}" if key is not None else "value"
77
+ raise ValueError(f"Non-finite value {converted!r} for {target}")
78
+ level = log_level if log_level is not None else logging.DEBUG
79
+ if key is not None:
80
+ _value_logger.log(level, "Non-finite value for %r: %s", key, converted)
81
+ else:
82
+ _value_logger.log(level, "Non-finite value: %s", converted)
83
+ return False, None
84
+ return True, converted
85
+
86
+
87
+ _DEFAULT_SENTINELS = frozenset({"auto", "none", "null"})
88
+
89
+
90
+ def normalize_optional_int(
91
+ value: Any,
92
+ *,
93
+ sentinels: Collection[str] | None = _DEFAULT_SENTINELS,
94
+ allow_non_positive: bool = True,
95
+ strict: bool = False,
96
+ error_message: str | None = None,
97
+ ) -> int | None:
98
+ """Normalise optional integers shared by CLI and runtime helpers.
99
+
100
+ Parameters
101
+ ----------
102
+ value:
103
+ Arbitrary object obtained from configuration, CLI options or graph
104
+ metadata.
105
+ sentinels:
106
+ Collection of case-insensitive strings that should be interpreted as
107
+ ``None``. When ``None`` or empty, no sentinel mapping is applied.
108
+ allow_non_positive:
109
+ When ``False`` values ``<= 0`` are rejected and converted to ``None``.
110
+ strict:
111
+ When ``True`` invalid inputs raise :class:`ValueError` instead of
112
+ returning ``None``.
113
+ error_message:
114
+ Optional message used when ``strict`` mode raises due to invalid input
115
+ or disallowed non-positive values.
116
+ """
117
+
118
+ if value is None:
119
+ return None
120
+
121
+ if isinstance(value, int):
122
+ result = value
123
+ elif isinstance(value, Real):
124
+ result = int(value)
125
+ else:
126
+ text = str(value).strip()
127
+ if not text:
128
+ if strict:
129
+ raise ValueError(
130
+ error_message
131
+ or "Empty value is not allowed for configuration options."
132
+ )
133
+ return None
134
+ sentinel_set: set[str] | None = None
135
+ if sentinels:
136
+ sentinel_set = {s.lower() for s in sentinels}
137
+ lowered = text.lower()
138
+ if lowered in sentinel_set:
139
+ return None
140
+ try:
141
+ result = int(text)
142
+ except (TypeError, ValueError) as exc:
143
+ if strict:
144
+ raise ValueError(
145
+ error_message or f"Invalid integer value: {value!r}"
146
+ ) from exc
147
+ return None
148
+
149
+ if not allow_non_positive and result <= 0:
150
+ if strict:
151
+ raise ValueError(
152
+ error_message
153
+ or "Non-positive values are not permitted for this option."
154
+ )
155
+ return None
156
+
157
+ return result
158
+
159
+
160
+ def negative_weights_warn_once(
161
+ *, maxsize: int = _MAX_NEGATIVE_WARN_ONCE
162
+ ) -> Callable[[Mapping[str, float]], None]:
163
+ """Return a ``WarnOnce`` callable for negative weight warnings."""
164
+
165
+ return _warn_once_factory(
166
+ _collections_logger, NEGATIVE_WEIGHTS_MSG, maxsize=maxsize
167
+ )
168
+
169
+
170
+ def _log_negative_weights(negatives: Mapping[str, float]) -> None:
171
+ """Log negative weight warnings without deduplicating keys."""
172
+
173
+ _collections_logger.warning(NEGATIVE_WEIGHTS_MSG, negatives)
174
+
175
+
176
+ def _resolve_negative_warn_handler(
177
+ warn_once: bool | Callable[[Mapping[str, float]], None],
178
+ ) -> Callable[[Mapping[str, float]], None]:
179
+ """Return a callable that logs negative weight warnings."""
180
+
181
+ if callable(warn_once):
182
+ return warn_once
183
+ if warn_once:
184
+ return negative_weights_warn_once()
185
+ return _log_negative_weights
186
+
187
+
188
+ def is_non_string_sequence(obj: Any) -> bool:
189
+ """Return ``True`` if ``obj`` is an ``Iterable`` but not string-like or a mapping."""
190
+
191
+ return isinstance(obj, Iterable) and not isinstance(obj, (*STRING_TYPES, Mapping))
192
+
193
+
194
+ def flatten_structure(
195
+ obj: Any,
196
+ *,
197
+ expand: Callable[[Any], Iterable[Any] | None] | None = None,
198
+ ) -> Iterator[Any]:
199
+ """Yield leaf items from ``obj`` following breadth-first semantics."""
200
+
201
+ stack = deque([obj])
202
+ seen: set[int] = set()
203
+ while stack:
204
+ item = stack.pop()
205
+ item_id = id(item)
206
+ if item_id in seen:
207
+ continue
208
+ if expand is not None:
209
+ replacement = expand(item)
210
+ if replacement is not None:
211
+ seen.add(item_id)
212
+ stack.extendleft(replacement)
213
+ continue
214
+ if is_non_string_sequence(item):
215
+ seen.add(item_id)
216
+ stack.extendleft(item)
217
+ else:
218
+ yield item
219
+
220
+
221
+ MAX_MATERIALIZE_DEFAULT: int = 1000
222
+ """Default materialization limit used by :func:`ensure_collection`."""
223
+
224
+
225
+ def normalize_materialize_limit(max_materialize: int | None) -> int | None:
226
+ """Normalize and validate ``max_materialize`` returning a usable limit."""
227
+
228
+ if max_materialize is None:
229
+ return None
230
+ limit = int(max_materialize)
231
+ if limit < 0:
232
+ raise ValueError("'max_materialize' must be non-negative")
233
+ return limit
234
+
235
+
236
+ @overload
237
+ def ensure_collection(
238
+ it: Iterable[T],
239
+ *,
240
+ max_materialize: int | None = MAX_MATERIALIZE_DEFAULT,
241
+ error_msg: str | None = None,
242
+ return_view: Literal[False] = False,
243
+ ) -> Collection[T]: ...
244
+
245
+
246
+ @overload
247
+ def ensure_collection(
248
+ it: Iterable[T],
249
+ *,
250
+ max_materialize: int | None = MAX_MATERIALIZE_DEFAULT,
251
+ error_msg: str | None = None,
252
+ return_view: Literal[True],
253
+ ) -> tuple[Collection[T], TypingIterable[T]]: ...
254
+
255
+
256
+ def ensure_collection(
257
+ it: Iterable[T],
258
+ *,
259
+ max_materialize: int | None = MAX_MATERIALIZE_DEFAULT,
260
+ error_msg: str | None = None,
261
+ return_view: bool = False,
262
+ ) -> Collection[T] | tuple[Collection[T], TypingIterable[T]]:
263
+ """Return ``it`` as a :class:`Collection`, materializing when needed.
264
+
265
+ When ``return_view`` is ``True`` the function returns a tuple containing the
266
+ materialised preview and an iterable that can be used to continue streaming
267
+ from the same source after the preview limit. The preview will contain up to
268
+ ``max_materialize`` items (when the limit is enforced); when ``max_materialize``
269
+ is ``None`` the preview is empty and the returned iterable is the original
270
+ stream.
271
+ """
272
+
273
+ def _finalize(
274
+ collection: Collection[T],
275
+ view: TypingIterable[T] | None = None,
276
+ ) -> Collection[T] | tuple[Collection[T], TypingIterable[T]]:
277
+ if not return_view:
278
+ return collection
279
+ if view is None:
280
+ return collection, collection
281
+ return collection, view
282
+
283
+ if isinstance(it, Collection):
284
+ if isinstance(it, STRING_TYPES):
285
+ wrapped = (cast(T, it),)
286
+ return _finalize(wrapped)
287
+ return _finalize(cast(Collection[T], it), cast(TypingIterable[T], it))
288
+
289
+ if isinstance(it, STRING_TYPES):
290
+ wrapped = (cast(T, it),)
291
+ return _finalize(wrapped)
292
+
293
+ if not isinstance(it, Iterable):
294
+ raise TypeError(f"{it!r} is not iterable")
295
+
296
+ limit = normalize_materialize_limit(max_materialize)
297
+
298
+ if return_view:
299
+ if limit is None:
300
+ return (), cast(TypingIterable[T], it)
301
+ if limit == 0:
302
+ return (), ()
303
+
304
+ iterator = iter(it)
305
+ preview = tuple(islice(iterator, limit + 1))
306
+ if len(preview) > limit:
307
+ examples = ", ".join(repr(x) for x in preview[:3])
308
+ msg = error_msg or (
309
+ f"Iterable produced {len(preview)} items, exceeds limit {limit}; first items: [{examples}]"
310
+ )
311
+ raise ValueError(msg)
312
+ if not preview:
313
+ return (), iterator
314
+ return preview, chain(preview, iterator)
315
+
316
+ if limit is None:
317
+ return tuple(it)
318
+ if limit == 0:
319
+ return ()
320
+
321
+ items = tuple(islice(it, limit + 1))
322
+ if len(items) > limit:
323
+ examples = ", ".join(repr(x) for x in items[:3])
324
+ msg = error_msg or (
325
+ f"Iterable produced {len(items)} items, exceeds limit {limit}; first items: [{examples}]"
326
+ )
327
+ raise ValueError(msg)
328
+ return items
329
+
330
+
331
+ def _convert_and_validate_weights(
332
+ dict_like: Mapping[str, Any],
333
+ keys: Iterable[str] | Sequence[str],
334
+ default: float,
335
+ *,
336
+ error_on_conversion: bool,
337
+ error_on_negative: bool,
338
+ warn_once: bool | Callable[[Mapping[str, float]], None],
339
+ ) -> tuple[dict[str, float], list[str], float]:
340
+ """Return converted weights, deduplicated keys and the accumulated total."""
341
+
342
+ keys_list = list(dict.fromkeys(keys))
343
+ default_float = float(default)
344
+
345
+ def convert(k: str) -> float:
346
+ ok, val = convert_value(
347
+ dict_like.get(k, default_float),
348
+ float,
349
+ strict=error_on_conversion,
350
+ key=k,
351
+ log_level=logging.WARNING,
352
+ )
353
+ return cast(float, val) if ok else default_float
354
+
355
+ weights = {k: convert(k) for k in keys_list}
356
+ negatives = {k: w for k, w in weights.items() if w < 0}
357
+ total = kahan_sum_nd(((w,) for w in weights.values()), dims=1)[0]
358
+
359
+ if negatives:
360
+ if error_on_negative:
361
+ raise ValueError(NEGATIVE_WEIGHTS_MSG % negatives)
362
+ warn_negative = _resolve_negative_warn_handler(warn_once)
363
+ warn_negative(negatives)
364
+ for key, weight in negatives.items():
365
+ weights[key] = 0.0
366
+ total -= weight
367
+
368
+ return weights, keys_list, total
369
+
370
+
371
+ def normalize_weights(
372
+ dict_like: Mapping[str, Any],
373
+ keys: Iterable[str] | Sequence[str],
374
+ default: float = 0.0,
375
+ *,
376
+ error_on_negative: bool = False,
377
+ warn_once: bool | Callable[[Mapping[str, float]], None] = True,
378
+ error_on_conversion: bool = False,
379
+ ) -> dict[str, float]:
380
+ """Normalize ``keys`` in mapping ``dict_like`` so their sum is 1."""
381
+
382
+ weights, keys_list, total = _convert_and_validate_weights(
383
+ dict_like,
384
+ keys,
385
+ default,
386
+ error_on_conversion=error_on_conversion,
387
+ error_on_negative=error_on_negative,
388
+ warn_once=warn_once,
389
+ )
390
+ if not keys_list:
391
+ return {}
392
+ if total <= 0:
393
+ uniform = 1.0 / len(keys_list)
394
+ return {k: uniform for k in keys_list}
395
+ return {k: w / total for k, w in weights.items()}
396
+
397
+
398
+ def normalize_counter(
399
+ counts: Mapping[str, float | int],
400
+ ) -> tuple[dict[str, float], float]:
401
+ """Normalize a ``Counter`` returning proportions and total."""
402
+
403
+ total = kahan_sum_nd(((c,) for c in counts.values()), dims=1)[0]
404
+ if total <= 0:
405
+ return {}, 0
406
+ dist = {k: v / total for k, v in counts.items() if v}
407
+ return dist, total
408
+
409
+
410
+ def mix_groups(
411
+ dist: Mapping[str, float],
412
+ groups: Mapping[str, Iterable[str]],
413
+ *,
414
+ prefix: str = "_",
415
+ ) -> dict[str, float]:
416
+ """Aggregate values of ``dist`` according to ``groups``."""
417
+
418
+ out: dict[str, float] = dict(dist)
419
+ out.update(
420
+ {
421
+ f"{prefix}{label}": kahan_sum_nd(
422
+ ((dist.get(k, 0.0),) for k in keys),
423
+ dims=1,
424
+ )[0]
425
+ for label, keys in groups.items()
426
+ }
427
+ )
428
+ return out
tnfr/utils/data.pyi ADDED
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence
4
+ from typing import Any, Callable, Literal, TypeVar, overload
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
+ @overload
42
+ def ensure_collection(
43
+ it: Iterable[T],
44
+ *,
45
+ max_materialize: int | None = ...,
46
+ error_msg: str | None = ...,
47
+ return_view: Literal[False] = ...,
48
+ ) -> Collection[T]: ...
49
+ @overload
50
+ def ensure_collection(
51
+ it: Iterable[T],
52
+ *,
53
+ max_materialize: int | None = ...,
54
+ error_msg: str | None = ...,
55
+ return_view: Literal[True],
56
+ ) -> tuple[Collection[T], Iterable[T]]: ...
57
+ def normalize_weights(
58
+ dict_like: Mapping[str, Any],
59
+ keys: Iterable[str] | Sequence[str],
60
+ default: float = ...,
61
+ *,
62
+ error_on_negative: bool = ...,
63
+ warn_once: bool | Callable[[Mapping[str, float]], None] = ...,
64
+ error_on_conversion: bool = ...,
65
+ ) -> dict[str, float]: ...
66
+ def normalize_counter(
67
+ counts: Mapping[str, float | int],
68
+ ) -> tuple[dict[str, float], float]: ...
69
+ def mix_groups(
70
+ dist: Mapping[str, float],
71
+ groups: Mapping[str, Iterable[str]],
72
+ *,
73
+ prefix: str = ...,
74
+ ) -> 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