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
@@ -0,0 +1,712 @@
1
+ """TNFR Invariant Validators.
2
+
3
+ This module implements the 10 canonical TNFR invariants as described in AGENTS.md.
4
+ Each invariant is a structural constraint that must be preserved to maintain
5
+ coherence within the TNFR paradigm.
6
+
7
+ Canonical Invariants:
8
+ 1. EPI as coherent form: changes only via structural operators
9
+ 2. Structural units: νf expressed in Hz_str (structural hertz)
10
+ 3. ΔNFR semantics: sign and magnitude modulate reorganization rate
11
+ 4. Operator closure: composition yields valid TNFR states
12
+ 5. Phase check: explicit phase verification for coupling
13
+ 6. Node birth/collapse: minimal conditions maintained
14
+ 7. Operational fractality: EPIs can nest without losing identity
15
+ 8. Controlled determinism: reproducible and traceable
16
+ 9. Structural metrics: expose C(t), Si, phase, νf
17
+ 10. Domain neutrality: trans-scale and trans-domain
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import math
23
+ from abc import ABC, abstractmethod
24
+ from dataclasses import dataclass
25
+ from enum import Enum
26
+ from typing import Any, Optional
27
+
28
+ from ..constants import DEFAULTS, DNFR_PRIMARY, EPI_PRIMARY, THETA_PRIMARY, VF_PRIMARY
29
+ from ..types import TNFRGraph
30
+
31
+ __all__ = [
32
+ "InvariantSeverity",
33
+ "InvariantViolation",
34
+ "TNFRInvariant",
35
+ "Invariant1_EPIOnlyThroughOperators",
36
+ "Invariant2_VfInHzStr",
37
+ "Invariant3_DNFRSemantics",
38
+ "Invariant4_OperatorClosure",
39
+ "Invariant5_ExplicitPhaseChecks",
40
+ "Invariant6_NodeBirthCollapse",
41
+ "Invariant7_OperationalFractality",
42
+ "Invariant8_ControlledDeterminism",
43
+ "Invariant9_StructuralMetrics",
44
+ "Invariant10_DomainNeutrality",
45
+ ]
46
+
47
+
48
+ class InvariantSeverity(Enum):
49
+ """Severity levels for invariant violations."""
50
+
51
+ INFO = "info" # Information, not a problem
52
+ WARNING = "warning" # Minor inconsistency
53
+ ERROR = "error" # Violation that prevents execution
54
+ CRITICAL = "critical" # Data corruption
55
+
56
+
57
+ @dataclass
58
+ class InvariantViolation:
59
+ """Detailed description of invariant violation."""
60
+
61
+ invariant_id: int
62
+ severity: InvariantSeverity
63
+ description: str
64
+ node_id: Optional[str] = None
65
+ expected_value: Optional[Any] = None
66
+ actual_value: Optional[Any] = None
67
+ suggestion: Optional[str] = None
68
+
69
+
70
+ class TNFRInvariant(ABC):
71
+ """Base class for TNFR invariant validators."""
72
+
73
+ @property
74
+ @abstractmethod
75
+ def invariant_id(self) -> int:
76
+ """TNFR invariant number (1-10)."""
77
+
78
+ @property
79
+ @abstractmethod
80
+ def description(self) -> str:
81
+ """Human-readable description of the invariant."""
82
+
83
+ @abstractmethod
84
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
85
+ """Validates invariant in the graph, returns found violations."""
86
+
87
+
88
+ class Invariant1_EPIOnlyThroughOperators(TNFRInvariant):
89
+ """Invariant 1: EPI changes only through structural operators."""
90
+
91
+ invariant_id = 1
92
+ description = "EPI changes only through structural operators"
93
+
94
+ def __init__(self) -> None:
95
+ self._previous_epi_values: dict[Any, float] = {}
96
+
97
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
98
+ violations = []
99
+
100
+ # Get configuration bounds
101
+ config = getattr(graph, "graph", {})
102
+ epi_min = config.get("EPI_MIN", DEFAULTS.get("EPI_MIN", 0.0))
103
+ epi_max = config.get("EPI_MAX", DEFAULTS.get("EPI_MAX", 1.0))
104
+
105
+ for node_id in graph.nodes():
106
+ node_data = graph.nodes[node_id]
107
+ current_epi = node_data.get(EPI_PRIMARY, 0.0)
108
+
109
+ # Handle complex EPI structures (dict, complex numbers)
110
+ # Extract scalar value for validation
111
+ if isinstance(current_epi, dict):
112
+ # EPI can be a dict with 'continuous', 'discrete', 'grid' keys
113
+ # Try to extract a scalar value for validation
114
+ if "continuous" in current_epi:
115
+ epi_value = current_epi["continuous"]
116
+ if isinstance(epi_value, (tuple, list)) and len(epi_value) > 0:
117
+ epi_value = epi_value[0]
118
+ if isinstance(epi_value, complex):
119
+ epi_value = abs(epi_value)
120
+ current_epi = (
121
+ float(epi_value)
122
+ if isinstance(epi_value, (int, float, complex))
123
+ else 0.0
124
+ )
125
+ else:
126
+ # Skip validation for complex structures we can't interpret
127
+ continue
128
+
129
+ elif isinstance(current_epi, complex):
130
+ # For complex numbers, use magnitude
131
+ current_epi = abs(current_epi)
132
+
133
+ # Verificar rango válido de EPI
134
+ if not (epi_min <= current_epi <= epi_max):
135
+ violations.append(
136
+ InvariantViolation(
137
+ invariant_id=1,
138
+ severity=InvariantSeverity.ERROR,
139
+ description=f"EPI out of valid range [{epi_min},{epi_max}]",
140
+ node_id=str(node_id),
141
+ expected_value=f"{epi_min} <= EPI <= {epi_max}",
142
+ actual_value=current_epi,
143
+ suggestion="Check operator implementation for EPI clamping",
144
+ )
145
+ )
146
+
147
+ # Verificar que EPI es un número finito
148
+ if not isinstance(current_epi, (int, float)) or not math.isfinite(
149
+ current_epi
150
+ ):
151
+ violations.append(
152
+ InvariantViolation(
153
+ invariant_id=1,
154
+ severity=InvariantSeverity.CRITICAL,
155
+ description="EPI is not a finite number",
156
+ node_id=str(node_id),
157
+ expected_value="finite float",
158
+ actual_value=f"{type(current_epi).__name__}: {current_epi}",
159
+ suggestion="Check operator implementation for EPI assignment",
160
+ )
161
+ )
162
+
163
+ # Detectar cambios no autorizados (requiere tracking)
164
+ # Solo verificar si hay un operador previo registrado
165
+ if hasattr(graph, "_last_operator_applied"):
166
+ if node_id in self._previous_epi_values:
167
+ prev_epi = self._previous_epi_values[node_id]
168
+ if abs(current_epi - prev_epi) > 1e-10: # Cambio detectado
169
+ if not graph._last_operator_applied:
170
+ violations.append(
171
+ InvariantViolation(
172
+ invariant_id=1,
173
+ severity=InvariantSeverity.CRITICAL,
174
+ description="EPI changed without operator application",
175
+ node_id=str(node_id),
176
+ expected_value=prev_epi,
177
+ actual_value=current_epi,
178
+ suggestion="Ensure all EPI modifications go through structural operators",
179
+ )
180
+ )
181
+
182
+ # Actualizar tracking
183
+ for node_id in graph.nodes():
184
+ epi_value = graph.nodes[node_id].get(EPI_PRIMARY, 0.0)
185
+ # Store scalar value for tracking
186
+ if isinstance(epi_value, dict) and "continuous" in epi_value:
187
+ epi_val = epi_value["continuous"]
188
+ if isinstance(epi_val, (tuple, list)) and len(epi_val) > 0:
189
+ epi_val = epi_val[0]
190
+ if isinstance(epi_val, complex):
191
+ epi_val = abs(epi_val)
192
+ epi_value = (
193
+ float(epi_val)
194
+ if isinstance(epi_val, (int, float, complex))
195
+ else 0.0
196
+ )
197
+ elif isinstance(epi_value, complex):
198
+ epi_value = abs(epi_value)
199
+
200
+ self._previous_epi_values[node_id] = epi_value
201
+
202
+ return violations
203
+
204
+
205
+ class Invariant2_VfInHzStr(TNFRInvariant):
206
+ """Invariante 2: νf stays in Hz_str units."""
207
+
208
+ invariant_id = 2
209
+ description = "νf stays in Hz_str units"
210
+
211
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
212
+ violations = []
213
+
214
+ # Get configuration bounds
215
+ config = getattr(graph, "graph", {})
216
+ vf_min = config.get("VF_MIN", DEFAULTS.get("VF_MIN", 0.001))
217
+ vf_max = config.get("VF_MAX", DEFAULTS.get("VF_MAX", 1000.0))
218
+
219
+ for node_id in graph.nodes():
220
+ node_data = graph.nodes[node_id]
221
+ vf = node_data.get(VF_PRIMARY, 0.0)
222
+
223
+ # Verificar rango estructural válido (Hz_str)
224
+ if not (vf_min <= vf <= vf_max):
225
+ violations.append(
226
+ InvariantViolation(
227
+ invariant_id=2,
228
+ severity=InvariantSeverity.ERROR,
229
+ description=f"νf outside typical Hz_str range [{vf_min}, {vf_max}]",
230
+ node_id=str(node_id),
231
+ expected_value=f"{vf_min} <= νf <= {vf_max} Hz_str",
232
+ actual_value=vf,
233
+ suggestion="Verify νf units and operator calculations",
234
+ )
235
+ )
236
+
237
+ # Verificar que sea un número válido
238
+ if not isinstance(vf, (int, float)) or not math.isfinite(vf):
239
+ violations.append(
240
+ InvariantViolation(
241
+ invariant_id=2,
242
+ severity=InvariantSeverity.CRITICAL,
243
+ description="νf is not a finite number",
244
+ node_id=str(node_id),
245
+ expected_value="finite float",
246
+ actual_value=f"{type(vf).__name__}: {vf}",
247
+ suggestion="Check operator implementation for νf assignment",
248
+ )
249
+ )
250
+
251
+ # Verificar que νf sea positivo (requerimiento estructural)
252
+ if isinstance(vf, (int, float)) and vf <= 0:
253
+ violations.append(
254
+ InvariantViolation(
255
+ invariant_id=2,
256
+ severity=InvariantSeverity.ERROR,
257
+ description="νf must be positive (structural frequency)",
258
+ node_id=str(node_id),
259
+ expected_value="νf > 0",
260
+ actual_value=vf,
261
+ suggestion="Structural frequency must be positive for coherent nodes",
262
+ )
263
+ )
264
+
265
+ return violations
266
+
267
+
268
+ class Invariant5_ExplicitPhaseChecks(TNFRInvariant):
269
+ """Invariante 5: Explicit phase checks for coupling."""
270
+
271
+ invariant_id = 5
272
+ description = "Explicit phase checks for coupling"
273
+
274
+ def __init__(self, phase_coupling_threshold: float = math.pi / 2) -> None:
275
+ self.phase_coupling_threshold = phase_coupling_threshold
276
+
277
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
278
+ violations = []
279
+
280
+ for node_id in graph.nodes():
281
+ node_data = graph.nodes[node_id]
282
+ phase = node_data.get(THETA_PRIMARY, 0.0)
283
+
284
+ # Verificar que phase sea un número finito
285
+ if not isinstance(phase, (int, float)) or not math.isfinite(phase):
286
+ violations.append(
287
+ InvariantViolation(
288
+ invariant_id=5,
289
+ severity=InvariantSeverity.CRITICAL,
290
+ description="Phase is not a finite number",
291
+ node_id=str(node_id),
292
+ expected_value="finite float",
293
+ actual_value=f"{type(phase).__name__}: {phase}",
294
+ suggestion="Check operator implementation for phase assignment",
295
+ )
296
+ )
297
+ continue
298
+
299
+ # Verificar rango de fase [0, 2π] o normalizable
300
+ # TNFR permite fases fuera de este rango si se pueden normalizar
301
+ # Emitir warning si la fase no está en el rango canónico
302
+ if not (0.0 <= phase <= 2 * math.pi):
303
+ violations.append(
304
+ InvariantViolation(
305
+ invariant_id=5,
306
+ severity=InvariantSeverity.WARNING,
307
+ description="Phase outside [0, 2π] range (normalization possible)",
308
+ node_id=str(node_id),
309
+ expected_value="0.0 <= phase <= 2π",
310
+ actual_value=phase,
311
+ suggestion="Consider normalizing phase to [0, 2π] range",
312
+ )
313
+ )
314
+
315
+ # Verificar sincronización en nodos acoplados (edges)
316
+ if hasattr(graph, "edges"):
317
+ for edge in graph.edges():
318
+ node1, node2 = edge
319
+ phase1 = graph.nodes[node1].get(THETA_PRIMARY, 0.0)
320
+ phase2 = graph.nodes[node2].get(THETA_PRIMARY, 0.0)
321
+
322
+ # Verificar que ambas fases sean números finitos antes de calcular diferencia
323
+ if not (
324
+ isinstance(phase1, (int, float))
325
+ and math.isfinite(phase1)
326
+ and isinstance(phase2, (int, float))
327
+ and math.isfinite(phase2)
328
+ ):
329
+ continue
330
+
331
+ phase_diff = abs(phase1 - phase2)
332
+ # Considerar periodicidad
333
+ phase_diff = min(phase_diff, 2 * math.pi - phase_diff)
334
+
335
+ # Si la diferencia es muy grande, puede indicar desacoplamiento
336
+ if phase_diff > self.phase_coupling_threshold:
337
+ violations.append(
338
+ InvariantViolation(
339
+ invariant_id=5,
340
+ severity=InvariantSeverity.WARNING,
341
+ description="Large phase difference between coupled nodes",
342
+ node_id=f"{node1}-{node2}",
343
+ expected_value=f"< {self.phase_coupling_threshold}",
344
+ actual_value=phase_diff,
345
+ suggestion="Check coupling strength or phase coordination",
346
+ )
347
+ )
348
+
349
+ return violations
350
+
351
+
352
+ class Invariant3_DNFRSemantics(TNFRInvariant):
353
+ """Invariante 3: ΔNFR semantics - sign and magnitude modulate reorganization rate."""
354
+
355
+ invariant_id = 3
356
+ description = "ΔNFR semantics: sign and magnitude modulate reorganization rate"
357
+
358
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
359
+ violations = []
360
+
361
+ for node_id in graph.nodes():
362
+ node_data = graph.nodes[node_id]
363
+ dnfr = node_data.get(DNFR_PRIMARY, 0.0)
364
+
365
+ # Verificar que ΔNFR es un número finito
366
+ if not isinstance(dnfr, (int, float)) or not math.isfinite(dnfr):
367
+ violations.append(
368
+ InvariantViolation(
369
+ invariant_id=3,
370
+ severity=InvariantSeverity.CRITICAL,
371
+ description="ΔNFR is not a finite number",
372
+ node_id=str(node_id),
373
+ expected_value="finite float",
374
+ actual_value=f"{type(dnfr).__name__}: {dnfr}",
375
+ suggestion="Check operator implementation for ΔNFR calculation",
376
+ )
377
+ )
378
+
379
+ # Verificar que ΔNFR no se trata como error/loss gradient
380
+ # (esto es más conceptual, pero podemos verificar rangos razonables)
381
+ if isinstance(dnfr, (int, float)) and math.isfinite(dnfr):
382
+ # ΔNFR excesivamente grande podría indicar tratamiento erróneo
383
+ if abs(dnfr) > 1000.0:
384
+ violations.append(
385
+ InvariantViolation(
386
+ invariant_id=3,
387
+ severity=InvariantSeverity.WARNING,
388
+ description="ΔNFR magnitude is unusually large",
389
+ node_id=str(node_id),
390
+ expected_value="|ΔNFR| < 1000",
391
+ actual_value=abs(dnfr),
392
+ suggestion="Verify ΔNFR is not being misused as error gradient",
393
+ )
394
+ )
395
+
396
+ return violations
397
+
398
+
399
+ class Invariant4_OperatorClosure(TNFRInvariant):
400
+ """Invariante 4: Operator closure - composition yields valid TNFR states."""
401
+
402
+ invariant_id = 4
403
+ description = "Operator closure: composition yields valid TNFR states"
404
+
405
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
406
+ violations = []
407
+
408
+ # Verificar que el grafo mantiene estado válido después de operadores
409
+ for node_id in graph.nodes():
410
+ node_data = graph.nodes[node_id]
411
+
412
+ # Verificar que los atributos esenciales existen
413
+ required_attrs = [EPI_PRIMARY, VF_PRIMARY, THETA_PRIMARY]
414
+ missing_attrs = [attr for attr in required_attrs if attr not in node_data]
415
+
416
+ if missing_attrs:
417
+ violations.append(
418
+ InvariantViolation(
419
+ invariant_id=4,
420
+ severity=InvariantSeverity.CRITICAL,
421
+ description=f"Node missing required TNFR attributes: {missing_attrs}",
422
+ node_id=str(node_id),
423
+ expected_value="All TNFR attributes present",
424
+ actual_value=f"Missing: {missing_attrs}",
425
+ suggestion="Operator composition broke TNFR state structure",
426
+ )
427
+ )
428
+
429
+ # Verificar que el grafo tiene hook ΔNFR
430
+ if hasattr(graph, "graph"):
431
+ if "compute_delta_nfr" not in graph.graph:
432
+ violations.append(
433
+ InvariantViolation(
434
+ invariant_id=4,
435
+ severity=InvariantSeverity.WARNING,
436
+ description="Graph missing ΔNFR computation hook",
437
+ expected_value="compute_delta_nfr hook present",
438
+ actual_value="Hook missing",
439
+ suggestion="Ensure ΔNFR hook is installed for proper operator closure",
440
+ )
441
+ )
442
+
443
+ return violations
444
+
445
+
446
+ class Invariant6_NodeBirthCollapse(TNFRInvariant):
447
+ """Invariante 6: Node birth/collapse - minimal conditions maintained."""
448
+
449
+ invariant_id = 6
450
+ description = "Node birth/collapse: minimal conditions maintained"
451
+
452
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
453
+ violations = []
454
+
455
+ for node_id in graph.nodes():
456
+ node_data = graph.nodes[node_id]
457
+ vf = node_data.get(VF_PRIMARY, 0.0)
458
+ dnfr = node_data.get(DNFR_PRIMARY, 0.0)
459
+
460
+ # Extract scalar values if needed
461
+ if isinstance(vf, dict) and "continuous" in vf:
462
+ continue # Skip complex structures
463
+
464
+ if isinstance(dnfr, dict):
465
+ continue # Skip complex structures
466
+
467
+ # Condiciones mínimas de nacimiento: νf suficiente
468
+ if isinstance(vf, (int, float)) and vf < 0.001:
469
+ violations.append(
470
+ InvariantViolation(
471
+ invariant_id=6,
472
+ severity=InvariantSeverity.WARNING,
473
+ description="Node has insufficient νf for sustained existence",
474
+ node_id=str(node_id),
475
+ expected_value="νf >= 0.001",
476
+ actual_value=vf,
477
+ suggestion="Node may be approaching collapse condition",
478
+ )
479
+ )
480
+
481
+ # Condiciones de colapso: ΔNFR extremo o νf cercano a cero
482
+ if isinstance(dnfr, (int, float)) and math.isfinite(dnfr):
483
+ if abs(dnfr) > 10.0: # Dissonance extrema
484
+ violations.append(
485
+ InvariantViolation(
486
+ invariant_id=6,
487
+ severity=InvariantSeverity.WARNING,
488
+ description="Node experiencing extreme dissonance (collapse risk)",
489
+ node_id=str(node_id),
490
+ expected_value="|ΔNFR| < 10",
491
+ actual_value=abs(dnfr),
492
+ suggestion="High dissonance may trigger node collapse",
493
+ )
494
+ )
495
+
496
+ return violations
497
+
498
+
499
+ class Invariant7_OperationalFractality(TNFRInvariant):
500
+ """Invariante 7: Operational fractality - EPIs can nest without losing identity."""
501
+
502
+ invariant_id = 7
503
+ description = "Operational fractality: EPIs can nest without losing identity"
504
+
505
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
506
+ violations = []
507
+
508
+ # Verificar que estructuras EPI complejas mantienen identidad
509
+ for node_id in graph.nodes():
510
+ node_data = graph.nodes[node_id]
511
+ epi = node_data.get(EPI_PRIMARY, 0.0)
512
+
513
+ # Si EPI es una estructura anidada, verificar integridad
514
+ if isinstance(epi, dict):
515
+ # Verificar que tiene las claves esperadas para fractality
516
+ expected_keys = {"continuous", "discrete", "grid"}
517
+ actual_keys = set(epi.keys())
518
+
519
+ if not actual_keys.issubset(expected_keys):
520
+ violations.append(
521
+ InvariantViolation(
522
+ invariant_id=7,
523
+ severity=InvariantSeverity.WARNING,
524
+ description="EPI structure has unexpected keys (fractality may be broken)",
525
+ node_id=str(node_id),
526
+ expected_value=f"Keys subset of {expected_keys}",
527
+ actual_value=f"Keys: {actual_keys}",
528
+ suggestion="Verify nested EPI structure maintains identity",
529
+ )
530
+ )
531
+
532
+ # Verificar que los sub-EPIs tienen valores válidos
533
+ for key in ["continuous", "discrete"]:
534
+ if key in epi:
535
+ sub_epi = epi[key]
536
+ if isinstance(sub_epi, (tuple, list)):
537
+ for val in sub_epi:
538
+ if isinstance(val, complex) and not math.isfinite(
539
+ abs(val)
540
+ ):
541
+ violations.append(
542
+ InvariantViolation(
543
+ invariant_id=7,
544
+ severity=InvariantSeverity.ERROR,
545
+ description=f"Sub-EPI '{key}' contains non-finite values",
546
+ node_id=str(node_id),
547
+ expected_value="finite values",
548
+ actual_value=f"{val}",
549
+ suggestion="Nested EPI identity compromised by invalid values",
550
+ )
551
+ )
552
+
553
+ return violations
554
+
555
+
556
+ class Invariant8_ControlledDeterminism(TNFRInvariant):
557
+ """Invariante 8: Controlled determinism - reproducible and traceable."""
558
+
559
+ invariant_id = 8
560
+ description = "Controlled determinism: reproducible and traceable"
561
+
562
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
563
+ violations = []
564
+
565
+ # Verificar que hay trazabilidad (history)
566
+ if hasattr(graph, "graph"):
567
+ config = graph.graph
568
+
569
+ # Verificar que hay sistema de history/trace
570
+ if "history" not in config and "HISTORY_MAXLEN" not in config:
571
+ violations.append(
572
+ InvariantViolation(
573
+ invariant_id=8,
574
+ severity=InvariantSeverity.WARNING,
575
+ description="No history tracking configured (traceability compromised)",
576
+ expected_value="history or HISTORY_MAXLEN in config",
577
+ actual_value="Not found",
578
+ suggestion="Configure history tracking for reproducibility",
579
+ )
580
+ )
581
+
582
+ # Verificar que hay seed configurado para reproducibilidad
583
+ if "RANDOM_SEED" not in config and "seed" not in config:
584
+ violations.append(
585
+ InvariantViolation(
586
+ invariant_id=8,
587
+ severity=InvariantSeverity.WARNING,
588
+ description="No random seed configured (reproducibility at risk)",
589
+ expected_value="RANDOM_SEED or seed in config",
590
+ actual_value="Not found",
591
+ suggestion="Set random seed for deterministic simulations",
592
+ )
593
+ )
594
+
595
+ return violations
596
+
597
+
598
+ class Invariant9_StructuralMetrics(TNFRInvariant):
599
+ """Invariante 9: Structural metrics - expose C(t), Si, phase, νf."""
600
+
601
+ invariant_id = 9
602
+ description = "Structural metrics: expose C(t), Si, phase, νf"
603
+
604
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
605
+ violations = []
606
+
607
+ # Verificar que los nodos exponen métricas estructurales
608
+ for node_id in graph.nodes():
609
+ node_data = graph.nodes[node_id]
610
+
611
+ # Verificar métricas básicas (νf, phase ya verificados en otros invariantes)
612
+ # Aquí verificamos métricas derivadas si existen
613
+
614
+ # Si hay métrica Si (sense index), verificar que es válida
615
+ if "Si" in node_data or "si" in node_data:
616
+ si = node_data.get("Si", node_data.get("si", 0.0))
617
+ if isinstance(si, (int, float)):
618
+ if not (0.0 <= si <= 1.0):
619
+ violations.append(
620
+ InvariantViolation(
621
+ invariant_id=9,
622
+ severity=InvariantSeverity.WARNING,
623
+ description="Sense index (Si) outside expected range",
624
+ node_id=str(node_id),
625
+ expected_value="0.0 <= Si <= 1.0",
626
+ actual_value=si,
627
+ suggestion="Verify Si calculation maintains TNFR semantics",
628
+ )
629
+ )
630
+
631
+ # Verificar que hay métricas globales de coherencia
632
+ if hasattr(graph, "graph"):
633
+ config = graph.graph
634
+ has_coherence_metric = (
635
+ "coherence" in config or "C_t" in config or "total_coherence" in config
636
+ )
637
+
638
+ if not has_coherence_metric:
639
+ violations.append(
640
+ InvariantViolation(
641
+ invariant_id=9,
642
+ severity=InvariantSeverity.WARNING,
643
+ description="No global coherence metric C(t) exposed",
644
+ expected_value="C(t) or coherence metric in graph",
645
+ actual_value="Not found",
646
+ suggestion="Expose total coherence C(t) for structural metrics",
647
+ )
648
+ )
649
+
650
+ return violations
651
+
652
+
653
+ class Invariant10_DomainNeutrality(TNFRInvariant):
654
+ """Invariante 10: Domain neutrality - trans-scale and trans-domain."""
655
+
656
+ invariant_id = 10
657
+ description = "Domain neutrality: trans-scale and trans-domain"
658
+
659
+ def validate(self, graph: TNFRGraph) -> list[InvariantViolation]:
660
+ violations = []
661
+
662
+ # Verificar que no hay hard-coded domain assumptions
663
+ if hasattr(graph, "graph"):
664
+ config = graph.graph
665
+
666
+ # Buscar claves que sugieran assumptions específicas de dominio
667
+ domain_specific_keys = [
668
+ "physical_units",
669
+ "meters",
670
+ "seconds",
671
+ "temperature",
672
+ "biology",
673
+ "neurons",
674
+ "particles",
675
+ ]
676
+
677
+ found_domain_keys = [key for key in domain_specific_keys if key in config]
678
+
679
+ if found_domain_keys:
680
+ violations.append(
681
+ InvariantViolation(
682
+ invariant_id=10,
683
+ severity=InvariantSeverity.WARNING,
684
+ description=f"Domain-specific keys found: {found_domain_keys}",
685
+ expected_value="Domain-neutral configuration",
686
+ actual_value=f"Found: {found_domain_keys}",
687
+ suggestion="Remove domain-specific assumptions from core engine",
688
+ )
689
+ )
690
+
691
+ # Verificar que las unidades son estructurales (Hz_str, no Hz físicos)
692
+ for node_id in graph.nodes():
693
+ node_data = graph.nodes[node_id]
694
+
695
+ # Si hay unidades explícitas, deben ser estructurales
696
+ if "units" in node_data:
697
+ units = node_data["units"]
698
+ if isinstance(units, dict) and "vf" in units:
699
+ if units["vf"] not in ["Hz_str", "structural_hertz", None]:
700
+ violations.append(
701
+ InvariantViolation(
702
+ invariant_id=10,
703
+ severity=InvariantSeverity.ERROR,
704
+ description=f"Non-structural units for νf: {units['vf']}",
705
+ node_id=str(node_id),
706
+ expected_value="Hz_str or structural_hertz",
707
+ actual_value=units["vf"],
708
+ suggestion="Use structural units (Hz_str) not physical units",
709
+ )
710
+ )
711
+
712
+ return violations