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/cli/execution.py ADDED
@@ -0,0 +1,914 @@
1
+ """CLI execution helpers for running canonical TNFR programs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
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
11
+ from pathlib import Path
12
+ from typing import Any, Optional, Sequence
13
+
14
+ import networkx as nx
15
+ import numpy as np
16
+
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
41
+ from ..metrics import (
42
+ build_metrics_summary,
43
+ export_metrics,
44
+ glyph_top,
45
+ register_metrics_callbacks,
46
+ )
47
+ from ..metrics.core import _metrics_step
48
+ from ..ontosim import prepare_network
49
+ from ..sense import register_sigma_callback
50
+ from ..trace import register_trace
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,
59
+ )
60
+ from .arguments import _args_to_dict
61
+ from .utils import _parse_cli_variants
62
+ from ..validation import validate_canon
63
+
64
+ logger = get_logger(__name__)
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
+ )
76
+
77
+ # CLI summaries should remain concise by default while allowing callers to
78
+ # inspect the full glyphogram series when needed.
79
+ DEFAULT_SUMMARY_SERIES_LIMIT = 10
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
+
119
+
120
+ def _save_json(path: str, data: Any) -> None:
121
+ payload = json_dumps(data, ensure_ascii=False, indent=2, default=list)
122
+ safe_write(path, lambda f: f.write(payload))
123
+
124
+
125
+ def _attach_callbacks(G: "nx.Graph") -> None:
126
+ register_sigma_callback(G)
127
+ register_metrics_callbacks(G)
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", [])
133
+ _metrics_step(G, ctx=None)
134
+
135
+
136
+ def _persist_history(G: "nx.Graph", args: argparse.Namespace) -> None:
137
+ if getattr(args, "save_history", None) or getattr(
138
+ args, "export_history_base", None
139
+ ):
140
+ history = ensure_history(G)
141
+ if getattr(args, "save_history", None):
142
+ _save_json(args.save_history, history)
143
+ if getattr(args, "export_history_base", None):
144
+ export_metrics(G, args.export_history_base, fmt=args.export_format)
145
+
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
+
291
+ def build_basic_graph(args: argparse.Namespace) -> "nx.Graph":
292
+ """Construct the base graph topology described by CLI ``args``."""
293
+
294
+ n = args.nodes
295
+ topology = getattr(args, "topology", "ring").lower()
296
+ seed = getattr(args, "seed", None)
297
+ if topology == "ring":
298
+ G = nx.cycle_graph(n)
299
+ elif topology == "complete":
300
+ G = nx.complete_graph(n)
301
+ elif topology == "erdos":
302
+ if getattr(args, "p", None) is not None:
303
+ prob = float(args.p)
304
+ else:
305
+ if n <= 0:
306
+ fallback = 0.0
307
+ else:
308
+ fallback = 3.0 / n
309
+ prob = clamp01(fallback)
310
+ if not 0.0 <= prob <= 1.0:
311
+ raise ValueError(f"p must be between 0 and 1; received {prob}")
312
+ G = nx.gnp_random_graph(n, prob, seed=seed)
313
+ else:
314
+ raise ValueError(
315
+ f"Invalid topology '{topology}'. Accepted options are: ring, complete, erdos"
316
+ )
317
+ if seed is not None:
318
+ G.graph["RANDOM_SEED"] = int(seed)
319
+ return G
320
+
321
+
322
+ def apply_cli_config(G: "nx.Graph", args: argparse.Namespace) -> None:
323
+ """Apply CLI overrides from ``args`` to graph-level configuration."""
324
+
325
+ if 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
331
+ arg_map = {
332
+ "dt": ("DT", float),
333
+ "integrator": ("INTEGRATOR_METHOD", str),
334
+ "remesh_mode": ("REMESH_MODE", str),
335
+ "glyph_hysteresis_window": ("GLYPH_HYSTERESIS_WINDOW", int),
336
+ }
337
+ for attr, (key, conv) in arg_map.items():
338
+ val = getattr(args, attr, None)
339
+ if val is not None:
340
+ G.graph[key] = conv(val)
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
+
352
+ gcanon = {
353
+ **base_gcanon,
354
+ **_args_to_dict(args, prefix="grammar_"),
355
+ }
356
+ if getattr(args, "grammar_canon", None) is not None:
357
+ gcanon["enabled"] = bool(args.grammar_canon)
358
+ G.graph["GRAMMAR_CANON"] = gcanon
359
+
360
+ selector = getattr(args, "selector", None)
361
+ if selector is not None:
362
+ sel_map = {
363
+ "basic": default_glyph_selector,
364
+ "param": parametric_glyph_selector,
365
+ }
366
+ G.graph["glyph_selector"] = sel_map.get(selector, default_glyph_selector)
367
+
368
+ if hasattr(args, "gamma_type"):
369
+ G.graph["GAMMA"] = {
370
+ "type": args.gamma_type,
371
+ "beta": args.gamma_beta,
372
+ "R0": args.gamma_R0,
373
+ }
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
+
406
+
407
+ def register_callbacks_and_observer(G: "nx.Graph") -> None:
408
+ """Attach callbacks and validators required for CLI runs."""
409
+
410
+ _attach_callbacks(G)
411
+ validate_canon(G)
412
+
413
+
414
+ def _build_graph_from_args(args: argparse.Namespace) -> "nx.Graph":
415
+ G = build_basic_graph(args)
416
+ apply_cli_config(G, args)
417
+ if getattr(args, "observer", False):
418
+ G.graph["ATTACH_STD_OBSERVER"] = True
419
+ prepare_network(G)
420
+ register_callbacks_and_observer(G)
421
+ _configure_math_engine(G, args)
422
+ return G
423
+
424
+
425
+ def _load_sequence(path: Path) -> ProgramTokens:
426
+ try:
427
+ data = read_structured_file(path)
428
+ except (StructuredFileError, OSError) as exc:
429
+ if isinstance(exc, StructuredFileError):
430
+ message = str(exc)
431
+ else:
432
+ message = str(StructuredFileError(path, exc))
433
+ logger.error("%s", message)
434
+ raise SystemExit(1) from exc
435
+ if isinstance(data, Mapping) and "sequence" in data:
436
+ data = data["sequence"]
437
+ return parse_program_tokens(data)
438
+
439
+
440
+ def resolve_program(
441
+ args: argparse.Namespace, default: Optional[ProgramTokens] = None
442
+ ) -> Optional[ProgramTokens]:
443
+ """Resolve preset/sequence inputs into program tokens."""
444
+
445
+ if getattr(args, "preset", None):
446
+ try:
447
+ return get_preset(args.preset)
448
+ except KeyError as exc:
449
+ details = exc.args[0] if exc.args else "Preset lookup failed."
450
+ logger.error(
451
+ (
452
+ "Unknown preset '%s'. Available presets: %s. %s "
453
+ "Use --sequence-file to execute custom sequences."
454
+ ),
455
+ args.preset,
456
+ _PREFERRED_PRESETS_DISPLAY,
457
+ details,
458
+ )
459
+ raise SystemExit(1) from exc
460
+ if getattr(args, "sequence_file", None):
461
+ return _load_sequence(Path(args.sequence_file))
462
+ return default
463
+
464
+
465
+ def run_program(
466
+ G: Optional["nx.Graph"],
467
+ program: Optional[ProgramTokens],
468
+ args: argparse.Namespace,
469
+ ) -> "nx.Graph":
470
+ """Execute ``program`` (or timed run) on ``G`` using CLI options."""
471
+
472
+ if G is None:
473
+ G = _build_graph_from_args(args)
474
+
475
+ if program is None:
476
+ steps = getattr(args, "steps", 100)
477
+ steps = 100 if steps is None else int(steps)
478
+ if steps < 0:
479
+ steps = 0
480
+
481
+ run_kwargs: dict[str, Any] = {}
482
+ for attr in ("dt", "use_Si", "apply_glyphs"):
483
+ value = getattr(args, attr, None)
484
+ if value is not None:
485
+ run_kwargs[attr] = value
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
+
494
+ run(G, steps=steps, **run_kwargs)
495
+ else:
496
+ play(G, program)
497
+
498
+ _persist_history(G, args)
499
+ return G
500
+
501
+
502
+ def _run_cli_program(
503
+ args: argparse.Namespace,
504
+ *,
505
+ default_program: Optional[ProgramTokens] = None,
506
+ graph: Optional["nx.Graph"] = None,
507
+ ) -> tuple[int, Optional["nx.Graph"]]:
508
+ try:
509
+ program = resolve_program(args, default=default_program)
510
+ except SystemExit as exc:
511
+ code = exc.code if isinstance(exc.code, int) else 1
512
+ return code or 1, None
513
+
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
519
+ return 0, result_graph
520
+
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
+
636
+ def _log_run_summaries(G: "nx.Graph", args: argparse.Namespace) -> None:
637
+ cfg_coh = G.graph.get("COHERENCE", METRIC_DEFAULTS["COHERENCE"])
638
+ cfg_diag = G.graph.get("DIAGNOSIS", METRIC_DEFAULTS["DIAGNOSIS"])
639
+ hist = ensure_history(G)
640
+
641
+ if cfg_coh.get("enabled", True):
642
+ Wstats = hist.get(cfg_coh.get("stats_history_key", "W_stats"), [])
643
+ if Wstats:
644
+ logger.info("[COHERENCE] last step: %s", Wstats[-1])
645
+
646
+ if cfg_diag.get("enabled", True):
647
+ last_diag = hist.get(cfg_diag.get("history_key", "nodal_diag"), [])
648
+ if last_diag:
649
+ sample = list(last_diag[-1].values())[:3]
650
+ logger.info("[DIAGNOSIS] sample: %s", sample)
651
+
652
+ if args.summary:
653
+ summary_limit = getattr(args, "summary_limit", DEFAULT_SUMMARY_SERIES_LIMIT)
654
+ summary, has_latency_values = build_metrics_summary(
655
+ G, series_limit=summary_limit
656
+ )
657
+ logger.info("Global Tg: %s", summary["Tg_global"])
658
+ logger.info("Top operators by Tg: %s", glyph_top(G, k=5))
659
+ if has_latency_values:
660
+ logger.info("Average latency: %s", summary["latency_mean"])
661
+
662
+ _log_math_engine_summary(G)
663
+
664
+
665
+ def cmd_run(args: argparse.Namespace) -> int:
666
+ """Execute ``tnfr run`` returning the exit status."""
667
+
668
+ code, graph = _run_cli_program(args)
669
+ if code != 0:
670
+ return code
671
+
672
+ if graph is not None:
673
+ _log_run_summaries(graph, args)
674
+ return 0
675
+
676
+
677
+ def cmd_sequence(args: argparse.Namespace) -> int:
678
+ """Execute ``tnfr sequence`` returning the exit status."""
679
+
680
+ if args.preset and args.sequence_file:
681
+ logger.error("Cannot use --preset and --sequence-file at the same time")
682
+ return 1
683
+ code, _ = _run_cli_program(args, default_program=get_preset(CANONICAL_PRESET_NAME))
684
+ return code
685
+
686
+
687
+ def cmd_metrics(args: argparse.Namespace) -> int:
688
+ """Execute ``tnfr metrics`` returning the exit status."""
689
+
690
+ if getattr(args, "steps", None) is None:
691
+ # Default a longer run for metrics stability
692
+ args.steps = 200
693
+
694
+ code, graph = _run_cli_program(args)
695
+ if code != 0 or graph is None:
696
+ return code
697
+
698
+ summary_limit = getattr(args, "summary_limit", None)
699
+ out, _ = build_metrics_summary(graph, series_limit=summary_limit)
700
+ if args.save:
701
+ _save_json(args.save, out)
702
+ else:
703
+ logger.info("%s", json_dumps(out))
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