tnfr 4.5.2__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 (365) hide show
  1. tnfr/__init__.py +334 -50
  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 +214 -37
  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 +149 -556
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +51 -16
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +344 -32
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +676 -50
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +18 -3
  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/{constants_glyphs.py → config/constants.py} +26 -20
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/{constants/core.py → config/defaults_core.py} +59 -6
  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 +51 -133
  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 +3 -1
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +9 -15
  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 +213 -633
  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 +2699 -398
  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 +496 -102
  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 +10 -5
  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 +77 -55
  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 +29 -50
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +66 -53
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +144 -57
  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 +70 -30
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +22 -16
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +5 -241
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.pyi +7 -0
  139. tnfr/mathematics/__init__.py +79 -0
  140. tnfr/mathematics/backend.py +453 -0
  141. tnfr/mathematics/backend.pyi +99 -0
  142. tnfr/mathematics/dynamics.py +408 -0
  143. tnfr/mathematics/dynamics.pyi +90 -0
  144. tnfr/mathematics/epi.py +391 -0
  145. tnfr/mathematics/epi.pyi +65 -0
  146. tnfr/mathematics/generators.py +242 -0
  147. tnfr/mathematics/generators.pyi +29 -0
  148. tnfr/mathematics/metrics.py +119 -0
  149. tnfr/mathematics/metrics.pyi +16 -0
  150. tnfr/mathematics/operators.py +239 -0
  151. tnfr/mathematics/operators.pyi +59 -0
  152. tnfr/mathematics/operators_factory.py +124 -0
  153. tnfr/mathematics/operators_factory.pyi +11 -0
  154. tnfr/mathematics/projection.py +87 -0
  155. tnfr/mathematics/projection.pyi +33 -0
  156. tnfr/mathematics/runtime.py +182 -0
  157. tnfr/mathematics/runtime.pyi +64 -0
  158. tnfr/mathematics/spaces.py +256 -0
  159. tnfr/mathematics/spaces.pyi +83 -0
  160. tnfr/mathematics/transforms.py +305 -0
  161. tnfr/mathematics/transforms.pyi +62 -0
  162. tnfr/metrics/__init__.py +47 -9
  163. tnfr/metrics/__init__.pyi +20 -0
  164. tnfr/metrics/buffer_cache.py +163 -0
  165. tnfr/metrics/buffer_cache.pyi +24 -0
  166. tnfr/metrics/cache_utils.py +214 -0
  167. tnfr/metrics/coherence.py +1510 -330
  168. tnfr/metrics/coherence.pyi +129 -0
  169. tnfr/metrics/common.py +23 -16
  170. tnfr/metrics/common.pyi +35 -0
  171. tnfr/metrics/core.py +251 -36
  172. tnfr/metrics/core.pyi +13 -0
  173. tnfr/metrics/diagnosis.py +709 -110
  174. tnfr/metrics/diagnosis.pyi +86 -0
  175. tnfr/metrics/emergence.py +245 -0
  176. tnfr/metrics/export.py +60 -18
  177. tnfr/metrics/export.pyi +7 -0
  178. tnfr/metrics/glyph_timing.py +233 -43
  179. tnfr/metrics/glyph_timing.pyi +81 -0
  180. tnfr/metrics/learning_metrics.py +280 -0
  181. tnfr/metrics/learning_metrics.pyi +21 -0
  182. tnfr/metrics/phase_coherence.py +351 -0
  183. tnfr/metrics/phase_compatibility.py +349 -0
  184. tnfr/metrics/reporting.py +63 -28
  185. tnfr/metrics/reporting.pyi +25 -0
  186. tnfr/metrics/sense_index.py +1126 -43
  187. tnfr/metrics/sense_index.pyi +9 -0
  188. tnfr/metrics/trig.py +215 -23
  189. tnfr/metrics/trig.pyi +13 -0
  190. tnfr/metrics/trig_cache.py +148 -24
  191. tnfr/metrics/trig_cache.pyi +10 -0
  192. tnfr/multiscale/__init__.py +32 -0
  193. tnfr/multiscale/hierarchical.py +517 -0
  194. tnfr/node.py +646 -140
  195. tnfr/node.pyi +139 -0
  196. tnfr/observers.py +160 -45
  197. tnfr/observers.pyi +31 -0
  198. tnfr/ontosim.py +23 -19
  199. tnfr/ontosim.pyi +28 -0
  200. tnfr/operators/__init__.py +1358 -106
  201. tnfr/operators/__init__.pyi +31 -0
  202. tnfr/operators/algebra.py +277 -0
  203. tnfr/operators/canonical_patterns.py +420 -0
  204. tnfr/operators/cascade.py +267 -0
  205. tnfr/operators/cycle_detection.py +358 -0
  206. tnfr/operators/definitions.py +4108 -0
  207. tnfr/operators/definitions.pyi +78 -0
  208. tnfr/operators/grammar.py +1164 -0
  209. tnfr/operators/grammar.pyi +140 -0
  210. tnfr/operators/hamiltonian.py +710 -0
  211. tnfr/operators/health_analyzer.py +809 -0
  212. tnfr/operators/jitter.py +107 -38
  213. tnfr/operators/jitter.pyi +11 -0
  214. tnfr/operators/lifecycle.py +314 -0
  215. tnfr/operators/metabolism.py +618 -0
  216. tnfr/operators/metrics.py +2138 -0
  217. tnfr/operators/network_analysis/__init__.py +27 -0
  218. tnfr/operators/network_analysis/source_detection.py +186 -0
  219. tnfr/operators/nodal_equation.py +395 -0
  220. tnfr/operators/pattern_detection.py +660 -0
  221. tnfr/operators/patterns.py +669 -0
  222. tnfr/operators/postconditions/__init__.py +38 -0
  223. tnfr/operators/postconditions/mutation.py +236 -0
  224. tnfr/operators/preconditions/__init__.py +1226 -0
  225. tnfr/operators/preconditions/coherence.py +305 -0
  226. tnfr/operators/preconditions/dissonance.py +236 -0
  227. tnfr/operators/preconditions/emission.py +128 -0
  228. tnfr/operators/preconditions/mutation.py +580 -0
  229. tnfr/operators/preconditions/reception.py +125 -0
  230. tnfr/operators/preconditions/resonance.py +364 -0
  231. tnfr/operators/registry.py +74 -0
  232. tnfr/operators/registry.pyi +9 -0
  233. tnfr/operators/remesh.py +1415 -91
  234. tnfr/operators/remesh.pyi +26 -0
  235. tnfr/operators/structural_units.py +268 -0
  236. tnfr/operators/unified_grammar.py +105 -0
  237. tnfr/parallel/__init__.py +54 -0
  238. tnfr/parallel/auto_scaler.py +234 -0
  239. tnfr/parallel/distributed.py +384 -0
  240. tnfr/parallel/engine.py +238 -0
  241. tnfr/parallel/gpu_engine.py +420 -0
  242. tnfr/parallel/monitoring.py +248 -0
  243. tnfr/parallel/partitioner.py +459 -0
  244. tnfr/py.typed +0 -0
  245. tnfr/recipes/__init__.py +22 -0
  246. tnfr/recipes/cookbook.py +743 -0
  247. tnfr/rng.py +75 -151
  248. tnfr/rng.pyi +26 -0
  249. tnfr/schemas/__init__.py +8 -0
  250. tnfr/schemas/grammar.json +94 -0
  251. tnfr/sdk/__init__.py +107 -0
  252. tnfr/sdk/__init__.pyi +19 -0
  253. tnfr/sdk/adaptive_system.py +173 -0
  254. tnfr/sdk/adaptive_system.pyi +21 -0
  255. tnfr/sdk/builders.py +370 -0
  256. tnfr/sdk/builders.pyi +51 -0
  257. tnfr/sdk/fluent.py +1121 -0
  258. tnfr/sdk/fluent.pyi +74 -0
  259. tnfr/sdk/templates.py +342 -0
  260. tnfr/sdk/templates.pyi +41 -0
  261. tnfr/sdk/utils.py +341 -0
  262. tnfr/secure_config.py +46 -0
  263. tnfr/security/__init__.py +70 -0
  264. tnfr/security/database.py +514 -0
  265. tnfr/security/subprocess.py +503 -0
  266. tnfr/security/validation.py +290 -0
  267. tnfr/selector.py +59 -22
  268. tnfr/selector.pyi +19 -0
  269. tnfr/sense.py +92 -67
  270. tnfr/sense.pyi +23 -0
  271. tnfr/services/__init__.py +17 -0
  272. tnfr/services/orchestrator.py +325 -0
  273. tnfr/sparse/__init__.py +39 -0
  274. tnfr/sparse/representations.py +492 -0
  275. tnfr/structural.py +639 -263
  276. tnfr/structural.pyi +83 -0
  277. tnfr/telemetry/__init__.py +35 -0
  278. tnfr/telemetry/cache_metrics.py +226 -0
  279. tnfr/telemetry/cache_metrics.pyi +64 -0
  280. tnfr/telemetry/nu_f.py +422 -0
  281. tnfr/telemetry/nu_f.pyi +108 -0
  282. tnfr/telemetry/verbosity.py +36 -0
  283. tnfr/telemetry/verbosity.pyi +15 -0
  284. tnfr/tokens.py +2 -4
  285. tnfr/tokens.pyi +36 -0
  286. tnfr/tools/__init__.py +20 -0
  287. tnfr/tools/domain_templates.py +478 -0
  288. tnfr/tools/sequence_generator.py +846 -0
  289. tnfr/topology/__init__.py +13 -0
  290. tnfr/topology/asymmetry.py +151 -0
  291. tnfr/trace.py +300 -126
  292. tnfr/trace.pyi +42 -0
  293. tnfr/tutorials/__init__.py +38 -0
  294. tnfr/tutorials/autonomous_evolution.py +285 -0
  295. tnfr/tutorials/interactive.py +1576 -0
  296. tnfr/tutorials/structural_metabolism.py +238 -0
  297. tnfr/types.py +743 -12
  298. tnfr/types.pyi +357 -0
  299. tnfr/units.py +68 -0
  300. tnfr/units.pyi +13 -0
  301. tnfr/utils/__init__.py +282 -0
  302. tnfr/utils/__init__.pyi +215 -0
  303. tnfr/utils/cache.py +4223 -0
  304. tnfr/utils/cache.pyi +470 -0
  305. tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
  306. tnfr/utils/callbacks.pyi +49 -0
  307. tnfr/utils/chunks.py +108 -0
  308. tnfr/utils/chunks.pyi +22 -0
  309. tnfr/utils/data.py +428 -0
  310. tnfr/utils/data.pyi +74 -0
  311. tnfr/utils/graph.py +85 -0
  312. tnfr/utils/graph.pyi +10 -0
  313. tnfr/utils/init.py +821 -0
  314. tnfr/utils/init.pyi +80 -0
  315. tnfr/utils/io.py +559 -0
  316. tnfr/utils/io.pyi +66 -0
  317. tnfr/{helpers → utils}/numeric.py +51 -24
  318. tnfr/utils/numeric.pyi +21 -0
  319. tnfr/validation/__init__.py +257 -0
  320. tnfr/validation/__init__.pyi +85 -0
  321. tnfr/validation/compatibility.py +460 -0
  322. tnfr/validation/compatibility.pyi +6 -0
  323. tnfr/validation/config.py +73 -0
  324. tnfr/validation/graph.py +139 -0
  325. tnfr/validation/graph.pyi +18 -0
  326. tnfr/validation/input_validation.py +755 -0
  327. tnfr/validation/invariants.py +712 -0
  328. tnfr/validation/rules.py +253 -0
  329. tnfr/validation/rules.pyi +44 -0
  330. tnfr/validation/runtime.py +279 -0
  331. tnfr/validation/runtime.pyi +28 -0
  332. tnfr/validation/sequence_validator.py +162 -0
  333. tnfr/validation/soft_filters.py +170 -0
  334. tnfr/validation/soft_filters.pyi +32 -0
  335. tnfr/validation/spectral.py +164 -0
  336. tnfr/validation/spectral.pyi +42 -0
  337. tnfr/validation/validator.py +1266 -0
  338. tnfr/validation/window.py +39 -0
  339. tnfr/validation/window.pyi +1 -0
  340. tnfr/visualization/__init__.py +98 -0
  341. tnfr/visualization/cascade_viz.py +256 -0
  342. tnfr/visualization/hierarchy.py +284 -0
  343. tnfr/visualization/sequence_plotter.py +784 -0
  344. tnfr/viz/__init__.py +60 -0
  345. tnfr/viz/matplotlib.py +278 -0
  346. tnfr/viz/matplotlib.pyi +35 -0
  347. tnfr-8.5.0.dist-info/METADATA +573 -0
  348. tnfr-8.5.0.dist-info/RECORD +353 -0
  349. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
  350. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
  351. tnfr/collections_utils.py +0 -300
  352. tnfr/config.py +0 -32
  353. tnfr/grammar.py +0 -344
  354. tnfr/graph_utils.py +0 -84
  355. tnfr/helpers/__init__.py +0 -71
  356. tnfr/import_utils.py +0 -228
  357. tnfr/json_utils.py +0 -162
  358. tnfr/logging_utils.py +0 -116
  359. tnfr/presets.py +0 -60
  360. tnfr/validators.py +0 -84
  361. tnfr/value_utils.py +0 -59
  362. tnfr-4.5.2.dist-info/METADATA +0 -379
  363. tnfr-4.5.2.dist-info/RECORD +0 -67
  364. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  365. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/cli/execution.py CHANGED
