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/structural.py CHANGED
@@ -1,20 +1,86 @@
1
- """Structural analysis."""
1
+ """Maintain TNFR structural coherence for nodes and operator sequences.
2
+
3
+ This module exposes the canonical entry points used by the engine to
4
+ instantiate coherent TNFR nodes and to orchestrate structural operator
5
+ pipelines while keeping the nodal equation
6
+ ``∂EPI/∂t = νf · ΔNFR(t)`` balanced. Consumers are expected to provide
7
+ graph instances honouring :class:`tnfr.types.GraphLike`: the structural
8
+ helpers reach into ``nodes``, ``neighbors``, ``number_of_nodes`` and the
9
+ ``.graph`` metadata mapping to propagate ΔNFR hooks and coherence metrics.
10
+
11
+ Public API
12
+ ----------
13
+ create_nfr
14
+ Initialise a node with canonical EPI, νf and phase attributes plus a
15
+ ΔNFR hook that propagates reorganisations through the graph.
16
+ run_sequence
17
+ Validate and execute operator trajectories so that ΔNFR hooks can
18
+ update EPI, νf and phase coherently after each step.
19
+ OPERATORS
20
+ Registry of canonical structural operators ready to be composed into
21
+ validated sequences.
22
+ validate_sequence
23
+ Grammar guard that ensures operator trajectories stay within TNFR
24
+ closure rules before execution.
25
+ """
2
26
 
3
27
  from __future__ import annotations
4
- from typing import Iterable
5
- import networkx as nx # type: ignore[import-untyped]
6
28
 
29
+ from copy import deepcopy
30
+ from typing import Iterable, Mapping, Sequence
31
+
32
+ import networkx as nx
33
+
34
+ from .constants import EPI_PRIMARY, THETA_PRIMARY, VF_PRIMARY
7
35
  from .dynamics import (
8
- set_delta_nfr_hook,
9
36
  dnfr_epi_vf_mixed,
37
+ set_delta_nfr_hook,
38
+ )
39
+ from .mathematics import (
40
+ BasicStateProjector,
41
+ CoherenceOperator,
42
+ FrequencyOperator,
43
+ HilbertSpace,
44
+ MathematicalDynamicsEngine,
45
+ make_coherence_operator,
46
+ make_frequency_operator,
47
+ )
48
+ from tnfr.validation import (
49
+ NFRValidator,
50
+ validate_sequence,
51
+ TNFRValidator as InvariantValidator,
52
+ SequenceSemanticValidator,
53
+ InvariantSeverity,
54
+ validation_config,
55
+ )
56
+ from .operators.definitions import (
57
+ Coherence,
58
+ Contraction,
59
+ Coupling,
60
+ Dissonance,
61
+ Emission,
62
+ Expansion,
63
+ Mutation,
64
+ Operator,
65
+ Reception,
66
+ Recursivity,
67
+ Resonance,
68
+ SelfOrganization,
69
+ Silence,
70
+ Transition,
10
71
  )
11
- from .grammar import apply_glyph_with_grammar
12
- from .types import Glyph
13
- from .constants import EPI_PRIMARY, VF_PRIMARY, THETA_PRIMARY
72
+ from .operators.registry import OPERATORS
73
+ from .types import DeltaNFRHook, NodeId, TNFRGraph
14
74
 
75
+ try: # pragma: no cover - optional dependency path exercised in CI extras
76
+ import numpy as np
77
+ except (
78
+ ImportError
79
+ ): # pragma: no cover - optional dependency path exercised in CI extras
80
+ np = None # type: ignore[assignment]
15
81
 
16
82
  # ---------------------------------------------------------------------------
17
- # 1) Factoría NFR
83
+ # 1) NFR factory
18
84
  # ---------------------------------------------------------------------------
19
85
 
20
86
 
