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
@@ -0,0 +1,13 @@
1
+ """Topological analysis utilities for TNFR networks.
2
+
3
+ This module provides tools for analyzing the topological structure of TNFR
4
+ networks, including asymmetry measures and structural disruption detection.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .asymmetry import compute_topological_asymmetry
10
+
11
+ __all__ = [
12
+ "compute_topological_asymmetry",
13
+ ]
@@ -0,0 +1,151 @@
1
+ """Topological asymmetry analysis for TNFR networks.
2
+
3
+ This module provides functions to measure local topological asymmetry around
4
+ nodes in a TNFR network. According to TNFR canonical theory, the OZ (Dissonance)
5
+ operator introduces topological disruption that breaks structural symmetry.
6
+
7
+ The asymmetry measure quantifies this symmetry breaking by analyzing the
8
+ heterogeneity of the node's ego-network (the node and its immediate neighbors).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from ..types import NodeId, TNFRGraph
17
+
18
+ __all__ = [
19
+ "compute_topological_asymmetry",
20
+ ]
21
+
22
+
23
+ def compute_topological_asymmetry(G: "TNFRGraph", node: "NodeId") -> float:
24
+ """Measure local topological asymmetry around node.
25
+
26
+ Uses ego-graph analysis to detect structural symmetry breaking introduced
27
+ by dissonance operators. According to TNFR canonical theory (§2.3.3, R4),
28
+ OZ (Dissonance) introduces **topological disruption**, not just numerical
29
+ instability.
30
+
31
+ The asymmetry is computed by analyzing the heterogeneity of degree and
32
+ clustering distributions in the node's 1-hop neighborhood (ego-graph).
33
+ Higher asymmetry indicates successful structural disruption.
34
+
35
+ Parameters
36
+ ----------
37
+ G : TNFRGraph
38
+ Graph containing the node
39
+ node : NodeId
40
+ Node identifier to analyze
41
+
42
+ Returns
43
+ -------
44
+ float
45
+ Topological asymmetry measure, range [0.0, 1.0]:
46
+ - 0.0 = perfect local symmetry (homogeneous neighborhood)
47
+ - 1.0 = maximal asymmetry (heterogeneous structure)
48
+
49
+ Notes
50
+ -----
51
+ The asymmetry measure combines two components:
52
+
53
+ 1. **Degree heterogeneity**: Coefficient of variation (CV) of node degrees
54
+ in the ego-graph. Measures structural connectivity variation.
55
+
56
+ 2. **Clustering heterogeneity**: CV of local clustering coefficients.
57
+ Measures variation in local cohesion patterns.
58
+
59
+ The final asymmetry score is a weighted combination:
60
+ asymmetry = 0.6 * degree_cv + 0.4 * clustering_cv
61
+
62
+ For isolated nodes or very small neighborhoods (≤2 nodes), returns 0.0
63
+ as there is insufficient structure for meaningful asymmetry measurement.
64
+
65
+ Examples
66
+ --------
67
+ >>> from tnfr.structural import create_nfr
68
+ >>> from tnfr.operators.definitions import Dissonance
69
+ >>> from tnfr.topology import compute_topological_asymmetry
70
+ >>>
71
+ >>> G, node = create_nfr("test", epi=0.5, vf=1.0)
72
+ >>> # Add neighbors to create network structure
73
+ >>> for i in range(4):
74
+ ... neighbor = f"n{i}"
75
+ ... G.add_node(neighbor)
76
+ ... G.add_edge(node, neighbor)
77
+ >>>
78
+ >>> # Measure asymmetry before dissonance
79
+ >>> asym_before = compute_topological_asymmetry(G, node)
80
+ >>>
81
+ >>> # Apply dissonance operator
82
+ >>> Dissonance()(G, node)
83
+ >>>
84
+ >>> # Measure asymmetry after dissonance
85
+ >>> asym_after = compute_topological_asymmetry(G, node)
86
+ >>>
87
+ >>> # Dissonance should increase asymmetry (topological disruption)
88
+ >>> assert asym_after >= asym_before
89
+
90
+ See Also
91
+ --------
92
+ tnfr.operators.definitions.Dissonance : OZ operator that introduces dissonance
93
+ tnfr.operators.metrics.dissonance_metrics : Collects asymmetry in metrics
94
+ """
95
+ import networkx as nx
96
+
97
+ from ..utils import get_numpy
98
+
99
+ np = get_numpy()
100
+
101
+ # Extract 1-hop ego graph (node + immediate neighbors)
102
+ try:
103
+ ego = nx.ego_graph(G, node, radius=1)
104
+ except nx.NetworkXError:
105
+ return 0.0
106
+
107
+ n_nodes = ego.number_of_nodes()
108
+
109
+ if n_nodes <= 2:
110
+ # Too small for meaningful asymmetry
111
+ # Isolated node (n=1) or single connection (n=2) are trivially symmetric
112
+ return 0.0
113
+
114
+ # Compute degree heterogeneity in ego-graph
115
+ degrees = [ego.degree(n) for n in ego.nodes()]
116
+
117
+ if not degrees or all(d == 0 for d in degrees):
118
+ # No edges in ego graph - symmetric by definition
119
+ return 0.0
120
+
121
+ degrees_arr = np.array(degrees, dtype=float)
122
+ mean_degree = np.mean(degrees_arr)
123
+
124
+ if mean_degree < 1e-10:
125
+ degree_cv = 0.0
126
+ else:
127
+ std_degree = np.std(degrees_arr)
128
+ degree_cv = std_degree / mean_degree
129
+
130
+ # Compute clustering heterogeneity in ego-graph
131
+ try:
132
+ clustering = [nx.clustering(ego, n) for n in ego.nodes()]
133
+ except (ZeroDivisionError, nx.NetworkXError):
134
+ # If clustering computation fails, use only degree asymmetry
135
+ clustering = [0.0] * n_nodes
136
+
137
+ clustering_arr = np.array(clustering, dtype=float)
138
+ mean_clustering = np.mean(clustering_arr)
139
+
140
+ if mean_clustering < 1e-10:
141
+ clustering_cv = 0.0
142
+ else:
143
+ std_clustering = np.std(clustering_arr)
144
+ clustering_cv = std_clustering / mean_clustering
145
+
146
+ # Combined asymmetry score (weighted)
147
+ # Degree asymmetry is primary (60%), clustering is secondary (40%)
148
+ asymmetry = 0.6 * degree_cv + 0.4 * clustering_cv
149
+
150
+ # Clip to [0, 1] range
151
+ return float(np.clip(asymmetry, 0.0, 1.0))
tnfr/trace.py ADDED
@@ -0,0 +1,543 @@
1
+ """Trace logging.
2
+
3
+ Field helpers avoid unnecessary copying by reusing dictionaries stored on
4
+ the graph whenever possible. Callers are expected to treat returned
5
+ structures as immutable snapshots.
6
+
7
+ Immutability Guarantees
8
+ -----------------------
9
+ Trace field producers return mappings wrapped in ``MappingProxyType`` to
10
+ prevent accidental mutation. These proxies enforce immutability while avoiding
11
+ unnecessary data copying. Consumers that need to modify trace data should
12
+ create mutable copies using ``dict(proxy)`` or merge patterns like
13
+ ``{**proxy1, **proxy2, "new_key": value}``.
14
+
15
+ Example safe mutation patterns::
16
+
17
+ # Get immutable trace data
18
+ result = gamma_field(G)
19
+ gamma_proxy = result["gamma"]
20
+
21
+ # Cannot mutate directly (TypeError will be raised)
22
+ # gamma_proxy["new_key"] = value # ❌ Error!
23
+
24
+ # Safe pattern: create mutable copy
25
+ mutable = dict(gamma_proxy)
26
+ mutable["new_key"] = value # ✓ OK
27
+
28
+ # Safe pattern: merge with new data
29
+ combined = {**gamma_proxy, "new_key": value} # ✓ OK
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import warnings
35
+ from collections.abc import Iterable, Mapping
36
+ from types import MappingProxyType
37
+ from typing import Any, NamedTuple, Protocol, cast
38
+
39
+ from .constants import TRACE
40
+ from .glyph_history import append_metric, count_glyphs, ensure_history
41
+ from .metrics.sense_index import _normalise_si_sensitivity_mapping
42
+ from .telemetry.verbosity import (
43
+ TELEMETRY_VERBOSITY_DEFAULT,
44
+ TelemetryVerbosity,
45
+ )
46
+ from .types import (
47
+ SigmaVector,
48
+ TNFRGraph,
49
+ TraceCallback,
50
+ TraceFieldFn,
51
+ TraceFieldMap,
52
+ TraceFieldRegistry,
53
+ TraceMetadata,
54
+ TraceSnapshot,
55
+ )
56
+ from .utils import cached_import, get_graph_mapping, is_non_string_sequence
57
+ from .utils.callbacks import CallbackSpec
58
+
59
+
60
+ class _KuramotoFn(Protocol):
61
+ def __call__(self, G: TNFRGraph) -> tuple[float, float]: ...
62
+
63
+
64
+ class _SigmaVectorFn(Protocol):
65
+ def __call__(self, G: TNFRGraph, weight_mode: str | None = None) -> SigmaVector: ...
66
+
67
+
68
+ class TraceFieldSpec(NamedTuple):
69
+ """Declarative specification for a trace field producer."""
70
+
71
+ name: str
72
+ phase: str
73
+ producer: TraceFieldFn
74
+ tiers: tuple[TelemetryVerbosity, ...]
75
+
76
+
77
+ TRACE_VERBOSITY_DEFAULT = TELEMETRY_VERBOSITY_DEFAULT
78
+ TRACE_VERBOSITY_PRESETS: dict[str, tuple[str, ...]] = {}
79
+ _TRACE_CAPTURE_ALIASES: Mapping[str, str] = MappingProxyType(
80
+ {
81
+ "glyphs": "glyph_counts",
82
+ }
83
+ )
84
+
85
+
86
+ def _canonical_capture_name(name: str) -> str:
87
+ """Return the canonical capture field name for ``name``."""
88
+
89
+ stripped = name.strip()
90
+ alias = _TRACE_CAPTURE_ALIASES.get(stripped)
91
+ if alias is not None:
92
+ return alias
93
+
94
+ lowered = stripped.lower()
95
+ alias = _TRACE_CAPTURE_ALIASES.get(lowered)
96
+ if alias is not None:
97
+ return alias
98
+
99
+ return stripped
100
+
101
+
102
+ def _normalise_capture_spec(raw: Any) -> set[str]:
103
+ """Coerce custom capture payloads to a ``set`` of field names."""
104
+
105
+ if raw is None:
106
+ return set()
107
+ if isinstance(raw, Mapping):
108
+ return {_canonical_capture_name(str(name)) for name in raw.keys()}
109
+ if isinstance(raw, str):
110
+ return {_canonical_capture_name(raw)}
111
+ if isinstance(raw, Iterable):
112
+ return {_canonical_capture_name(str(name)) for name in raw}
113
+ return {_canonical_capture_name(str(raw))}
114
+
115
+
116
+ def _resolve_trace_capture(cfg: Mapping[str, Any]) -> set[str]:
117
+ """Return the capture set declared by ``cfg`` respecting verbosity."""
118
+
119
+ if "capture" in cfg:
120
+ return _normalise_capture_spec(cfg.get("capture"))
121
+
122
+ raw_verbosity = cfg.get("verbosity", TRACE_VERBOSITY_DEFAULT)
123
+ verbosity = str(raw_verbosity).lower()
124
+ fields = TRACE_VERBOSITY_PRESETS.get(verbosity)
125
+ if fields is None:
126
+ warnings.warn(
127
+ (
128
+ "Unknown TRACE verbosity %r; falling back to %s"
129
+ % (raw_verbosity, TRACE_VERBOSITY_DEFAULT)
130
+ ),
131
+ UserWarning,
132
+ stacklevel=3,
133
+ )
134
+ fields = TRACE_VERBOSITY_PRESETS[TRACE_VERBOSITY_DEFAULT]
135
+ return set(fields)
136
+
137
+
138
+ def _kuramoto_fallback(G: TNFRGraph) -> tuple[float, float]:
139
+ return 0.0, 0.0
140
+
141
+
142
+ kuramoto_R_psi: _KuramotoFn = cast(
143
+ _KuramotoFn,
144
+ cached_import("tnfr.gamma", "kuramoto_R_psi", fallback=_kuramoto_fallback),
145
+ )
146
+
147
+
148
+ def _sigma_fallback(G: TNFRGraph, _weight_mode: str | None = None) -> SigmaVector:
149
+ """Return a null sigma vector regardless of ``_weight_mode``."""
150
+
151
+ return {"x": 0.0, "y": 0.0, "mag": 0.0, "angle": 0.0, "n": 0}
152
+
153
+
154
+ # Public exports for this module
155
+ __all__ = (
156
+ "CallbackSpec",
157
+ "TraceFieldSpec",
158
+ "TraceMetadata",
159
+ "TraceSnapshot",
160
+ "register_trace",
161
+ "register_trace_field",
162
+ "_callback_names",
163
+ "gamma_field",
164
+ "grammar_field",
165
+ )
166
+
167
+ # -------------------------
168
+ # Helpers
169
+ # -------------------------
170
+
171
+
172
+ def _trace_setup(
173
+ G: TNFRGraph,
174
+ ) -> tuple[
175
+ Mapping[str, Any] | None,
176
+ set[str],
177
+ dict[str, Any] | None,
178
+ str | None,
179
+ ]:
180
+ """Prepare common configuration for trace snapshots.
181
+
182
+ Returns the active configuration, capture set, history and key under
183
+ which metadata will be stored. If tracing is disabled returns
184
+ ``(None, set(), None, None)``.
185
+ """
186
+
187
+ cfg_raw = G.graph.get("TRACE", TRACE)
188
+ cfg = cfg_raw if isinstance(cfg_raw, Mapping) else TRACE
189
+ if not cfg.get("enabled", True):
190
+ return None, set(), None, None
191
+
192
+ capture = _resolve_trace_capture(cfg)
193
+ hist = ensure_history(G)
194
+ key = cast(str | None, cfg.get("history_key", "trace_meta"))
195
+ return cfg, capture, hist, key
196
+
197
+
198
+ def _callback_names(
199
+ callbacks: Mapping[str, CallbackSpec] | Iterable[CallbackSpec],
200
+ ) -> list[str]:
201
+ """Return callback names from ``callbacks``."""
202
+ if isinstance(callbacks, Mapping):
203
+ callbacks = callbacks.values()
204
+ return [
205
+ cb.name if cb.name is not None else str(getattr(cb.func, "__name__", "fn"))
206
+ for cb in callbacks
207
+ ]
208
+
209
+
210
+ EMPTY_MAPPING: Mapping[str, Any] = MappingProxyType({})
211
+
212
+
213
+ def mapping_field(G: TNFRGraph, graph_key: str, out_key: str) -> TraceMetadata:
214
+ """Copy mappings from ``G.graph`` into trace output."""
215
+ mapping = get_graph_mapping(
216
+ G, graph_key, f"G.graph[{graph_key!r}] is not a mapping; ignoring"
217
+ )
218
+ if mapping is None:
219
+ return {}
220
+ return {out_key: mapping}
221
+
222
+
223
+ # -------------------------
224
+ # Builders
225
+ # -------------------------
226
+
227
+
228
+ def _new_trace_meta(
229
+ G: TNFRGraph, phase: str
230
+ ) -> tuple[TraceSnapshot, set[str], dict[str, Any] | None, str | None] | None:
231
+ """Initialise trace metadata for a ``phase``.
232
+
233
+ Wraps :func:`_trace_setup` and creates the base structure with timestamp
234
+ and current phase. Returns ``None`` if tracing is disabled.
235
+ """
236
+
237
+ cfg, capture, hist, key = _trace_setup(G)
238
+ if not cfg:
239
+ return None
240
+
241
+ meta: TraceSnapshot = {"t": float(G.graph.get("_t", 0.0)), "phase": phase}
242
+ return meta, capture, hist, key
243
+
244
+
245
+ # -------------------------
246
+ # Snapshots
247
+ # -------------------------
248
+
249
+
250
+ def _trace_capture(G: TNFRGraph, phase: str, fields: TraceFieldMap) -> None:
251
+ """Capture ``fields`` for ``phase`` and store the snapshot.
252
+
253
+ A :class:`TraceSnapshot` is appended to the configured history when
254
+ tracing is active. If there is no active history or storage key the
255
+ capture is silently ignored.
256
+ """
257
+
258
+ res = _new_trace_meta(G, phase)
259
+ if not res:
260
+ return
261
+
262
+ meta, capture, hist, key = res
263
+ if not capture:
264
+ return
265
+ for name, getter in fields.items():
266
+ if name in capture:
267
+ meta.update(getter(G))
268
+ if hist is None or key is None:
269
+ return
270
+ append_metric(hist, key, meta)
271
+
272
+
273
+ # -------------------------
274
+ # Registry
275
+ # -------------------------
276
+
277
+ TRACE_FIELDS: TraceFieldRegistry = {}
278
+
279
+
280
+ def register_trace_field(phase: str, name: str, func: TraceFieldFn) -> None:
281
+ """Register ``func`` to populate trace field ``name`` during ``phase``."""
282
+
283
+ TRACE_FIELDS.setdefault(phase, {})[name] = func
284
+
285
+
286
+ def gamma_field(G: TNFRGraph) -> TraceMetadata:
287
+ """Expose γ-field metadata stored under ``G.graph['GAMMA']``."""
288
+
289
+ return mapping_field(G, "GAMMA", "gamma")
290
+
291
+
292
+ def grammar_field(G: TNFRGraph) -> TraceMetadata:
293
+ """Expose canonical grammar metadata for trace emission."""
294
+
295
+ return mapping_field(G, "GRAMMAR_CANON", "grammar")
296
+
297
+
298
+ def dnfr_weights_field(G: TNFRGraph) -> TraceMetadata:
299
+ return mapping_field(G, "DNFR_WEIGHTS", "dnfr_weights")
300
+
301
+
302
+ def selector_field(G: TNFRGraph) -> TraceMetadata:
303
+ sel = G.graph.get("glyph_selector")
304
+ selector_name = getattr(sel, "__name__", str(sel)) if sel else None
305
+ return {"selector": selector_name}
306
+
307
+
308
+ def _si_weights_field(G: TNFRGraph) -> TraceMetadata:
309
+ weights = mapping_field(G, "_Si_weights", "si_weights")
310
+ if weights:
311
+ return weights
312
+ return {"si_weights": EMPTY_MAPPING}
313
+
314
+
315
+ def _si_sensitivity_field(G: TNFRGraph) -> TraceMetadata:
316
+ mapping = get_graph_mapping(
317
+ G,
318
+ "_Si_sensitivity",
319
+ "G.graph['_Si_sensitivity'] is not a mapping; ignoring",
320
+ )
321
+ if mapping is None:
322
+ return {"si_sensitivity": EMPTY_MAPPING}
323
+
324
+ normalised = _normalise_si_sensitivity_mapping(mapping, warn=True)
325
+
326
+ if normalised != mapping:
327
+ G.graph["_Si_sensitivity"] = normalised
328
+
329
+ return {"si_sensitivity": MappingProxyType(normalised)}
330
+
331
+
332
+ def si_weights_field(G: TNFRGraph) -> TraceMetadata:
333
+ """Return sense-plane weights and sensitivity."""
334
+
335
+ weights = _si_weights_field(G)
336
+ sensitivity = _si_sensitivity_field(G)
337
+ return {**weights, **sensitivity}
338
+
339
+
340
+ def callbacks_field(G: TNFRGraph) -> TraceMetadata:
341
+ cb = G.graph.get("callbacks")
342
+ if not isinstance(cb, Mapping):
343
+ return {}
344
+ out: dict[str, list[str] | None] = {}
345
+ for phase, cb_map in cb.items():
346
+ if isinstance(cb_map, Mapping) or is_non_string_sequence(cb_map):
347
+ out[phase] = _callback_names(cb_map)
348
+ else:
349
+ out[phase] = None
350
+ return {"callbacks": out}
351
+
352
+
353
+ def thol_state_field(G: TNFRGraph) -> TraceMetadata:
354
+ th_open = 0
355
+ for _, nd in G.nodes(data=True):
356
+ st = nd.get("_GRAM", {})
357
+ if st.get("thol_open", False):
358
+ th_open += 1
359
+ return {"thol_open_nodes": th_open}
360
+
361
+
362
+ def kuramoto_field(G: TNFRGraph) -> TraceMetadata:
363
+ R, psi = kuramoto_R_psi(G)
364
+ return {"kuramoto": {"R": float(R), "psi": float(psi)}}
365
+
366
+
367
+ def sigma_field(G: TNFRGraph) -> TraceMetadata:
368
+ sigma_vector_from_graph: _SigmaVectorFn = cast(
369
+ _SigmaVectorFn,
370
+ cached_import(
371
+ "tnfr.sense",
372
+ "sigma_vector_from_graph",
373
+ fallback=_sigma_fallback,
374
+ ),
375
+ )
376
+ sv = sigma_vector_from_graph(G)
377
+ return {
378
+ "sigma": {
379
+ "x": float(sv.get("x", 0.0)),
380
+ "y": float(sv.get("y", 0.0)),
381
+ "mag": float(sv.get("mag", 0.0)),
382
+ "angle": float(sv.get("angle", 0.0)),
383
+ }
384
+ }
385
+
386
+
387
+ def glyph_counts_field(G: TNFRGraph) -> TraceMetadata:
388
+ """Return structural operator application count snapshot.
389
+
390
+ Provides a snapshot of which structural operator symbols (glyphs) have been
391
+ applied in the current step. ``count_glyphs`` already produces a fresh
392
+ mapping so no additional copy is taken. Treat the returned mapping as read-only.
393
+ """
394
+
395
+ cnt = count_glyphs(G, window=1)
396
+ return {"glyphs": cnt}
397
+
398
+
399
+ TRACE_FIELD_SPECS: tuple[TraceFieldSpec, ...] = (
400
+ TraceFieldSpec(
401
+ name="gamma",
402
+ phase="before",
403
+ producer=gamma_field,
404
+ tiers=(
405
+ TelemetryVerbosity.BASIC,
406
+ TelemetryVerbosity.DETAILED,
407
+ TelemetryVerbosity.DEBUG,
408
+ ),
409
+ ),
410
+ TraceFieldSpec(
411
+ name="grammar",
412
+ phase="before",
413
+ producer=grammar_field,
414
+ tiers=(
415
+ TelemetryVerbosity.BASIC,
416
+ TelemetryVerbosity.DETAILED,
417
+ TelemetryVerbosity.DEBUG,
418
+ ),
419
+ ),
420
+ TraceFieldSpec(
421
+ name="selector",
422
+ phase="before",
423
+ producer=selector_field,
424
+ tiers=(
425
+ TelemetryVerbosity.BASIC,
426
+ TelemetryVerbosity.DETAILED,
427
+ TelemetryVerbosity.DEBUG,
428
+ ),
429
+ ),
430
+ TraceFieldSpec(
431
+ name="dnfr_weights",
432
+ phase="before",
433
+ producer=dnfr_weights_field,
434
+ tiers=(
435
+ TelemetryVerbosity.BASIC,
436
+ TelemetryVerbosity.DETAILED,
437
+ TelemetryVerbosity.DEBUG,
438
+ ),
439
+ ),
440
+ TraceFieldSpec(
441
+ name="si_weights",
442
+ phase="before",
443
+ producer=si_weights_field,
444
+ tiers=(
445
+ TelemetryVerbosity.BASIC,
446
+ TelemetryVerbosity.DETAILED,
447
+ TelemetryVerbosity.DEBUG,
448
+ ),
449
+ ),
450
+ TraceFieldSpec(
451
+ name="callbacks",
452
+ phase="before",
453
+ producer=callbacks_field,
454
+ tiers=(
455
+ TelemetryVerbosity.BASIC,
456
+ TelemetryVerbosity.DETAILED,
457
+ TelemetryVerbosity.DEBUG,
458
+ ),
459
+ ),
460
+ TraceFieldSpec(
461
+ name="thol_open_nodes",
462
+ phase="before",
463
+ producer=thol_state_field,
464
+ tiers=(
465
+ TelemetryVerbosity.BASIC,
466
+ TelemetryVerbosity.DETAILED,
467
+ TelemetryVerbosity.DEBUG,
468
+ ),
469
+ ),
470
+ TraceFieldSpec(
471
+ name="kuramoto",
472
+ phase="after",
473
+ producer=kuramoto_field,
474
+ tiers=(TelemetryVerbosity.DETAILED, TelemetryVerbosity.DEBUG),
475
+ ),
476
+ TraceFieldSpec(
477
+ name="sigma",
478
+ phase="after",
479
+ producer=sigma_field,
480
+ tiers=(TelemetryVerbosity.DETAILED, TelemetryVerbosity.DEBUG),
481
+ ),
482
+ TraceFieldSpec(
483
+ name="glyph_counts",
484
+ phase="after",
485
+ producer=glyph_counts_field,
486
+ tiers=(TelemetryVerbosity.DEBUG,),
487
+ ),
488
+ )
489
+
490
+ TRACE_VERBOSITY_PRESETS = {
491
+ level.value: tuple(spec.name for spec in TRACE_FIELD_SPECS if level in spec.tiers)
492
+ for level in TelemetryVerbosity
493
+ }
494
+
495
+ for spec in TRACE_FIELD_SPECS:
496
+ register_trace_field(spec.phase, spec.name, spec.producer)
497
+
498
+ # -------------------------
499
+ # API
500
+ # -------------------------
501
+
502
+
503
+ def register_trace(G: TNFRGraph) -> None:
504
+ """Enable before/after-step snapshots and dump operational metadata to history.
505
+
506
+ Trace snapshots are stored as :class:`TraceSnapshot` entries in
507
+ ``G.graph['history'][TRACE.history_key]`` with:
508
+ - gamma: active Γi(R) specification
509
+ - grammar: canonical grammar configuration
510
+ - selector: glyph selector name
511
+ - dnfr_weights: ΔNFR mix declared in the engine
512
+ - si_weights: α/β/γ weights and Si sensitivity
513
+ - callbacks: callbacks registered per phase (if in
514
+ ``G.graph['callbacks']``)
515
+ - thol_open_nodes: how many nodes have an open THOL block
516
+ - kuramoto: network ``(R, ψ)``
517
+ - sigma: global sense-plane vector
518
+ - glyphs: glyph counts after the step
519
+
520
+ Field helpers reuse graph dictionaries and expect them to be treated as
521
+ immutable snapshots by consumers.
522
+ """
523
+ if G.graph.get("_trace_registered"):
524
+ return
525
+
526
+ from .utils import callback_manager
527
+
528
+ for phase in TRACE_FIELDS.keys():
529
+ event = f"{phase}_step"
530
+
531
+ def _make_cb(ph: str) -> TraceCallback:
532
+ def _cb(graph: TNFRGraph, ctx: dict[str, Any]) -> None:
533
+ del ctx
534
+
535
+ _trace_capture(graph, ph, TRACE_FIELDS.get(ph, {}))
536
+
537
+ return _cb
538
+
539
+ callback_manager.register_callback(
540
+ G, event=event, func=_make_cb(phase), name=f"trace_{phase}"
541
+ )
542
+
543
+ G.graph["_trace_registered"] = True