@@ -1,48 +1,121 @@
1
+ """CLI execution helpers for running canonical TNFR programs."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  import argparse
4
-
6
+ import math
7
+ from collections import deque
8
+ from collections.abc import Iterable, Mapping, Sized
9
+ from copy import deepcopy
10
+ from importlib import import_module
5
11
  from pathlib import Path
6
- from typing import Any, Optional
12
+ from typing import Any, Optional, Sequence
7
13
 
8
- import networkx as nx # type: ignore[import-untyped]
14
+ import networkx as nx
15
+ import numpy as np
9
16
 
10
- from ..constants import METRIC_DEFAULTS
11
- from ..sense import register_sigma_callback
17
+ # Constants
18
+ TWO_PI = 2 * math.pi
19
+
20
+ from ..config import apply_config
21
+ from ..config.presets import (
22
+ PREFERRED_PRESET_NAMES,
23
+ get_preset,
24
+ )
25
+ from ..alias import get_attr
26
+ from ..constants import METRIC_DEFAULTS, VF_PRIMARY, get_aliases, get_param
27
+ from ..dynamics import default_glyph_selector, parametric_glyph_selector, run
28
+ from ..execution import CANONICAL_PRESET_NAME, play
29
+ from ..flatten import parse_program_tokens
30
+ from ..glyph_history import ensure_history
31
+ from ..mathematics import (
32
+ BasicStateProjector,
33
+ CoherenceOperator,
34
+ FrequencyOperator,
35
+ HilbertSpace,
36
+ MathematicalDynamicsEngine,
37
+ make_coherence_operator,
38
+ make_frequency_operator,
39
+ )
40
+ from ..validation import NFRValidator
12
41
  from ..metrics import (
13
- register_metrics_callbacks,
14
- glyph_top,
15
- export_metrics,
16
42
  build_metrics_summary,
43
+ export_metrics,
44
+ glyph_top,
45
+ register_metrics_callbacks,
17
46
  )
18
47
  from ..metrics.core import _metrics_step
48
+ from ..ontosim import prepare_network
49
+ from ..sense import register_sigma_callback
19
50
  from ..trace import register_trace
20
- from ..execution import CANONICAL_PRESET_NAME, play, seq
21
- from ..dynamics import (
22
- run,
23
- default_glyph_selector,
24
- parametric_glyph_selector,
25
- validate_canon,
51
+ from ..types import ProgramTokens
52
+ from ..utils import (
53
+ StructuredFileError,
54
+ clamp01,
55
+ get_logger,
56
+ json_dumps,
57
+ read_structured_file,
58
+ safe_write,
26
59
  )
27
- from ..presets import get_preset
28
- from ..config import apply_config
29
- from ..io import read_structured_file, safe_write, StructuredFileError
30
- from ..glyph_history import ensure_history
31
- from ..ontosim import preparar_red
32
- from ..logging_utils import get_logger
33
- from ..types import Glyph
34
- from ..json_utils import json_dumps
35
- from ..flatten import parse_program_tokens
36
-
37
60
  from .arguments import _args_to_dict
61
+ from .utils import _parse_cli_variants
62
+ from ..validation import validate_canon
38
63
 
39
64
  logger = get_logger(__name__)
40
65
 
66
+ _VF_ALIASES = get_aliases("VF")
67
+ VF_ALIAS_KEYS: tuple[str, ...] = (VF_PRIMARY,) + tuple(
68
+ alias for alias in _VF_ALIASES if alias != VF_PRIMARY
69
+ )
70
+
71
+ _EPI_ALIASES = get_aliases("EPI")
72
+ EPI_PRIMARY = _EPI_ALIASES[0]
73
+ EPI_ALIAS_KEYS: tuple[str, ...] = (EPI_PRIMARY,) + tuple(
74
+ alias for alias in _EPI_ALIASES if alias != EPI_PRIMARY
75
+ )
41
76
 
42
77
  # CLI summaries should remain concise by default while allowing callers to
43
78
  # inspect the full glyphogram series when needed.
44
79
  DEFAULT_SUMMARY_SERIES_LIMIT = 10
45
80
 
81
+ _PREFERRED_PRESETS_DISPLAY = ", ".join(PREFERRED_PRESET_NAMES)
82
+
83
+
84
+ def _as_iterable_view(view: Any) -> Iterable[Any]:
85
+ """Return ``view`` as an iterable, resolving callable cached views."""
86
+
87
+ if hasattr(view, "__iter__"):
88
+ return view # type: ignore[return-value]
89
+ if callable(view):
90
+ resolved = view()
91
+ if not hasattr(resolved, "__iter__"):
92
+ raise TypeError("Graph view did not return an iterable")
93
+ return resolved
94
+ return ()
95
+
96
+
97
+ def _iter_graph_nodes(graph: Any) -> Iterable[Any]:
98
+ """Yield nodes from ``graph`` normalising NetworkX-style accessors."""
99
+
100
+ return _as_iterable_view(getattr(graph, "nodes", ()))
101
+
102
+
103
+ def _iter_graph_edges(graph: Any) -> Iterable[Any]:
104
+ """Yield edges from ``graph`` normalising NetworkX-style accessors."""
105
+
106
+ return _as_iterable_view(getattr(graph, "edges", ()))
107
+
108
+
109
+ def _count_graph_nodes(graph: Any) -> int:
110
+ """Return node count honouring :class:`tnfr.types.GraphLike` semantics."""
111
+
112
+ if hasattr(graph, "number_of_nodes"):
113
+ return int(graph.number_of_nodes())
114
+ nodes_view = _iter_graph_nodes(graph)
115
+ if isinstance(nodes_view, Sized):
116
+ return len(nodes_view) # type: ignore[arg-type]
117
+ return len(tuple(nodes_view))
118
+
46
119
 
47
120
  def _save_json(path: str, data: Any) -> None:
48
121
  payload = json_dumps(data, ensure_ascii=False, indent=2, default=list)
@@ -53,11 +126,17 @@ def _attach_callbacks(G: "nx.Graph") -> None:
53
126
  register_sigma_callback(G)
54
127
  register_metrics_callbacks(G)
55
128
  register_trace(G)
129
+ history = ensure_history(G)
130
+ maxlen = int(get_param(G, "PROGRAM_TRACE_MAXLEN"))
131
+ history.setdefault("program_trace", deque(maxlen=maxlen))
132
+ history.setdefault("trace_meta", [])
56
133
  _metrics_step(G, ctx=None)
57
134
 
58
135
 
59
136
  def _persist_history(G: "nx.Graph", args: argparse.Namespace) -> None:
60
- if getattr(args, "save_history", None) or getattr(args, "export_history_base", None):
137
+ if getattr(args, "save_history", None) or getattr(
138
+ args, "export_history_base", None
139
+ ):
61
140
  history = ensure_history(G)
62
141
  if getattr(args, "save_history", None):
63
142
  _save_json(args.save_history, history)
@@ -65,7 +144,153 @@ def _persist_history(G: "nx.Graph", args: argparse.Namespace) -> None:
65
144
  export_metrics(G, args.export_history_base, fmt=args.export_format)
66
145
 
67
146
 
147
+ def _to_float_array(values: Sequence[float] | None, *, name: str) -> np.ndarray | None:
148
+ if values is None:
149
+ return None
150
+ array = np.asarray(list(values), dtype=float)
151
+ if array.ndim != 1:
152
+ raise ValueError(f"{name} must be a one-dimensional sequence of numbers")
153
+ return array
154
+
155
+
156
+ def _resolve_math_dimension(args: argparse.Namespace, fallback: int) -> int:
157
+ dimension = getattr(args, "math_dimension", None)
158
+ candidate_lengths: list[int] = []
159
+ for attr in (
160
+ "math_coherence_spectrum",
161
+ "math_frequency_diagonal",
162
+ "math_generator_diagonal",
163
+ ):
164
+ seq = getattr(args, attr, None)
165
+ if seq is not None:
166
+ candidate_lengths.append(len(seq))
167
+ if dimension is None:
168
+ if candidate_lengths:
169
+ unique = set(candidate_lengths)
170
+ if len(unique) > 1:
171
+ raise ValueError(
172
+ "Math engine configuration requires matching sequence lengths"
173
+ )
174
+ dimension = unique.pop()
175
+ else:
176
+ dimension = fallback
177
+ else:
178
+ for length in candidate_lengths:
179
+ if length != dimension:
180
+ raise ValueError(
181
+ "Math engine sequence lengths must match the requested dimension"
182
+ )
183
+ if dimension is None or dimension <= 0:
184
+ raise ValueError("Hilbert space dimension must be a positive integer")
185
+ return int(dimension)
186
+
187
+
188
+ def _build_math_engine_config(
189
+ G: "nx.Graph", args: argparse.Namespace
190
+ ) -> dict[str, Any]:
191
+ node_count = _count_graph_nodes(G)
192
+ fallback_dim = max(1, int(node_count) if node_count is not None else 1)
193
+ dimension = _resolve_math_dimension(args, fallback=fallback_dim)
194
+
195
+ coherence_spectrum = _to_float_array(
196
+ getattr(args, "math_coherence_spectrum", None),
197
+ name="--math-coherence-spectrum",
198
+ )
199
+ if coherence_spectrum is not None and coherence_spectrum.size != dimension:
200
+ raise ValueError("Coherence spectrum length must equal the Hilbert dimension")
201
+
202
+ frequency_diagonal = _to_float_array(
203
+ getattr(args, "math_frequency_diagonal", None),
204
+ name="--math-frequency-diagonal",
205
+ )
206
+ if frequency_diagonal is not None and frequency_diagonal.size != dimension:
207
+ raise ValueError("Frequency diagonal length must equal the Hilbert dimension")
208
+
209
+ generator_diagonal = _to_float_array(
210
+ getattr(args, "math_generator_diagonal", None),
211
+ name="--math-generator-diagonal",
212
+ )
213
+ if generator_diagonal is not None and generator_diagonal.size != dimension:
214
+ raise ValueError("Generator diagonal length must equal the Hilbert dimension")
215
+
216
+ coherence_c_min = getattr(args, "math_coherence_c_min", None)
217
+ if coherence_spectrum is None:
218
+ coherence_operator = make_coherence_operator(
219
+ dimension,
220
+ c_min=float(coherence_c_min) if coherence_c_min is not None else 0.1,
221
+ )
222
+ else:
223
+ if coherence_c_min is not None:
224
+ coherence_operator = CoherenceOperator(
225
+ coherence_spectrum, c_min=float(coherence_c_min)
226
+ )
227
+ else:
228
+ coherence_operator = CoherenceOperator(coherence_spectrum)
229
+ if not coherence_operator.is_positive_semidefinite():
230
+ raise ValueError("Coherence spectrum must be positive semidefinite")
231
+
232
+ frequency_matrix: np.ndarray
233
+ if frequency_diagonal is None:
234
+ frequency_matrix = np.eye(dimension, dtype=float)
235
+ else:
236
+ frequency_matrix = np.diag(frequency_diagonal)
237
+ frequency_operator = make_frequency_operator(frequency_matrix)
238
+
239
+ generator_matrix: np.ndarray
240
+ if generator_diagonal is None:
241
+ generator_matrix = np.zeros((dimension, dimension), dtype=float)
242
+ else:
243
+ generator_matrix = np.diag(generator_diagonal)
244
+
245
+ hilbert_space = HilbertSpace(dimension)
246
+ dynamics_engine = MathematicalDynamicsEngine(
247
+ generator_matrix,
248
+ hilbert_space=hilbert_space,
249
+ )
250
+
251
+ coherence_threshold = getattr(args, "math_coherence_threshold", None)
252
+ if coherence_threshold is None:
253
+ coherence_threshold = float(coherence_operator.c_min)
254
+ else:
255
+ coherence_threshold = float(coherence_threshold)
256
+
257
+ state_projector = BasicStateProjector()
258
+ validator = NFRValidator(
259
+ hilbert_space,
260
+ coherence_operator,
261
+ coherence_threshold,
262
+ frequency_operator=frequency_operator,
263
+ )
264
+
265
+ return {
266
+ "enabled": True,
267
+ "dimension": dimension,
268
+ "hilbert_space": hilbert_space,
269
+ "coherence_operator": coherence_operator,
270
+ "frequency_operator": frequency_operator,
271
+ "coherence_threshold": coherence_threshold,
272
+ "state_projector": state_projector,
273
+ "validator": validator,
274
+ "dynamics_engine": dynamics_engine,
275
+ "generator_matrix": generator_matrix,
276
+ }
277
+
278
+
279
+ def _configure_math_engine(G: "nx.Graph", args: argparse.Namespace) -> None:
280
+ if not getattr(args, "math_engine", False):
281
+ G.graph.pop("MATH_ENGINE", None)
282
+ return
283
+ try:
284
+ config = _build_math_engine_config(G, args)
285
+ except ValueError as exc:
286
+ logger.error("Math engine configuration error: %s", exc)
287
+ raise SystemExit(1) from exc
288
+ G.graph["MATH_ENGINE"] = config
289
+
290
+
68
291
  def build_basic_graph(args: argparse.Namespace) -> "nx.Graph":
292
+ """Construct the base graph topology described by CLI ``args``."""
293
+
69
294
  n = args.nodes
70
295
  topology = getattr(args, "topology", "ring").lower()
71
296
  seed = getattr(args, "seed", None)
@@ -81,7 +306,7 @@ def build_basic_graph(args: argparse.Namespace) -> "nx.Graph":
81
306
  fallback = 0.0
82
307
  else:
83
308
  fallback = 3.0 / n
84
- prob = min(max(fallback, 0.0), 1.0)
309
+ prob = clamp01(fallback)
85
310
  if not 0.0 <= prob <= 1.0:
86
311
  raise ValueError(f"p must be between 0 and 1; received {prob}")
87
312
  G = nx.gnp_random_graph(n, prob, seed=seed)
@@ -95,8 +320,14 @@ def build_basic_graph(args: argparse.Namespace) -> "nx.Graph":
95
320
 
96
321
 
97
322
  def apply_cli_config(G: "nx.Graph", args: argparse.Namespace) -> None:
323
+ """Apply CLI overrides from ``args`` to graph-level configuration."""
324
+
98
325
  if args.config:
99
- apply_config(G, Path(args.config))
326
+ try:
327
+ apply_config(G, Path(args.config))
328
+ except (StructuredFileError, ValueError) as exc:
329
+ logger.error("%s", exc)
330
+ raise SystemExit(1) from exc
100
331
  arg_map = {
101
332
  "dt": ("DT", float),
102
333
  "integrator": ("INTEGRATOR_METHOD", str),
@@ -108,8 +339,18 @@ def apply_cli_config(G: "nx.Graph", args: argparse.Namespace) -> None:
108
339
  if val is not None:
109
340
  G.graph[key] = conv(val)
110
341
 
342
+ base_gcanon: dict[str, Any]
343
+ existing_gcanon = G.graph.get("GRAMMAR_CANON")
344
+ if isinstance(existing_gcanon, Mapping):
345
+ base_gcanon = {
346
+ **METRIC_DEFAULTS["GRAMMAR_CANON"],
347
+ **dict(existing_gcanon),
348
+ }
349
+ else:
350
+ base_gcanon = dict(METRIC_DEFAULTS["GRAMMAR_CANON"])
351
+
111
352
  gcanon = {
112
- **METRIC_DEFAULTS["GRAMMAR_CANON"],
353
+ **base_gcanon,
113
354
  **_args_to_dict(args, prefix="grammar_"),
114
355
  }
115
356
  if getattr(args, "grammar_canon", None) is not None:
@@ -122,9 +363,7 @@ def apply_cli_config(G: "nx.Graph", args: argparse.Namespace) -> None:
122
363
  "basic": default_glyph_selector,
123
364
  "param": parametric_glyph_selector,
124
365
  }
125
- G.graph["glyph_selector"] = sel_map.get(
126
- selector, default_glyph_selector
127
- )
366
+ G.graph["glyph_selector"] = sel_map.get(selector, default_glyph_selector)
128
367
 
129
368
  if hasattr(args, "gamma_type"):
130
369
  G.graph["GAMMA"] = {
@@ -133,8 +372,41 @@ def apply_cli_config(G: "nx.Graph", args: argparse.Namespace) -> None:
133
372
  "R0": args.gamma_R0,
134
373
  }
135
374
 
375
+ for attr, key in (
376
+ ("trace_verbosity", "TRACE"),
377
+ ("metrics_verbosity", "METRICS"),
378
+ ):
379
+ cfg = G.graph.get(key)
380
+ if not isinstance(cfg, dict):
381
+ cfg = deepcopy(METRIC_DEFAULTS[key])
382
+ G.graph[key] = cfg
383
+ value = getattr(args, attr, None)
384
+ if value is not None:
385
+ cfg["verbosity"] = value
386
+
387
+ candidate_count = getattr(args, "um_candidate_count", None)
388
+ if candidate_count is not None:
389
+ G.graph["UM_CANDIDATE_COUNT"] = int(candidate_count)
390
+
391
+ stop_window = getattr(args, "stop_early_window", None)
392
+ stop_fraction = getattr(args, "stop_early_fraction", None)
393
+ if stop_window is not None or stop_fraction is not None:
394
+ stop_cfg = G.graph.get("STOP_EARLY")
395
+ if isinstance(stop_cfg, Mapping):
396
+ next_cfg = {**stop_cfg}
397
+ else:
398
+ next_cfg = deepcopy(METRIC_DEFAULTS["STOP_EARLY"])
399
+ if stop_window is not None:
400
+ next_cfg["window"] = int(stop_window)
401
+ if stop_fraction is not None:
402
+ next_cfg["fraction"] = float(stop_fraction)
403
+ next_cfg.setdefault("enabled", True)
404
+ G.graph["STOP_EARLY"] = next_cfg
405
+
136
406
 
137
407
  def register_callbacks_and_observer(G: "nx.Graph") -> None:
408
+ """Attach callbacks and validators required for CLI runs."""
409
+
138
410
  _attach_callbacks(G)
139
411
  validate_canon(G)
140
412
 
@@ -144,12 +416,13 @@ def _build_graph_from_args(args: argparse.Namespace) -> "nx.Graph":
144
416
  apply_cli_config(G, args)
145
417
  if getattr(args, "observer", False):
146
418
  G.graph["ATTACH_STD_OBSERVER"] = True
147
- preparar_red(G)
419
+ prepare_network(G)
148
420
  register_callbacks_and_observer(G)
421
+ _configure_math_engine(G, args)
149
422
  return G
150
423
 
151
424
 
152
- def _load_sequence(path: Path) -> list[Any]:
425
+ def _load_sequence(path: Path) -> ProgramTokens:
153
426
  try:
154
427
  data = read_structured_file(path)
155
428
  except (StructuredFileError, OSError) as exc:
@@ -159,19 +432,29 @@ def _load_sequence(path: Path) -> list[Any]:
159
432
  message = str(StructuredFileError(path, exc))
160
433
  logger.error("%s", message)
161
434
  raise SystemExit(1) from exc
435
+ if isinstance(data, Mapping) and "sequence" in data:
436
+ data = data["sequence"]
162
437
  return parse_program_tokens(data)
163
438
 
164
439
 
165
440
  def resolve_program(
166
- args: argparse.Namespace, default: Optional[Any] = None
167
- ) -> Optional[Any]:
441
+ args: argparse.Namespace, default: Optional[ProgramTokens] = None
442
+ ) -> Optional[ProgramTokens]:
443
+ """Resolve preset/sequence inputs into program tokens."""
444
+
168
445
  if getattr(args, "preset", None):
169
446
  try:
170
447
  return get_preset(args.preset)
171
448
  except KeyError as exc:
449
+ details = exc.args[0] if exc.args else "Preset lookup failed."
172
450
  logger.error(
173
- "Preset desconocido '%s'. Usa --sequence-file para cargar secuencias personalizadas",
451
+ (
452
+ "Unknown preset '%s'. Available presets: %s. %s "
453
+ "Use --sequence-file to execute custom sequences."
454
+ ),
174
455
  args.preset,
456
+ _PREFERRED_PRESETS_DISPLAY,
457
+ details,
175
458
  )
176
459
  raise SystemExit(1) from exc
177
460
  if getattr(args, "sequence_file", None):
@@ -180,8 +463,12 @@ def resolve_program(
180
463
 
181
464
 
182
465
  def run_program(
183
- G: Optional["nx.Graph"], program: Optional[Any], args: argparse.Namespace
466
+ G: Optional["nx.Graph"],
467
+ program: Optional[ProgramTokens],
468
+ args: argparse.Namespace,
184
469
  ) -> "nx.Graph":
470
+ """Execute ``program`` (or timed run) on ``G`` using CLI options."""
471
+
185
472
  if G is None:
186
473
  G = _build_graph_from_args(args)
187
474
 
@@ -197,6 +484,13 @@ def run_program(
197
484
  if value is not None:
198
485
  run_kwargs[attr] = value
199
486
 
487
+ job_overrides: dict[str, Any] = {}
488
+ dnfr_jobs = getattr(args, "dnfr_n_jobs", None)
489
+ if dnfr_jobs is not None:
490
+ job_overrides["dnfr_n_jobs"] = int(dnfr_jobs)
491
+ if job_overrides:
492
+ run_kwargs["n_jobs"] = job_overrides
493
+
200
494
  run(G, steps=steps, **run_kwargs)
201
495
  else:
202
496
  play(G, program)
@@ -208,7 +502,7 @@ def run_program(
208
502
  def _run_cli_program(
209
503
  args: argparse.Namespace,
210
504
  *,
211
- default_program: Optional[Any] = None,
505
+ default_program: Optional[ProgramTokens] = None,
212
506
  graph: Optional["nx.Graph"] = None,
213
507
  ) -> tuple[int, Optional["nx.Graph"]]:
214
508
  try:
@@ -217,10 +511,128 @@ def _run_cli_program(
217
511
  code = exc.code if isinstance(exc.code, int) else 1
218
512
  return code or 1, None
219
513
 
220
- result_graph = run_program(graph, program, args)
514
+ try:
515
+ result_graph = run_program(graph, program, args)
516
+ except SystemExit as exc:
517
+ code = exc.code if isinstance(exc.code, int) else 1
518
+ return code or 1, None
221
519
  return 0, result_graph
222
520
 
223
521
 
522
+ def _log_math_engine_summary(G: "nx.Graph") -> None:
523
+ math_cfg = G.graph.get("MATH_ENGINE")
524
+ if not isinstance(math_cfg, Mapping) or not math_cfg.get("enabled"):
525
+ return
526
+
527
+ nodes = list(G.nodes)
528
+ if not nodes:
529
+ logger.info("[MATH] Math engine validation skipped: no nodes present")
530
+ return
531
+
532
+ hilbert_space: HilbertSpace = math_cfg["hilbert_space"]
533
+ coherence_operator: CoherenceOperator = math_cfg["coherence_operator"]
534
+ frequency_operator: FrequencyOperator | None = math_cfg.get("frequency_operator")
535
+ state_projector: BasicStateProjector = math_cfg.get(
536
+ "state_projector", BasicStateProjector()
537
+ )
538
+ validator: NFRValidator | None = math_cfg.get("validator")
539
+ if validator is None:
540
+ coherence_threshold = math_cfg.get("coherence_threshold")
541
+ validator = NFRValidator(
542
+ hilbert_space,
543
+ coherence_operator,
544
+ float(coherence_threshold) if coherence_threshold is not None else 0.0,
545
+ frequency_operator=frequency_operator,
546
+ )
547
+ math_cfg["validator"] = validator
548
+
549
+ enforce_frequency = bool(frequency_operator is not None)
550
+
551
+ norm_values: list[float] = []
552
+ normalized_flags: list[bool] = []
553
+ coherence_flags: list[bool] = []
554
+ coherence_values: list[float] = []
555
+ coherence_threshold: float | None = None
556
+ frequency_flags: list[bool] = []
557
+ frequency_values: list[float] = []
558
+ frequency_spectrum_min: float | None = None
559
+
560
+ for node_id in nodes:
561
+ data = G.nodes[node_id]
562
+ epi = float(
563
+ get_attr(
564
+ data,
565
+ EPI_ALIAS_KEYS,
566
+ default=0.0,
567
+ )
568
+ )
569
+ nu_f = float(
570
+ get_attr(
571
+ data,
572
+ VF_ALIAS_KEYS,
573
+ default=float(data.get(VF_PRIMARY, 0.0)),
574
+ )
575
+ )
576
+ theta = float(data.get("theta", 0.0))
577
+ state = state_projector(
578
+ epi=epi, nu_f=nu_f, theta=theta, dim=hilbert_space.dimension
579
+ )
580
+ norm_values.append(float(hilbert_space.norm(state)))
581
+ outcome = validator.validate(
582
+ state,
583
+ enforce_frequency_positivity=enforce_frequency,
584
+ )
585
+ summary = outcome.summary
586
+ normalized_flags.append(bool(summary.get("normalized", False)))
587
+
588
+ coherence_summary = summary.get("coherence")
589
+ if isinstance(coherence_summary, Mapping):
590
+ coherence_flags.append(bool(coherence_summary.get("passed", False)))
591
+ coherence_values.append(float(coherence_summary.get("value", 0.0)))
592
+ if coherence_threshold is None and "threshold" in coherence_summary:
593
+ coherence_threshold = float(coherence_summary.get("threshold", 0.0))
594
+
595
+ frequency_summary = summary.get("frequency")
596
+ if isinstance(frequency_summary, Mapping):
597
+ frequency_flags.append(bool(frequency_summary.get("passed", False)))
598
+ frequency_values.append(float(frequency_summary.get("value", 0.0)))
599
+ if frequency_spectrum_min is None and "spectrum_min" in frequency_summary:
600
+ frequency_spectrum_min = float(
601
+ frequency_summary.get("spectrum_min", 0.0)
602
+ )
603
+
604
+ if norm_values:
605
+ logger.info(
606
+ "[MATH] Hilbert norm preserved=%s (min=%.6f, max=%.6f)",
607
+ all(normalized_flags),
608
+ min(norm_values),
609
+ max(norm_values),
610
+ )
611
+
612
+ if coherence_values and coherence_threshold is not None:
613
+ logger.info(
614
+ "[MATH] Coherence ≥ C_min=%s (C_min=%.6f, min=%.6f)",
615
+ all(coherence_flags),
616
+ float(coherence_threshold),
617
+ min(coherence_values),
618
+ )
619
+
620
+ if frequency_values:
621
+ if frequency_spectrum_min is not None:
622
+ logger.info(
623
+ "[MATH] νf positivity=%s (min=%.6f, spectrum_min=%.6f)",
624
+ all(frequency_flags),
625
+ min(frequency_values),
626
+ frequency_spectrum_min,
627
+ )
628
+ else:
629
+ logger.info(
630
+ "[MATH] νf positivity=%s (min=%.6f)",
631
+ all(frequency_flags),
632
+ min(frequency_values),
633
+ )
634
+
635
+
224
636
  def _log_run_summaries(G: "nx.Graph", args: argparse.Namespace) -> None:
225
637
  cfg_coh = G.graph.get("COHERENCE", METRIC_DEFAULTS["COHERENCE"])
226
638
  cfg_diag = G.graph.get("DIAGNOSIS", METRIC_DEFAULTS["DIAGNOSIS"])
@@ -229,26 +641,30 @@ def _log_run_summaries(G: "nx.Graph", args: argparse.Namespace) -> None:
229
641
  if cfg_coh.get("enabled", True):
230
642
  Wstats = hist.get(cfg_coh.get("stats_history_key", "W_stats"), [])
231
643
  if Wstats:
232
- logger.info("[COHERENCE] último paso: %s", Wstats[-1])
644
+ logger.info("[COHERENCE] last step: %s", Wstats[-1])
233
645
 
234
646
  if cfg_diag.get("enabled", True):
235
647
  last_diag = hist.get(cfg_diag.get("history_key", "nodal_diag"), [])
236
648
  if last_diag:
237
649
  sample = list(last_diag[-1].values())[:3]
238
- logger.info("[DIAGNOSIS] ejemplo: %s", sample)
650
+ logger.info("[DIAGNOSIS] sample: %s", sample)
239
651
 
240
652
  if args.summary:
241
653
  summary_limit = getattr(args, "summary_limit", DEFAULT_SUMMARY_SERIES_LIMIT)
242
654
  summary, has_latency_values = build_metrics_summary(
243
655
  G, series_limit=summary_limit
244
656
  )
245
- logger.info("Tg global: %s", summary["Tg_global"])
246
- logger.info("Top operadores por Tg: %s", glyph_top(G, k=5))
657
+ logger.info("Global Tg: %s", summary["Tg_global"])
658
+ logger.info("Top operators by Tg: %s", glyph_top(G, k=5))
247
659
  if has_latency_values:
248
- logger.info("Latencia media: %s", summary["latency_mean"])
660
+ logger.info("Average latency: %s", summary["latency_mean"])
661
+
662
+ _log_math_engine_summary(G)
249
663
 
250
664
 
251
665
  def cmd_run(args: argparse.Namespace) -> int:
666
+ """Execute ``tnfr run`` returning the exit status."""
667
+
252
668
  code, graph = _run_cli_program(args)
253
669
  if code != 0:
254
670
  return code
@@ -259,18 +675,18 @@ def cmd_run(args: argparse.Namespace) -> int:
259
675
 
260
676
 
261
677
  def cmd_sequence(args: argparse.Namespace) -> int:
678
+ """Execute ``tnfr sequence`` returning the exit status."""
679
+
262
680
  if args.preset and args.sequence_file:
263
- logger.error(
264
- "No se puede usar --preset y --sequence-file al mismo tiempo"
265
- )
681
+ logger.error("Cannot use --preset and --sequence-file at the same time")
266
682
  return 1
267
- code, _ = _run_cli_program(
268
- args, default_program=get_preset(CANONICAL_PRESET_NAME)
269
- )
683
+ code, _ = _run_cli_program(args, default_program=get_preset(CANONICAL_PRESET_NAME))
270
684
  return code
271
685
 
272
686
 
273
687
  def cmd_metrics(args: argparse.Namespace) -> int:
688
+ """Execute ``tnfr metrics`` returning the exit status."""
689
+
274
690
  if getattr(args, "steps", None) is None:
275
691
  # Default a longer run for metrics stability
276
692
  args.steps = 200
@@ -286,3 +702,213 @@ def cmd_metrics(args: argparse.Namespace) -> int:
286
702
  else:
287
703
  logger.info("%s", json_dumps(out))
288
704
  return 0
705
+
706
+
707
+ def cmd_profile_si(args: argparse.Namespace) -> int:
708
+ """Execute ``tnfr profile-si`` returning the exit status."""
709
+
710
+ try:
711
+ profile_module = import_module("benchmarks.compute_si_profile")
712
+ except ModuleNotFoundError as exc: # pragma: no cover - optional dependency
713
+ logger.error("Sense Index profiling helpers unavailable: %s", exc)
714
+ return 1
715
+
716
+ profile_compute_si = getattr(profile_module, "profile_compute_si")
717
+
718
+ profile_compute_si(
719
+ node_count=int(args.nodes),
720
+ chord_step=int(args.chord_step),
721
+ loops=int(args.loops),
722
+ output_dir=Path(args.output_dir),
723
+ fmt=str(args.format),
724
+ sort=str(args.sort),
725
+ )
726
+ return 0
727
+
728
+
729
+ def cmd_profile_pipeline(args: argparse.Namespace) -> int:
730
+ """Execute ``tnfr profile-pipeline`` returning the exit status."""
731
+
732
+ try:
733
+ profile_module = import_module("benchmarks.full_pipeline_profile")
734
+ except ModuleNotFoundError as exc: # pragma: no cover - optional dependency
735
+ logger.error("Full pipeline profiling helpers unavailable: %s", exc)
736
+ return 1
737
+
738
+ profile_full_pipeline = getattr(profile_module, "profile_full_pipeline")
739
+
740
+ try:
741
+ si_chunk_sizes = _parse_cli_variants(getattr(args, "si_chunk_sizes", None))
742
+ dnfr_chunk_sizes = _parse_cli_variants(getattr(args, "dnfr_chunk_sizes", None))
743
+ si_workers = _parse_cli_variants(getattr(args, "si_workers", None))
744
+ dnfr_workers = _parse_cli_variants(getattr(args, "dnfr_workers", None))
745
+ except ValueError as exc:
746
+ logger.error("%s", exc)
747
+ return 2
748
+
749
+ profile_full_pipeline(
750
+ node_count=int(args.nodes),
751
+ edge_probability=float(args.edge_probability),
752
+ loops=int(args.loops),
753
+ seed=int(args.seed),
754
+ output_dir=Path(args.output_dir),
755
+ sort=str(args.sort),
756
+ si_chunk_sizes=si_chunk_sizes,
757
+ dnfr_chunk_sizes=dnfr_chunk_sizes,
758
+ si_workers=si_workers,
759
+ dnfr_workers=dnfr_workers,
760
+ )
761
+ return 0
762
+
763
+
764
+ def cmd_math_run(args: argparse.Namespace) -> int:
765
+ """Execute ``tnfr math.run`` returning the exit status.
766
+
767
+ This command always enables the mathematical dynamics engine for
768
+ validation of TNFR structural invariants on Hilbert space.
769
+ """
770
+
771
+ # Force math engine to be enabled
772
+ setattr(args, "math_engine", True)
773
+
774
+ # Set default attributes if not present
775
+ if not hasattr(args, "summary"):
776
+ setattr(args, "summary", False)
777
+ if not hasattr(args, "summary_limit"):
778
+ setattr(args, "summary_limit", DEFAULT_SUMMARY_SERIES_LIMIT)
779
+
780
+ code, graph = _run_cli_program(args)
781
+ if code != 0:
782
+ return code
783
+
784
+ if graph is not None:
785
+ _log_run_summaries(graph, args)
786
+ logger.info("[MATH.RUN] Mathematical dynamics validation completed")
787
+ return 0
788
+
789
+
790
+ def cmd_epi_validate(args: argparse.Namespace) -> int:
791
+ """Execute ``tnfr epi.validate`` returning the exit status.
792
+
793
+ This command validates EPI structural integrity, coherence preservation,
794
+ and operator closure according to TNFR canonical invariants.
795
+ """
796
+
797
+ code, graph = _run_cli_program(args)
798
+ if code != 0:
799
+ return code
800
+
801
+ if graph is None:
802
+ logger.error("[EPI.VALIDATE] No graph generated for validation")
803
+ return 1
804
+
805
+ # Validation checks
806
+ tolerance = getattr(args, "tolerance", 1e-6)
807
+ check_coherence = getattr(args, "check_coherence", True)
808
+ check_frequency = getattr(args, "check_frequency", True)
809
+ check_phase = getattr(args, "check_phase", True)
810
+
811
+ validation_passed = True
812
+ validation_summary = []
813
+
814
+ # Check coherence preservation
815
+ if check_coherence:
816
+ hist = ensure_history(graph)
817
+ cfg_coh = graph.graph.get("COHERENCE", METRIC_DEFAULTS["COHERENCE"])
818
+ if cfg_coh.get("enabled", True):
819
+ Wstats = hist.get(cfg_coh.get("stats_history_key", "W_stats"), [])
820
+ if Wstats:
821
+ # Check that coherence is non-negative and bounded
822
+ for i, stats in enumerate(Wstats):
823
+ W_mean = float(stats.get("mean", 0.0))
824
+ if W_mean < -tolerance:
825
+ validation_passed = False
826
+ validation_summary.append(
827
+ f" [FAIL] Step {i}: Coherence W_mean={W_mean:.6f} < 0"
828
+ )
829
+ if validation_passed:
830
+ validation_summary.append(
831
+ f" [PASS] Coherence preserved (W_mean ≥ 0 across {len(Wstats)} steps)"
832
+ )
833
+ else:
834
+ validation_summary.append(" [SKIP] No coherence history available")
835
+ else:
836
+ validation_summary.append(" [SKIP] Coherence tracking disabled")
837
+
838
+ # Check structural frequency positivity
839
+ if check_frequency:
840
+ nodes = list(_iter_graph_nodes(graph))
841
+ if nodes:
842
+ negative_frequencies = []
843
+ for node_id in nodes:
844
+ data = graph.nodes[node_id]
845
+ nu_f = float(
846
+ get_attr(
847
+ data,
848
+ VF_ALIAS_KEYS,
849
+ default=float(data.get(VF_PRIMARY, 0.0)),
850
+ )
851
+ )
852
+ if nu_f < -tolerance:
853
+ negative_frequencies.append((node_id, nu_f))
854
+
855
+ if negative_frequencies:
856
+ validation_passed = False
857
+ for node_id, nu_f in negative_frequencies[:5]: # Show first 5
858
+ validation_summary.append(
859
+ f" [FAIL] Node {node_id}: νf={nu_f:.6f} < 0"
860
+ )
861
+ if len(negative_frequencies) > 5:
862
+ validation_summary.append(
863
+ f" ... and {len(negative_frequencies) - 5} more nodes"
864
+ )
865
+ else:
866
+ validation_summary.append(
867
+ f" [PASS] Structural frequency νf ≥ 0 for all {len(nodes)} nodes"
868
+ )
869
+ else:
870
+ validation_summary.append(" [SKIP] No nodes to validate")
871
+
872
+ # Check phase synchrony in couplings
873
+ if check_phase:
874
+ edges = list(_iter_graph_edges(graph))
875
+ if edges:
876
+ phase_violations = []
877
+ for u, v in edges:
878
+ theta_u = float(graph.nodes[u].get("theta", 0.0))
879
+ theta_v = float(graph.nodes[v].get("theta", 0.0))
880
+ # Check if phases are defined (not both zero)
881
+ if abs(theta_u) > tolerance or abs(theta_v) > tolerance:
882
+ # Phase difference should be bounded
883
+ phase_diff = abs(theta_u - theta_v)
884
+ if phase_diff > TWO_PI: # > 2π
885
+ phase_violations.append((u, v, phase_diff))
886
+
887
+ if phase_violations:
888
+ validation_passed = False
889
+ for u, v, diff in phase_violations[:5]:
890
+ validation_summary.append(
891
+ f" [WARN] Edge ({u},{v}): phase diff={diff:.6f} > 2π"
892
+ )
893
+ if len(phase_violations) > 5:
894
+ validation_summary.append(
895
+ f" ... and {len(phase_violations) - 5} more edges"
896
+ )
897
+ else:
898
+ validation_summary.append(
899
+ f" [PASS] Phase synchrony maintained across {len(edges)} edges"
900
+ )
901
+ else:
902
+ validation_summary.append(" [SKIP] No edges to validate")
903
+
904
+ # Log validation results
905
+ logger.info("[EPI.VALIDATE] Validation Summary:")
906
+ for line in validation_summary:
907
+ logger.info("%s", line)
908
+
909
+ if validation_passed:
910
+ logger.info("[EPI.VALIDATE] ✓ All validation checks passed")
911
+ return 0
912
+ else:
913
+ logger.info("[EPI.VALIDATE] ✗ Some validation checks failed")
914
+ return 1