@@ -24,13 +90,105 @@ def create_nfr(
24
90
  epi: float = 0.0,
25
91
  vf: float = 1.0,
26
92
  theta: float = 0.0,
27
- graph: nx.Graph | None = None,
28
- dnfr_hook=dnfr_epi_vf_mixed,
29
- ) -> tuple[nx.Graph, str]:
30
- """Create a graph with an initialised NFR node.
31
-
32
- Returns the tuple ``(G, name)`` for convenience.
93
+ graph: TNFRGraph | None = None,
94
+ dnfr_hook: DeltaNFRHook = dnfr_epi_vf_mixed,
95
+ ) -> tuple[TNFRGraph, str]:
96
+ """Anchor a TNFR node by seeding EPI, νf, phase and ΔNFR coupling.
97
+
98
+ The factory secures the structural state of a node: it stores canonical
99
+ values for the Primary Information Structure (EPI), structural frequency
100
+ (νf) and phase, then installs a ΔNFR hook so that later operator
101
+ sequences can reorganise the node without breaking the nodal equation.
102
+
103
+ Parameters
104
+ ----------
105
+ name : str
106
+ Identifier for the new node. The identifier is stored as the node key
107
+ and must remain hashable by :mod:`networkx`.
108
+ epi : float, optional
109
+ Initial Primary Information Structure (EPI) assigned to the node. The
110
+ value provides the baseline form that subsequent ΔNFR hooks reorganise
111
+ through the nodal equation.
112
+ vf : float, optional
113
+ Structural frequency (νf, expressed in Hz_str) used as the starting
114
+ reorganisation rate for the node.
115
+ theta : float, optional
116
+ Initial phase of the node in radians, used to keep phase alignment with
117
+ neighbouring coherence structures.
118
+ graph : TNFRGraph, optional
119
+ Existing graph where the node will be registered. When omitted a new
120
+ :class:`networkx.Graph` instance is created.
121
+ dnfr_hook : DeltaNFRHook, optional
122
+ Callable responsible for computing ΔNFR and updating EPI/νf after each
123
+ operator application. By default the canonical ``dnfr_epi_vf_mixed``
124
+ hook is installed, which keeps the nodal equation coherent with TNFR
125
+ invariants.
126
+
127
+ Returns
128
+ -------
129
+ tuple[TNFRGraph, str]
130
+ The graph that stores the node together with the node identifier. The
131
+ tuple form allows immediate reuse with :func:`run_sequence`.
132
+
133
+ Notes
134
+ -----
135
+ The factory does not introduce additional TNFR-specific errors. Any
136
+ exceptions raised by :mod:`networkx` when adding nodes propagate unchanged.
137
+
138
+ Examples
139
+ --------
140
+ Create a node, connect a ΔNFR hook and launch a coherent operator
141
+ trajectory while tracking the evolving metrics.
142
+
143
+ >>> from tnfr.constants import DNFR_PRIMARY, EPI_PRIMARY, THETA_PRIMARY, VF_PRIMARY
144
+ >>> from tnfr.dynamics import set_delta_nfr_hook
145
+ >>> from tnfr.structural import (
146
+ ... Coherence,
147
+ ... Emission,
148
+ ... Reception,
149
+ ... Resonance,
150
+ ... Silence,
151
+ ... create_nfr,
152
+ ... run_sequence,
153
+ ... )
154
+ >>> G, node = create_nfr("seed", epi=1.0, vf=2.0, theta=0.1)
155
+ >>> def synchronise_delta(graph):
156
+ ... delta = graph.nodes[node][VF_PRIMARY] * 0.2
157
+ ... graph.nodes[node][DNFR_PRIMARY] = delta
158
+ ... graph.nodes[node][EPI_PRIMARY] += delta
159
+ ... graph.nodes[node][VF_PRIMARY] += delta * 0.05
160
+ ... graph.nodes[node][THETA_PRIMARY] += 0.01
161
+ >>> set_delta_nfr_hook(G, synchronise_delta)
162
+ >>> run_sequence(G, node, [Emission(), Reception(), Coherence(), Resonance(), Silence()]) # doctest: +SKIP
163
+ >>> (
164
+ ... G.nodes[node][EPI_PRIMARY],
165
+ ... G.nodes[node][VF_PRIMARY],
166
+ ... G.nodes[node][THETA_PRIMARY],
167
+ ... G.nodes[node][DNFR_PRIMARY],
168
+ ... ) # doctest: +SKIP
169
+ (..., ..., ..., ...)
33
170
  """
171
+ from .validation.input_validation import (
172
+ ValidationError,
173
+ validate_epi_value,
174
+ validate_node_id,
175
+ validate_theta_value,
176
+ validate_tnfr_graph,
177
+ validate_vf_value,
178
+ )
179
+
180
+ # Validate input parameters
181
+ try:
182
+ validate_node_id(name)
183
+ config = graph.graph if graph is not None else None
184
+ epi = validate_epi_value(epi, config=config, allow_complex=False)
185
+ vf = validate_vf_value(vf, config=config)
186
+ theta = validate_theta_value(theta, normalize=False)
187
+ if graph is not None:
188
+ validate_tnfr_graph(graph)
189
+ except ValidationError as e:
190
+ raise ValueError(f"Invalid parameters for create_nfr: {e}") from e
191
+
34
192
  G = graph if graph is not None else nx.Graph()
35
193
  G.add_node(
36
194
  name,
@@ -44,286 +202,504 @@ def create_nfr(
44
202
  return G, name
45
203
 
46
204
 
47
- # ---------------------------------------------------------------------------
48
- # 2) Operadores estructurales como API de primer orden
49
- # ---------------------------------------------------------------------------
50
-
51
-
52
- class Operador:
53
- """Base class for TNFR operators.
54
-
55
- Each operator defines ``name`` (ASCII identifier) and ``glyph``
56
- (símbolo TNFR canónico). Calling an instance applies the corresponding
57
- symbol to the node.
205
+ def _resolve_dimension(
206
+ G: TNFRGraph,
207
+ *,
208
+ dimension: int | None,
209
+ hilbert_space: HilbertSpace | None,
210
+ existing_cfg: Mapping[str, object] | None,
211
+ ) -> int:
212
+ if hilbert_space is not None:
213
+ resolved = int(getattr(hilbert_space, "dimension", 0) or 0)
214
+ if resolved <= 0:
215
+ raise ValueError("Hilbert space dimension must be positive.")
216
+ return resolved
217
+
218
+ if dimension is None and existing_cfg:
219
+ candidate = existing_cfg.get("dimension")
220
+ if isinstance(candidate, int) and candidate > 0:
221
+ dimension = candidate
222
+
223
+ if dimension is None:
224
+ if hasattr(G, "number_of_nodes"):
225
+ count = int(G.number_of_nodes())
226
+ else:
227
+ count = len(tuple(G.nodes))
228
+ dimension = max(1, count)
229
+
230
+ resolved = int(dimension)
231
+ if resolved <= 0:
232
+ raise ValueError("Hilbert space dimension must be positive.")
233
+ return resolved
234
+
235
+
236
+ def _ensure_coherence_operator(
237
+ *,
238
+ operator: CoherenceOperator | None,
239
+ dimension: int,
240
+ spectrum: Sequence[float] | None,
241
+ c_min: float | None,
242
+ ) -> CoherenceOperator:
243
+ if operator is not None:
244
+ return operator
245
+
246
+ kwargs: dict[str, object] = {}
247
+ if spectrum is not None:
248
+ spectrum_array = np.asarray(spectrum, dtype=np.complex128)
249
+ if spectrum_array.ndim != 1:
250
+ raise ValueError("Coherence spectrum must be one-dimensional.")
251
+ kwargs["spectrum"] = spectrum_array
252
+ if c_min is not None:
253
+ kwargs["c_min"] = float(c_min)
254
+ return make_coherence_operator(dimension, **kwargs)
255
+
256
+
257
+ def _ensure_frequency_operator(
258
+ *,
259
+ operator: FrequencyOperator | None,
260
+ dimension: int,
261
+ diagonal: Sequence[float] | None,
262
+ ) -> FrequencyOperator:
263
+ if operator is not None:
264
+ return operator
265
+
266
+ if diagonal is None:
267
+ matrix = np.eye(dimension, dtype=float)
268
+ else:
269
+ diag_array = np.asarray(diagonal, dtype=float)
270
+ if diag_array.ndim != 1:
271
+ raise ValueError("Frequency diagonal must be one-dimensional.")
272
+ if diag_array.shape[0] != int(dimension):
273
+ raise ValueError("Frequency diagonal size must match Hilbert dimension.")
274
+ matrix = np.diag(diag_array)
275
+ return make_frequency_operator(np.asarray(matrix, dtype=np.complex128))
276
+
277
+
278
+ def _ensure_generator_matrix(
279
+ *,
280
+ dimension: int,
281
+ diagonal: Sequence[float] | None,
282
+ ) -> "np.ndarray":
283
+ if diagonal is None:
284
+ return np.zeros((dimension, dimension), dtype=np.complex128)
285
+ diag_array = np.asarray(diagonal, dtype=np.complex128)
286
+ if diag_array.ndim != 1:
287
+ raise ValueError("Generator diagonal must be one-dimensional.")
288
+ if diag_array.shape[0] != int(dimension):
289
+ raise ValueError("Generator diagonal size must match Hilbert dimension.")
290
+ return np.diag(diag_array)
291
+
292
+
293
+ def create_math_nfr(
294
+ name: str,
295
+ *,
296
+ epi: float = 0.0,
297
+ vf: float = 1.0,
298
+ theta: float = 0.0,
299
+ graph: TNFRGraph | None = None,
300
+ dnfr_hook: DeltaNFRHook = dnfr_epi_vf_mixed,
301
+ dimension: int | None = None,
302
+ hilbert_space: HilbertSpace | None = None,
303
+ coherence_operator: CoherenceOperator | None = None,
304
+ coherence_spectrum: Sequence[float] | None = None,
305
+ coherence_c_min: float | None = None,
306
+ coherence_threshold: float | None = None,
307
+ frequency_operator: FrequencyOperator | None = None,
308
+ frequency_diagonal: Sequence[float] | None = None,
309
+ generator_diagonal: Sequence[float] | None = None,
310
+ state_projector: BasicStateProjector | None = None,
311
+ dynamics_engine: MathematicalDynamicsEngine | None = None,
312
+ validator: NFRValidator | None = None,
313
+ ) -> tuple[TNFRGraph, str]:
314
+ """Create a TNFR node with canonical mathematical validation attached.
315
+
316
+ The helper wraps :func:`create_nfr` while projecting the structural state
317
+ into a Hilbert space so coherence, νf and norm invariants can be tracked via
318
+ the mathematical runtime. It installs operators and validation metadata on
319
+ both the node and the hosting graph so that the
320
+ :class:`~tnfr.mathematics.MathematicalDynamicsEngine` can consume them
321
+ directly.
322
+
323
+ Parameters
324
+ ----------
325
+ name : str
326
+ Identifier for the new node.
327
+ epi, vf, theta : float, optional
328
+ Canonical TNFR scalars forwarded to :func:`create_nfr`.
329
+ dimension : int, optional
330
+ Hilbert space dimension. When omitted it is inferred from the graph size
331
+ (at least one).
332
+ hilbert_space : HilbertSpace, optional
333
+ Pre-built Hilbert space to reuse. Its dimension supersedes ``dimension``.
334
+ coherence_operator, frequency_operator : optional
335
+ Custom operators to install. When omitted they are derived from
336
+ ``coherence_spectrum``/``coherence_c_min`` and
337
+ ``frequency_diagonal`` respectively.
338
+ coherence_threshold : float, optional
339
+ Validation floor. Defaults to ``coherence_operator.c_min``.
340
+ generator_diagonal : sequence of float, optional
341
+ Diagonal entries for the unitary generator used by the mathematical
342
+ dynamics engine. Defaults to a null generator.
343
+ state_projector : BasicStateProjector, optional
344
+ Projector used to build the canonical spectral state for validation.
345
+
346
+ Returns
347
+ -------
348
+ tuple[TNFRGraph, str]
349
+ The graph and node identifier, mirroring :func:`create_nfr`.
350
+
351
+ Examples
352
+ --------
353
+ >>> G, node = create_math_nfr("math-seed", epi=0.4, vf=1.2, theta=0.05, dimension=3)
354
+ >>> metrics = G.nodes[node]["math_metrics"]
355
+ >>> round(metrics["norm"], 6)
356
+ 1.0
357
+ >>> metrics["coherence_passed"], metrics["frequency_passed"]
358
+ (True, True)
359
+ >>> metrics["coherence_value"] >= metrics["coherence_threshold"]
360
+ True
361
+
362
+ Notes
363
+ -----
364
+ The helper mutates/extends ``G.graph['MATH_ENGINE']`` so subsequent calls to
365
+ :mod:`tnfr.dynamics.runtime` can advance the mathematical engine without
366
+ further configuration.
58
367
  """
