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/structural.py ADDED
@@ -0,0 +1,705 @@
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
+ """
26
+
27
+ from __future__ import annotations
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
35
+ from .dynamics import (
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,
71
+ )
72
+ from .operators.registry import OPERATORS
73
+ from .types import DeltaNFRHook, NodeId, TNFRGraph
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]
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # 1) NFR factory
84
+ # ---------------------------------------------------------------------------
85
+
86
+
87
+ def create_nfr(
88
+ name: str,
89
+ *,
90
+ epi: float = 0.0,
91
+ vf: float = 1.0,
92
+ theta: float = 0.0,
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
+ (..., ..., ..., ...)
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
+
192
+ G = graph if graph is not None else nx.Graph()
193
+ G.add_node(
194
+ name,
195
+ **{
196
+ EPI_PRIMARY: float(epi),
197
+ VF_PRIMARY: float(vf),
198
+ THETA_PRIMARY: float(theta),
199
+ },
200
+ )
201
+ set_delta_nfr_hook(G, dnfr_hook)
202
+ return G, name
203
+
204
+
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.
367
+ """
368
+
369
+ if np is None:
370
+ raise ImportError(
371
+ "create_math_nfr requires NumPy; install the 'tnfr[math]' extras."
372
+ )
373
+
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
+ )
382
+
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
+ )
396
+
397
+ hilbert = hilbert_space or HilbertSpace(resolved_dimension)
398
+ resolved_dimension = int(getattr(hilbert, "dimension", resolved_dimension))
399
+
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
+ )
409
+
410
+ frequency = _ensure_frequency_operator(
411
+ operator=frequency_operator,
412
+ dimension=resolved_dimension,
413
+ diagonal=frequency_diagonal,
414
+ )
415
+
416
+ projector = state_projector or BasicStateProjector()
417
+
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
+ )
426
+
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
+ )
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
520
+
521
+ return G, node
522
+
523
+
524
+ __all__ = (
525
+ "create_nfr",
526
+ "create_math_nfr",
527
+ "Operator",
528
+ "Emission",
529
+ "Reception",
530
+ "Coherence",
531
+ "Dissonance",
532
+ "Coupling",
533
+ "Resonance",
534
+ "Silence",
535
+ "Expansion",
536
+ "Contraction",
537
+ "SelfOrganization",
538
+ "Mutation",
539
+ "Transition",
540
+ "Recursivity",
541
+ "OPERATORS",
542
+ "validate_sequence",
543
+ "run_sequence",
544
+ )
545
+
546
+
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
+ """
613
+
614
+ compute = G.graph.get("compute_delta_nfr")
615
+ ops_list = list(ops)
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
668
+
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
+
675
+ op(G, node)
676
+ if callable(compute):
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
+
691
+ # ``update_epi_via_nodal_equation`` was previously invoked here to
692
+ # recalculate the EPI value after each operator. The responsibility for
693
+ # updating EPI now lies with the dynamics hook configured in
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