59
368
 
60
- name = "operador"
61
- glyph = None # tipo: str
62
-
63
- def __call__(self, G: nx.Graph, node, **kw) -> None:
64
- if self.glyph is None:
65
- raise NotImplementedError("Operador sin glyph asignado")
66
- apply_glyph_with_grammar(G, [node], self.glyph, kw.get("window"))
67
-
68
-
69
- class Emision(Operador):
70
- """Aplicación del operador de emisión (símbolo ``AL``)."""
71
-
72
- __slots__ = ()
73
- name = "emision"
74
- glyph = Glyph.AL.value
75
-
76
-
77
- class Recepcion(Operador):
78
- """Operador de recepción (símbolo ``EN``)."""
79
-
80
- __slots__ = ()
81
- name = "recepcion"
82
- glyph = Glyph.EN.value
83
-
84
-
85
- class Coherencia(Operador):
86
- """Operador de coherencia (símbolo ``IL``)."""
87
-
88
- __slots__ = ()
89
- name = "coherencia"
90
- glyph = Glyph.IL.value
91
-
92
-
93
- class Disonancia(Operador):
94
- """Operador de disonancia (símbolo ``OZ``)."""
95
-
96
- __slots__ = ()
97
- name = "disonancia"
98
- glyph = Glyph.OZ.value
99
-
100
-
101
- class Acoplamiento(Operador):
102
- """Operador de acoplamiento (símbolo ``UM``)."""
103
-
104
- __slots__ = ()
105
- name = "acoplamiento"
106
- glyph = Glyph.UM.value
107
-
108
-
109
- class Resonancia(Operador):
110
- """Operador de resonancia (símbolo ``RA``)."""
111
-
112
- __slots__ = ()
113
- name = "resonancia"
114
- glyph = Glyph.RA.value
115
-
116
-
117
- class Silencio(Operador):
118
- """Operador de silencio (símbolo ``SHA``)."""
119
-
120
- __slots__ = ()
121
- name = "silencio"
122
- glyph = Glyph.SHA.value
123
-
124
-
125
- class Expansion(Operador):
126
- """Operador de expansión (símbolo ``VAL``)."""
127
-
128
- __slots__ = ()
129
- name = "expansion"
130
- glyph = Glyph.VAL.value
131
-
369
+ if np is None:
370
+ raise ImportError(
371
+ "create_math_nfr requires NumPy; install the 'tnfr[math]' extras."
372
+ )
132
373
 
133
- class Contraccion(Operador):
134
- """Operador de contracción (símbolo ``NUL``)."""
135
-
136
- __slots__ = ()
137
- name = "contraccion"
138
- glyph = Glyph.NUL.value
139
-
140
-
141
- class Autoorganizacion(Operador):
142
- """Operador de autoorganización (símbolo ``THOL``)."""
143
-
144
- __slots__ = ()
145
- name = "autoorganizacion"
146
- glyph = Glyph.THOL.value
147
-
148
-
149
- class Mutacion(Operador):
150
- """Operador de mutación (símbolo ``ZHIR``)."""
374
+ G, node = create_nfr(
375
+ name,
376
+ epi=epi,
377
+ vf=vf,
378
+ theta=theta,
379
+ graph=graph,
380
+ dnfr_hook=dnfr_hook,
381
+ )
151
382
 
152
- __slots__ = ()
153
- name = "mutacion"
154
- glyph = Glyph.ZHIR.value
383
+ existing_cfg = G.graph.get("MATH_ENGINE")
384
+ mapping_cfg: Mapping[str, object] | None
385
+ if isinstance(existing_cfg, Mapping):
386
+ mapping_cfg = existing_cfg
387
+ else:
388
+ mapping_cfg = None
389
+
390
+ resolved_dimension = _resolve_dimension(
391
+ G,
392
+ dimension=dimension,
393
+ hilbert_space=hilbert_space,
394
+ existing_cfg=mapping_cfg,
395
+ )
155
396
 
397
+ hilbert = hilbert_space or HilbertSpace(resolved_dimension)
398
+ resolved_dimension = int(getattr(hilbert, "dimension", resolved_dimension))
156
399
 
157
- class Transicion(Operador):
158
- """Operador de transición (símbolo ``NAV``)."""
400
+ coherence = _ensure_coherence_operator(
401
+ operator=coherence_operator,
402
+ dimension=resolved_dimension,
403
+ spectrum=coherence_spectrum,
404
+ c_min=coherence_c_min,
405
+ )
406
+ threshold = float(
407
+ coherence_threshold if coherence_threshold is not None else coherence.c_min
408
+ )
159
409
 
160
- __slots__ = ()
161
- name = "transicion"
162
- glyph = Glyph.NAV.value
410
+ frequency = _ensure_frequency_operator(
411
+ operator=frequency_operator,
412
+ dimension=resolved_dimension,
413
+ diagonal=frequency_diagonal,
414
+ )
163
415
 
416
+ projector = state_projector or BasicStateProjector()
164
417
 
165
- class Recursividad(Operador):
166
- """Operador de recursividad (símbolo ``REMESH``)."""
418
+ generator_matrix = _ensure_generator_matrix(
419
+ dimension=resolved_dimension,
420
+ diagonal=generator_diagonal,
421
+ )
422
+ engine = dynamics_engine or MathematicalDynamicsEngine(
423
+ generator_matrix,
424
+ hilbert_space=hilbert,
425
+ )
167
426
 
168
- __slots__ = ()
169
- name = "recursividad"
170
- glyph = Glyph.REMESH.value
427
+ enforce_frequency = frequency is not None
428
+ spectral_validator = validator or NFRValidator(
429
+ hilbert,
430
+ coherence,
431
+ threshold,
432
+ frequency_operator=frequency if enforce_frequency else None,
433
+ )
171
434
 
435
+ state = projector(
436
+ epi=float(epi),
437
+ nu_f=float(vf),
438
+ theta=float(theta),
439
+ dim=resolved_dimension,
440
+ )
441
+ norm_value = float(hilbert.norm(state))
442
+ outcome = spectral_validator.validate(
443
+ state,
444
+ enforce_frequency_positivity=enforce_frequency,
445
+ )
446
+ summary_raw = outcome.summary
447
+ summary = {key: deepcopy(value) for key, value in summary_raw.items()}
448
+
449
+ coherence_summary = summary.get("coherence")
450
+ frequency_summary = summary.get("frequency")
451
+
452
+ math_metrics = {
453
+ "norm": norm_value,
454
+ "normalized": bool(summary.get("normalized", False)),
455
+ "coherence_value": (
456
+ float(coherence_summary.get("value", 0.0))
457
+ if isinstance(coherence_summary, Mapping)
458
+ else 0.0
459
+ ),
460
+ "coherence_threshold": (
461
+ float(coherence_summary.get("threshold", threshold))
462
+ if isinstance(coherence_summary, Mapping)
463
+ else threshold
464
+ ),
465
+ "coherence_passed": (
466
+ bool(coherence_summary.get("passed", False))
467
+ if isinstance(coherence_summary, Mapping)
468
+ else False
469
+ ),
470
+ "frequency_value": (
471
+ float(frequency_summary.get("value", 0.0))
472
+ if isinstance(frequency_summary, Mapping)
473
+ else 0.0
474
+ ),
475
+ "frequency_passed": (
476
+ bool(frequency_summary.get("passed", False))
477
+ if isinstance(frequency_summary, Mapping)
478
+ else True
479
+ ),
480
+ "frequency_spectrum_min": (
481
+ float(frequency_summary.get("spectrum_min", 0.0))
482
+ if isinstance(frequency_summary, Mapping)
483
+ and "spectrum_min" in frequency_summary
484
+ else None
485
+ ),
486
+ "unitary_passed": bool(
487
+ summary.get("unitary_stability", {}).get("passed", False)
488
+ ),
489
+ }
490
+
491
+ node_context = {
492
+ "hilbert_space": hilbert,
493
+ "coherence_operator": coherence,
494
+ "frequency_operator": frequency,
495
+ "coherence_threshold": threshold,
496
+ "dimension": resolved_dimension,
497
+ }
498
+
499
+ node_data = G.nodes[node]
500
+ node_data["math_metrics"] = math_metrics
501
+ node_data["math_summary"] = summary
502
+ node_data["math_context"] = node_context
503
+
504
+ cfg = dict(mapping_cfg) if mapping_cfg is not None else {}
505
+ cfg.update(
506
+ {
507
+ "enabled": True,
508
+ "dimension": resolved_dimension,
509
+ "hilbert_space": hilbert,
510
+ "coherence_operator": coherence,
511
+ "coherence_threshold": threshold,
512
+ "frequency_operator": frequency,
513
+ "state_projector": projector,
514
+ "validator": spectral_validator,
515
+ "generator_matrix": generator_matrix,
516
+ "dynamics_engine": engine,
517
+ }
518
+ )
519
+ G.graph["MATH_ENGINE"] = cfg
172
520
 
173
- OPERADORES: dict[str, type[Operador]] = {
174
- Emision.name: Emision,
175
- Recepcion.name: Recepcion,
176
- Coherencia.name: Coherencia,
177
- Disonancia.name: Disonancia,
178
- Acoplamiento.name: Acoplamiento,
179
- Resonancia.name: Resonancia,
180
- Silencio.name: Silencio,
181
- Expansion.name: Expansion,
182
- Contraccion.name: Contraccion,
183
- Autoorganizacion.name: Autoorganizacion,
184
- Mutacion.name: Mutacion,
185
- Transicion.name: Transicion,
186
- Recursividad.name: Recursividad,
187
- }
521
+ return G, node
188
522
 
189
523
 
190
524
  __all__ = (
191
525
  "create_nfr",
192
- "Operador",
193
- "Emision",
194
- "Recepcion",
195
- "Coherencia",
196
- "Disonancia",
197
- "Acoplamiento",
198
- "Resonancia",
199
- "Silencio",
526
+ "create_math_nfr",
527
+ "Operator",
528
+ "Emission",
529
+ "Reception",
530
+ "Coherence",
531
+ "Dissonance",
532
+ "Coupling",
533
+ "Resonance",
534
+ "Silence",
200
535
  "Expansion",
201
- "Contraccion",
202
- "Autoorganizacion",
203
- "Mutacion",
204
- "Transicion",
205
- "Recursividad",
206
- "OPERADORES",
536
+ "Contraction",
537
+ "SelfOrganization",
538
+ "Mutation",
539
+ "Transition",
540
+ "Recursivity",
541
+ "OPERATORS",
207
542
  "validate_sequence",
208
543
  "run_sequence",
209
544
  )
210
- # ---------------------------------------------------------------------------
211
- # 3) Motor de secuencias + validador sintáctico
212
- # ---------------------------------------------------------------------------
213
-
214
-
215
- _INICIO_VALIDOS = {"emision", "recursividad"}
216
- _TRAMO_INTERMEDIO = {"disonancia", "acoplamiento", "resonancia"}
217
- _CIERRE_VALIDO = {"silencio", "transicion", "recursividad"}
218
-
219
-
220
- def _validate_start(token: str) -> tuple[bool, str]:
221
- """Ensure the sequence begins with a valid structural operator."""
222
-
223
- if not isinstance(token, str):
224
- return False, "tokens must be str"
225
- if token not in _INICIO_VALIDOS:
226
- return False, "must start with emission or recursion"
227
- return True, ""
228
-
229
545
 
230
- def _validate_intermediate(
231
- found_recepcion: bool, found_coherencia: bool, seen_intermedio: bool
232
- ) -> tuple[bool, str]:
233
- """Check that the central TNFR segment is present."""
234
546
 
235
- if not (found_recepcion and found_coherencia):
236
- return False, "missing input→coherence segment"
237
- if not seen_intermedio:
238
- return False, "missing tension/coupling/resonance segment"
239
- return True, ""
240
-
241
-
242
- def _validate_end(last_token: str, open_thol: bool) -> tuple[bool, str]:
243
- """Validate closing operator and any pending THOL blocks."""
244
-
245
- if last_token not in _CIERRE_VALIDO:
246
- return False, "sequence must end with silence/transition/recursion"
247
- if open_thol:
248
- return False, "THOL block without closure"
249
- return True, ""
250
-
251
-
252
- def _validate_known_tokens(nombres_set: set[str]) -> tuple[bool, str]:
253
- """Ensure all tokens map to canonical operators."""
254
-
255
- desconocidos = nombres_set - OPERADORES.keys()
256
- if desconocidos:
257
- return False, f"unknown tokens: {', '.join(desconocidos)}"
258
- return True, ""
259
-
260
-
261
- def _validate_token_sequence(nombres: list[str]) -> tuple[bool, str]:
262
- """Validate token format and logical coherence in one pass."""
263
-
264
- if not nombres:
265
- return False, "empty sequence"
266
-
267
- ok, msg = _validate_start(nombres[0])
268
- if not ok:
269
- return False, msg
270
-
271
- nombres_set: set[str] = set()
272
- found_recepcion = False
273
- found_coherencia = False
274
- seen_intermedio = False
275
- open_thol = False
276
-
277
- for n in nombres:
278
- if not isinstance(n, str):
279
- return False, "tokens must be str"
280
- nombres_set.add(n)
281
-
282
- if n == "recepcion" and not found_recepcion:
283
- found_recepcion = True
284
- elif found_recepcion and n == "coherencia" and not found_coherencia:
285
- found_coherencia = True
286
- elif found_coherencia and not seen_intermedio and n in _TRAMO_INTERMEDIO:
287
- seen_intermedio = True
288
-
289
- if n == "autoorganizacion":
290
- open_thol = True
291
- elif open_thol and n in {"silencio", "contraccion"}:
292
- open_thol = False
293
-
294
- ok, msg = _validate_known_tokens(nombres_set)
295
- if not ok:
296
- return False, msg
297
- ok, msg = _validate_intermediate(found_recepcion, found_coherencia, seen_intermedio)
298
- if not ok:
299
- return False, msg
300
- ok, msg = _validate_end(nombres[-1], open_thol)
301
- if not ok:
302
- return False, msg
303
- return True, "ok"
304
-
305
-
306
- def validate_sequence(nombres: list[str]) -> tuple[bool, str]:
307
- """Validate minimal TNFR syntax rules."""
308
- return _validate_token_sequence(nombres)
309
-
310
-
311
- def run_sequence(G: nx.Graph, node, ops: Iterable[Operador]) -> None:
312
- """Execute a sequence of operators on ``node`` after validation."""
547
+ def run_sequence(G: TNFRGraph, node: NodeId, ops: Iterable[Operator]) -> None:
548
+ """Drive structural sequences that rebalance EPI, νf, phase and ΔNFR.
549
+
550
+ The function enforces the canonical operator grammar, then executes each
551
+ operator so that the configured ΔNFR hook can update the nodal equation in
552
+ place. Each step is expected to express the structural effect of the
553
+ operator, while the hook keeps EPI, νf and phase consistent with the
554
+ resulting ΔNFR variations.
555
+
556
+ Parameters
557
+ ----------
558
+ G : TNFRGraph
559
+ Graph that stores the node and its ΔNFR orchestration hook. The hook is
560
+ read from ``G.graph['compute_delta_nfr']`` and is responsible for
561
+ keeping the nodal equation up to date after each operator.
562
+ node : NodeId
563
+ Identifier of the node that will receive the operators. The node must
564
+ already contain the canonical attributes ``EPI``, ``νf`` and ``θ``.
565
+ ops : Iterable[Operator]
566
+ Iterable of canonical structural operators to apply. Their
567
+ concatenation must respect the validated TNFR grammar.
568
+
569
+ Returns
570
+ -------
571
+ None
572
+ The function mutates ``G`` in-place by updating the node attributes.
573
+
574
+ Raises
575
+ ------
576
+ ValueError
577
+ Raised when the provided operator names do not satisfy the canonical
578
+ sequence validation rules.
579
+
580
+ Examples
581
+ --------
582
+ Run a validated trajectory that highlights the ΔNFR-driven evolution of the
583
+ node metrics.
584
+
585
+ >>> from tnfr.constants import DNFR_PRIMARY, EPI_PRIMARY, THETA_PRIMARY, VF_PRIMARY
586
+ >>> from tnfr.dynamics import set_delta_nfr_hook
587
+ >>> from tnfr.structural import (
588
+ ... Coherence,
589
+ ... Emission,
590
+ ... Reception,
591
+ ... Resonance,
592
+ ... Silence,
593
+ ... create_nfr,
594
+ ... run_sequence,
595
+ ... )
596
+ >>> G, node = create_nfr("seed", epi=0.8, vf=1.5, theta=0.0)
597
+ >>> def amplify_delta(graph):
598
+ ... delta = graph.nodes[node][VF_PRIMARY] * 0.15
599
+ ... graph.nodes[node][DNFR_PRIMARY] = delta
600
+ ... graph.nodes[node][EPI_PRIMARY] += delta * 0.8
601
+ ... graph.nodes[node][VF_PRIMARY] += delta * 0.1
602
+ ... graph.nodes[node][THETA_PRIMARY] += 0.02
603
+ >>> set_delta_nfr_hook(G, amplify_delta)
604
+ >>> run_sequence(G, node, [Emission(), Reception(), Coherence(), Resonance(), Silence()]) # doctest: +SKIP
605
+ >>> (
606
+ ... G.nodes[node][EPI_PRIMARY],
607
+ ... G.nodes[node][VF_PRIMARY],
608
+ ... G.nodes[node][THETA_PRIMARY],
609
+ ... G.nodes[node][DNFR_PRIMARY],
610
+ ... ) # doctest: +SKIP
611
+ (..., ..., ..., ...)
612
+ """
313
613
 
314
614
  compute = G.graph.get("compute_delta_nfr")
315
615
  ops_list = list(ops)
316
- nombres = [op.name for op in ops_list]
317
-
318
- ok, msg = validate_sequence(nombres)
319
- if not ok:
320
- raise ValueError(f"Invalid sequence: {msg}")
616
+ names = [op.name for op in ops_list]
617
+
618
+ # Initialize validators (reuse global instances for performance)
619
+ if not hasattr(run_sequence, "_invariant_validator"):
620
+ run_sequence._invariant_validator = InvariantValidator() # type: ignore[attr-defined]
621
+ if not hasattr(run_sequence, "_semantic_validator"):
622
+ run_sequence._semantic_validator = SequenceSemanticValidator() # type: ignore[attr-defined]
623
+
624
+ # Skip validation for empty sequences (TNFR: empty sequence is structural identity)
625
+ if names:
626
+ outcome = validate_sequence(names)
627
+ if not outcome.passed:
628
+ summary_message = outcome.summary.get("message", "validation failed")
629
+ raise ValueError(f"Invalid sequence: {summary_message}")
630
+
631
+ # Semantic validation of sequence (if enabled)
632
+ if validation_config.enable_semantic_validation:
633
+ semantic_violations = run_sequence._semantic_validator.validate_semantic_sequence(names) # type: ignore[attr-defined]
634
+ if semantic_violations:
635
+ # Filter by configured settings
636
+ error_violations = [
637
+ v
638
+ for v in semantic_violations
639
+ if v.severity == InvariantSeverity.ERROR
640
+ or v.severity == InvariantSeverity.CRITICAL
641
+ ]
642
+ warning_violations = [
643
+ v
644
+ for v in semantic_violations
645
+ if v.severity == InvariantSeverity.WARNING
646
+ ]
647
+
648
+ # Always raise on errors
649
+ if error_violations:
650
+ report = run_sequence._invariant_validator.generate_report(error_violations) # type: ignore[attr-defined]
651
+ raise ValueError(f"Semantic sequence violations:\n{report}")
652
+
653
+ # Show warnings if allowed
654
+ if warning_violations and validation_config.allow_semantic_warnings:
655
+ report = run_sequence._invariant_validator.generate_report(warning_violations) # type: ignore[attr-defined]
656
+ print(f"⚠️ Semantic sequence warnings:\n{report}")
657
+
658
+ # Pre-execution invariant validation (if enabled)
659
+ if validation_config.validate_invariants:
660
+ try:
661
+ run_sequence._invariant_validator.validate_and_raise( # type: ignore[attr-defined]
662
+ G, validation_config.min_severity
663
+ )
664
+ except Exception as e:
665
+ # If validation fails, provide context but don't block if it's just warnings
666
+ if validation_config.min_severity != InvariantSeverity.WARNING:
667
+ raise
321
668
 
322
669
  for op in ops_list:
670
+ # Mark last operator for tracking (for Invariant 1)
671
+ if not hasattr(G, "_last_operator_applied"):
672
+ G._last_operator_applied = None # type: ignore[attr-defined]
673
+ G._last_operator_applied = op.name # type: ignore[attr-defined]
674
+
323
675
  op(G, node)
324
676
  if callable(compute):
325
677
  compute(G)
678
+
679
+ # Per-step validation (expensive, only if configured)
680
+ if (
681
+ validation_config.validate_each_step
682
+ and validation_config.validate_invariants
683
+ ):
684
+ violations = run_sequence._invariant_validator.validate_graph( # type: ignore[attr-defined]
685
+ G, InvariantSeverity.ERROR
686
+ )
687
+ if violations:
688
+ report = run_sequence._invariant_validator.generate_report(violations) # type: ignore[attr-defined]
689
+ raise ValueError(f"Invariant violations after {op.name}:\n{report}")
690
+
326
691
  # ``update_epi_via_nodal_equation`` was previously invoked here to
327
692
  # recalculate the EPI value after each operator. The responsibility for
328
693
  # updating EPI now lies with the dynamics hook configured in
329
694
  # ``compute_delta_nfr`` or with external callers.
695
+
696
+ # Post-execution invariant validation (if enabled)
697
+ if validation_config.validate_invariants:
698
+ try:
699
+ run_sequence._invariant_validator.validate_and_raise( # type: ignore[attr-defined]
700
+ G, validation_config.min_severity
701
+ )
702
+ except Exception as e:
703
+ # If validation fails, provide context but don't block if it's just warnings
704
+ if validation_config.min_severity != InvariantSeverity.WARNING:
705
+ raise