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
@@ -0,0 +1,1226 @@
1
+ """Precondition validators for TNFR structural operators.
2
+
3
+ Each operator has specific requirements that must be met before execution
4
+ to maintain TNFR structural invariants. This package provides validators
5
+ for each of the 13 canonical operators.
6
+
7
+ The preconditions package has been restructured to support both legacy
8
+ imports (from ..preconditions import validate_*) and new modular imports
9
+ (from ..preconditions.emission import validate_emission_strict).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ if TYPE_CHECKING:
17
+ from ...types import NodeId, TNFRGraph
18
+ import logging
19
+
20
+ from ...alias import get_attr
21
+ from ...config.operator_names import (
22
+ BIFURCATION_WINDOWS,
23
+ DESTABILIZERS_MODERATE,
24
+ DESTABILIZERS_STRONG,
25
+ DESTABILIZERS_WEAK,
26
+ )
27
+ from ...constants.aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_THETA, ALIAS_VF
28
+
29
+ __all__ = [
30
+ "OperatorPreconditionError",
31
+ "validate_emission",
32
+ "validate_reception",
33
+ "validate_coherence",
34
+ "validate_dissonance",
35
+ "validate_coupling",
36
+ "validate_resonance",
37
+ "validate_silence",
38
+ "validate_expansion",
39
+ "validate_contraction",
40
+ "validate_self_organization",
41
+ "validate_mutation",
42
+ "validate_transition",
43
+ "validate_recursivity",
44
+ "diagnose_coherence_readiness",
45
+ "diagnose_resonance_readiness",
46
+ "diagnose_mutation_readiness",
47
+ ]
48
+
49
+
50
+ class OperatorPreconditionError(Exception):
51
+ """Raised when an operator's preconditions are not met."""
52
+
53
+ def __init__(self, operator: str, reason: str) -> None:
54
+ """Initialize precondition error.
55
+
56
+ Parameters
57
+ ----------
58
+ operator : str
59
+ Name of the operator that failed validation
60
+ reason : str
61
+ Description of why the precondition failed
62
+ """
63
+ self.operator = operator
64
+ self.reason = reason
65
+ super().__init__(f"{operator}: {reason}")
66
+
67
+
68
+ def _get_node_attr(
69
+ G: "TNFRGraph", node: "NodeId", aliases: tuple[str, ...], default: float = 0.0
70
+ ) -> float:
71
+ """Get node attribute using alias fallback."""
72
+ return float(get_attr(G.nodes[node], aliases, default))
73
+
74
+
75
+ def validate_emission(G: "TNFRGraph", node: "NodeId") -> None:
76
+ """AL - Emission requires node in latent or low activation state.
77
+
78
+ Parameters
79
+ ----------
80
+ G : TNFRGraph
81
+ Graph containing the node
82
+ node : NodeId
83
+ Node to validate
84
+
85
+ Raises
86
+ ------
87
+ OperatorPreconditionError
88
+ If EPI is already too high for emission to be meaningful
89
+ """
90
+ epi = _get_node_attr(G, node, ALIAS_EPI)
91
+ # Emission is meant to activate latent nodes, not boost already active ones
92
+ # This is a soft threshold - configurable via graph metadata
93
+ max_epi = float(G.graph.get("AL_MAX_EPI_FOR_EMISSION", 0.8))
94
+ if epi >= max_epi:
95
+ raise OperatorPreconditionError(
96
+ "Emission", f"Node already active (EPI={epi:.3f} >= {max_epi:.3f})"
97
+ )
98
+
99
+
100
+ def validate_reception(G: "TNFRGraph", node: "NodeId") -> None:
101
+ """EN - Reception requires node to have neighbors to receive from.
102
+
103
+ Parameters
104
+ ----------
105
+ G : TNFRGraph
106
+ Graph containing the node
107
+ node : NodeId
108
+ Node to validate
109
+
110
+ Raises
111
+ ------
112
+ OperatorPreconditionError
113
+ If node has no neighbors to receive energy from
114
+ """
115
+ neighbors = list(G.neighbors(node))
116
+ if not neighbors:
117
+ raise OperatorPreconditionError(
118
+ "Reception", "Node has no neighbors to receive energy from"
119
+ )
120
+
121
+
122
+ def validate_coherence(G: "TNFRGraph", node: "NodeId") -> None:
123
+ """IL - Coherence requires active EPI, νf, and manageable ΔNFR.
124
+
125
+ This function delegates to the strict validation implementation
126
+ in coherence.py module, which provides comprehensive canonical
127
+ precondition checks according to TNFR.pdf §2.2.1.
128
+
129
+ Parameters
130
+ ----------
131
+ G : TNFRGraph
132
+ Graph containing the node
133
+ node : NodeId
134
+ Node to validate
135
+
136
+ Raises
137
+ ------
138
+ ValueError
139
+ If critical preconditions are not met (active EPI, νf, non-saturated state)
140
+
141
+ Warnings
142
+ --------
143
+ UserWarning
144
+ For suboptimal conditions (zero ΔNFR, critical ΔNFR, isolated node)
145
+
146
+ Notes
147
+ -----
148
+ For backward compatibility, this function maintains the same signature
149
+ as the legacy validate_coherence but now provides enhanced validation.
150
+
151
+ See Also
152
+ --------
153
+ tnfr.operators.preconditions.coherence.validate_coherence_strict : Full implementation
154
+ """
155
+ from .coherence import validate_coherence_strict
156
+
157
+ validate_coherence_strict(G, node)
158
+
159
+
160
+ def diagnose_coherence_readiness(G: "TNFRGraph", node: "NodeId") -> dict:
161
+ """Diagnose node readiness for IL (Coherence) operator.
162
+
163
+ Provides comprehensive diagnostic report with readiness status and
164
+ actionable recommendations for IL operator application.
165
+
166
+ Parameters
167
+ ----------
168
+ G : TNFRGraph
169
+ Graph containing the node
170
+ node : NodeId
171
+ Node to diagnose
172
+
173
+ Returns
174
+ -------
175
+ dict
176
+ Diagnostic report with readiness status, check results, values, and recommendations
177
+
178
+ See Also
179
+ --------
180
+ tnfr.operators.preconditions.coherence.diagnose_coherence_readiness : Full implementation
181
+ """
182
+ from .coherence import diagnose_coherence_readiness as _diagnose
183
+
184
+ return _diagnose(G, node)
185
+
186
+
187
+ def validate_dissonance(G: "TNFRGraph", node: "NodeId") -> None:
188
+ """OZ - Dissonance requires comprehensive structural preconditions.
189
+
190
+ This function delegates to the strict validation implementation in
191
+ dissonance.py module, which provides canonical precondition checks:
192
+
193
+ 1. Minimum coherence base (EPI >= threshold)
194
+ 2. ΔNFR not critically high (avoid overload)
195
+ 3. Sufficient νf for reorganization response
196
+ 4. No overload pattern (sobrecarga disonante)
197
+ 5. Network connectivity (warning)
198
+
199
+ Also detects bifurcation readiness when ∂²EPI/∂t² > τ, enabling
200
+ alternative structural paths (ZHIR, NUL, IL, THOL).
201
+
202
+ Parameters
203
+ ----------
204
+ G : TNFRGraph
205
+ Graph containing the node
206
+ node : NodeId
207
+ Node to validate
208
+
209
+ Raises
210
+ ------
211
+ OperatorPreconditionError
212
+ If critical preconditions are not met (EPI, ΔNFR, νf, overload)
213
+
214
+ Notes
215
+ -----
216
+ For backward compatibility, this function maintains the same signature
217
+ as the legacy validate_dissonance but now provides enhanced validation.
218
+
219
+ When bifurcation threshold is exceeded, sets node['_bifurcation_ready'] = True
220
+ and logs the event for telemetry.
221
+
222
+ See Also
223
+ --------
224
+ tnfr.operators.preconditions.dissonance.validate_dissonance_strict : Full implementation
225
+ """
226
+ import logging
227
+
228
+ logger = logging.getLogger(__name__)
229
+
230
+ # First, apply strict canonical preconditions
231
+ # This validates EPI, ΔNFR, νf, overload, and connectivity
232
+ from .dissonance import validate_dissonance_strict
233
+
234
+ try:
235
+ validate_dissonance_strict(G, node)
236
+ except ValueError as e:
237
+ # Convert ValueError to OperatorPreconditionError for backward compatibility
238
+ raise OperatorPreconditionError(
239
+ "Dissonance", str(e).replace("OZ precondition failed: ", "")
240
+ )
241
+
242
+ # Check bifurcation readiness using existing THOL infrastructure
243
+ # Reuse _compute_epi_acceleration from SelfOrganization
244
+ from ..definitions import SelfOrganization
245
+
246
+ thol_instance = SelfOrganization()
247
+ d2_epi = thol_instance._compute_epi_acceleration(G, node)
248
+
249
+ # Get bifurcation threshold
250
+ tau = float(G.graph.get("BIFURCATION_THRESHOLD_TAU", 0.5))
251
+
252
+ # Store d²EPI for telemetry (using existing ALIAS_D2EPI)
253
+ from ...alias import set_attr
254
+ from ...constants.aliases import ALIAS_D2EPI
255
+
256
+ set_attr(G.nodes[node], ALIAS_D2EPI, d2_epi)
257
+
258
+ # Check if bifurcation threshold exceeded
259
+ if d2_epi > tau:
260
+ # Mark node as bifurcation-ready
261
+ G.nodes[node]["_bifurcation_ready"] = True
262
+ logger.info(
263
+ f"Node {node}: bifurcation threshold exceeded "
264
+ f"(∂²EPI/∂t²={d2_epi:.3f} > τ={tau}). "
265
+ f"Alternative structural paths enabled."
266
+ )
267
+ else:
268
+ # Clear flag if previously set
269
+ G.nodes[node]["_bifurcation_ready"] = False
270
+
271
+
272
+ def validate_coupling(G: "TNFRGraph", node: "NodeId") -> None:
273
+ """UM - Coupling requires active nodes with compatible phases.
274
+
275
+ Validates comprehensive canonical preconditions for the UM (Coupling) operator
276
+ according to TNFR theory:
277
+
278
+ 1. **Graph connectivity**: At least one other node exists for coupling
279
+ 2. **Active EPI**: Node has sufficient structural form (EPI > threshold)
280
+ 3. **Structural frequency**: Node has capacity for synchronization (νf > threshold)
281
+ 4. **Phase compatibility** (MANDATORY per Invariant #5): At least one neighbor within phase range
282
+
283
+ Configuration Parameters
284
+ ------------------------
285
+ UM_MIN_EPI : float, default 0.05
286
+ Minimum EPI magnitude required for coupling
287
+ UM_MIN_VF : float, default 0.01
288
+ Minimum structural frequency required for coupling
289
+ UM_STRICT_PHASE_CHECK : bool, default True (changed from False per RC3)
290
+ Enable strict phase compatibility checking with existing neighbors.
291
+ **MANDATORY per AGENTS.md Invariant #5**: "no coupling is valid without
292
+ explicit phase verification (synchrony)"
293
+ UM_MAX_PHASE_DIFF : float, default π/2
294
+ Maximum phase difference for compatible coupling (radians)
295
+
296
+ Parameters
297
+ ----------
298
+ G : TNFRGraph
299
+ Graph containing the node
300
+ node : NodeId
301
+ Node to validate
302
+
303
+ Raises
304
+ ------
305
+ OperatorPreconditionError
306
+ If node state is unsuitable for coupling:
307
+ - Graph has no other nodes
308
+ - EPI below threshold
309
+ - Structural frequency below threshold
310
+ - No phase-compatible neighbors (when strict checking enabled)
311
+
312
+ Notes
313
+ -----
314
+ **IMPORTANT**: Phase compatibility check is now MANDATORY by default
315
+ (UM_STRICT_PHASE_CHECK=True) to align with AGENTS.md Invariant #5 and RC3.
316
+
317
+ Set UM_STRICT_PHASE_CHECK=False to disable (NOT RECOMMENDED - violates
318
+ canonical physics requirements).
319
+
320
+ Examples
321
+ --------
322
+ >>> from tnfr.structural import create_nfr
323
+ >>> from tnfr.operators.preconditions import validate_coupling
324
+ >>>
325
+ >>> # Valid node for coupling
326
+ >>> G, node = create_nfr("active", epi=0.15, vf=0.50)
327
+ >>> validate_coupling(G, node) # Passes
328
+ >>>
329
+ >>> # Invalid: EPI too low
330
+ >>> G, node = create_nfr("inactive", epi=0.02, vf=0.50)
331
+ >>> validate_coupling(G, node) # Raises OperatorPreconditionError
332
+
333
+ See Also
334
+ --------
335
+ Coupling : UM operator that uses this validation
336
+ AGENTS.md : Invariant #5 (phase check mandatory)
337
+ EMERGENT_GRAMMAR_ANALYSIS.md : RC3 derivation
338
+ """
339
+ import math
340
+
341
+ # Basic graph check - at least one other node required
342
+ if G.number_of_nodes() <= 1:
343
+ raise OperatorPreconditionError(
344
+ "Coupling", "Graph has no other nodes to couple with"
345
+ )
346
+
347
+ # Node must be active (non-zero EPI)
348
+ epi = _get_node_attr(G, node, ALIAS_EPI)
349
+ min_epi = float(G.graph.get("UM_MIN_EPI", 0.05))
350
+ if abs(epi) < min_epi:
351
+ raise OperatorPreconditionError(
352
+ "Coupling",
353
+ f"Node EPI too low for coupling (|EPI|={abs(epi):.3f} < {min_epi:.3f})",
354
+ )
355
+
356
+ # Node must have structural frequency capacity
357
+ vf = _get_node_attr(G, node, ALIAS_VF)
358
+ min_vf = float(G.graph.get("UM_MIN_VF", 0.01))
359
+ if vf < min_vf:
360
+ raise OperatorPreconditionError(
361
+ "Coupling", f"Structural frequency too low (νf={vf:.3f} < {min_vf:.3f})"
362
+ )
363
+
364
+ # RC3: Phase compatibility check
365
+ # Per AGENTS.md Invariant #5: "no coupling is valid without explicit phase verification"
366
+ # Changed from False to True to align with canonical physics requirements
367
+ strict_phase = bool(G.graph.get("UM_STRICT_PHASE_CHECK", True))
368
+ if strict_phase:
369
+ neighbors = list(G.neighbors(node))
370
+ if neighbors:
371
+ from ...utils.numeric import angle_diff
372
+
373
+ theta_i = _get_node_attr(G, node, ALIAS_THETA)
374
+ max_phase_diff = float(G.graph.get("UM_MAX_PHASE_DIFF", math.pi / 2))
375
+
376
+ # Check if at least one neighbor is phase-compatible
377
+ has_compatible = False
378
+ for neighbor in neighbors:
379
+ theta_j = _get_node_attr(G, neighbor, ALIAS_THETA)
380
+ phase_diff = abs(angle_diff(theta_i, theta_j))
381
+ if phase_diff <= max_phase_diff:
382
+ has_compatible = True
383
+ break
384
+
385
+ if not has_compatible:
386
+ raise OperatorPreconditionError(
387
+ "Coupling",
388
+ f"No phase-compatible neighbors (all |Δθ| > {max_phase_diff:.3f})",
389
+ )
390
+
391
+
392
+ def validate_resonance(G: "TNFRGraph", node: "NodeId") -> None:
393
+ """RA - Resonance requires comprehensive canonical preconditions.
394
+
395
+ This function delegates to the strict validation implementation in
396
+ resonance.py module, which provides canonical precondition checks:
397
+
398
+ 1. Coherent source EPI (minimum structural form)
399
+ 2. Network connectivity (edges for propagation)
400
+ 3. Phase compatibility with neighbors (synchronization)
401
+ 4. Controlled dissonance (stable resonance state)
402
+ 5. Sufficient νf (propagation capacity)
403
+
404
+ Parameters
405
+ ----------
406
+ G : TNFRGraph
407
+ Graph containing the node
408
+ node : NodeId
409
+ Node to validate
410
+
411
+ Raises
412
+ ------
413
+ ValueError
414
+ If critical preconditions are not met (EPI, connectivity, νf, ΔNFR)
415
+
416
+ Warnings
417
+ --------
418
+ UserWarning
419
+ For suboptimal conditions (phase misalignment, isolated node)
420
+
421
+ Notes
422
+ -----
423
+ For backward compatibility, this function maintains the same signature
424
+ as the legacy validate_resonance but now provides enhanced validation.
425
+
426
+ Typical canonical sequences that satisfy RA preconditions:
427
+ - UM → RA: Coupling followed by propagation
428
+ - AL → RA: Emission followed by propagation
429
+ - IL → RA: Coherence stabilized then propagated
430
+
431
+ See Also
432
+ --------
433
+ tnfr.operators.preconditions.resonance.validate_resonance_strict : Full implementation
434
+ """
435
+ from .resonance import validate_resonance_strict
436
+
437
+ validate_resonance_strict(G, node)
438
+
439
+
440
+ def diagnose_resonance_readiness(G: "TNFRGraph", node: "NodeId") -> dict:
441
+ """Diagnose node readiness for RA (Resonance) operator.
442
+
443
+ Provides comprehensive diagnostic report with readiness status and
444
+ actionable recommendations for RA operator application.
445
+
446
+ Parameters
447
+ ----------
448
+ G : TNFRGraph
449
+ Graph containing the node
450
+ node : NodeId
451
+ Node to diagnose
452
+
453
+ Returns
454
+ -------
455
+ dict
456
+ Diagnostic report with readiness status, check results, values, and recommendations
457
+
458
+ See Also
459
+ --------
460
+ tnfr.operators.preconditions.resonance.diagnose_resonance_readiness : Full implementation
461
+ """
462
+ from .resonance import diagnose_resonance_readiness as _diagnose
463
+
464
+ return _diagnose(G, node)
465
+
466
+
467
+ def validate_silence(G: "TNFRGraph", node: "NodeId") -> None:
468
+ """SHA - Silence requires vf > 0 to reduce.
469
+
470
+ Parameters
471
+ ----------
472
+ G : TNFRGraph
473
+ Graph containing the node
474
+ node : NodeId
475
+ Node to validate
476
+
477
+ Raises
478
+ ------
479
+ OperatorPreconditionError
480
+ If structural frequency already near zero
481
+ """
482
+ vf = _get_node_attr(G, node, ALIAS_VF)
483
+ min_vf = float(G.graph.get("SHA_MIN_VF", 0.01))
484
+ if vf < min_vf:
485
+ raise OperatorPreconditionError(
486
+ "Silence",
487
+ f"Structural frequency already minimal (νf={vf:.3f} < {min_vf:.3f})",
488
+ )
489
+
490
+
491
+ def validate_expansion(G: "TNFRGraph", node: "NodeId") -> None:
492
+ """VAL - Expansion requires comprehensive canonical preconditions.
493
+
494
+ Canonical Requirements (TNFR Physics):
495
+ 1. **νf < max_vf**: Structural frequency below saturation
496
+ 2. **ΔNFR > 0**: Positive reorganization gradient (growth pressure)
497
+ 3. **EPI >= min_epi**: Sufficient base coherence for expansion
498
+ 4. **(Optional) Network capacity**: Check if network can support expansion
499
+
500
+ Physical Basis:
501
+ ----------------
502
+ From nodal equation: ∂EPI/∂t = νf · ΔNFR(t)
503
+
504
+ For coherent expansion:
505
+ - ΔNFR > 0 required: expansion needs outward pressure
506
+ - EPI > threshold: must have coherent base to expand from
507
+ - νf < max: must have capacity for increased reorganization
508
+
509
+ Parameters
510
+ ----------
511
+ G : TNFRGraph
512
+ Graph containing the node
513
+ node : NodeId
514
+ Node to validate
515
+
516
+ Raises
517
+ ------
518
+ OperatorPreconditionError
519
+ If any precondition fails:
520
+ - Structural frequency at maximum
521
+ - ΔNFR non-positive (no growth pressure)
522
+ - EPI below minimum (insufficient coherence base)
523
+ - (Optional) Network at capacity
524
+
525
+ Configuration Parameters
526
+ ------------------------
527
+ VAL_MAX_VF : float, default 10.0
528
+ Maximum structural frequency threshold
529
+ VAL_MIN_DNFR : float, default 1e-6
530
+ Minimum ΔNFR for expansion (must be positive, very low to minimize breaking changes)
531
+ VAL_MIN_EPI : float, default 0.2
532
+ Minimum EPI for coherent expansion
533
+ VAL_CHECK_NETWORK_CAPACITY : bool, default False
534
+ Enable network capacity validation
535
+ VAL_MAX_NETWORK_SIZE : int, default 1000
536
+ Maximum network size if capacity checking enabled
537
+
538
+ Examples
539
+ --------
540
+ >>> from tnfr.structural import create_nfr
541
+ >>> from tnfr.operators.preconditions import validate_expansion
542
+ >>>
543
+ >>> # Valid node for expansion
544
+ >>> G, node = create_nfr("expanding", epi=0.5, vf=2.0)
545
+ >>> G.nodes[node]['delta_nfr'] = 0.1 # Positive ΔNFR
546
+ >>> validate_expansion(G, node) # Passes
547
+ >>>
548
+ >>> # Invalid: negative ΔNFR
549
+ >>> G.nodes[node]['delta_nfr'] = -0.1
550
+ >>> validate_expansion(G, node) # Raises OperatorPreconditionError
551
+
552
+ Notes
553
+ -----
554
+ VAL increases both EPI magnitude and νf, enabling exploration of new
555
+ structural configurations while maintaining core identity (fractality).
556
+
557
+ See Also
558
+ --------
559
+ Expansion : VAL operator implementation
560
+ validate_contraction : NUL preconditions (inverse operation)
561
+ """
562
+ # 1. νf below maximum (existing check)
563
+ vf = _get_node_attr(G, node, ALIAS_VF)
564
+ max_vf = float(G.graph.get("VAL_MAX_VF", 10.0))
565
+ if vf >= max_vf:
566
+ raise OperatorPreconditionError(
567
+ "Expansion",
568
+ f"Structural frequency at maximum (νf={vf:.3f} >= {max_vf:.3f}). "
569
+ f"Node at reorganization capacity limit.",
570
+ )
571
+
572
+ # 2. ΔNFR positivity check (NEW - CRITICAL)
573
+ dnfr = _get_node_attr(G, node, ALIAS_DNFR)
574
+ min_dnfr = float(G.graph.get("VAL_MIN_DNFR", 1e-6))
575
+ if dnfr < min_dnfr:
576
+ raise OperatorPreconditionError(
577
+ "Expansion",
578
+ f"ΔNFR must be positive for expansion (ΔNFR={dnfr:.3f} < {min_dnfr:.3f}). "
579
+ f"No outward growth pressure detected. Consider OZ (Dissonance) to generate ΔNFR.",
580
+ )
581
+
582
+ # 3. EPI minimum check (NEW - IMPORTANT)
583
+ epi = _get_node_attr(G, node, ALIAS_EPI)
584
+ min_epi = float(G.graph.get("VAL_MIN_EPI", 0.2))
585
+ if epi < min_epi:
586
+ raise OperatorPreconditionError(
587
+ "Expansion",
588
+ f"EPI too low for coherent expansion (EPI={epi:.3f} < {min_epi:.3f}). "
589
+ f"Insufficient structural base. Consider AL (Emission) to activate node first.",
590
+ )
591
+
592
+ # 4. Network capacity check (OPTIONAL - for large-scale systems)
593
+ check_capacity = bool(G.graph.get("VAL_CHECK_NETWORK_CAPACITY", False))
594
+ if check_capacity:
595
+ max_network_size = int(G.graph.get("VAL_MAX_NETWORK_SIZE", 1000))
596
+ current_size = G.number_of_nodes()
597
+ if current_size >= max_network_size:
598
+ raise OperatorPreconditionError(
599
+ "Expansion",
600
+ f"Network at capacity (n={current_size} >= {max_network_size}). "
601
+ f"Cannot support further expansion. Set VAL_CHECK_NETWORK_CAPACITY=False to disable.",
602
+ )
603
+
604
+
605
+ def validate_contraction(G: "TNFRGraph", node: "NodeId") -> None:
606
+ """NUL - Enhanced precondition validation with over-compression check.
607
+
608
+ Canonical Requirements (TNFR Physics):
609
+ 1. **νf > min_vf**: Structural frequency above minimum for reorganization
610
+ 2. **EPI >= min_epi**: Sufficient structural form to contract safely
611
+ 3. **density <= max_density**: Not already at critical compression
612
+
613
+ Physical Basis:
614
+ ----------------
615
+ From nodal equation: ∂EPI/∂t = νf · ΔNFR(t)
616
+
617
+ For safe contraction:
618
+ - EPI must have sufficient magnitude (can't compress vacuum)
619
+ - Density ρ = |ΔNFR| / EPI must not exceed critical threshold
620
+ - Over-compression (ρ → ∞) causes structural collapse
621
+
622
+ Density is the structural pressure per unit form. When EPI contracts
623
+ while ΔNFR increases (canonical densification), density rises. If already
624
+ at critical density, further contraction risks fragmentation.
625
+
626
+ Parameters
627
+ ----------
628
+ G : TNFRGraph
629
+ Graph containing the node
630
+ node : NodeId
631
+ Node to validate
632
+
633
+ Raises
634
+ ------
635
+ OperatorPreconditionError
636
+ If any precondition fails:
637
+ - Structural frequency at minimum
638
+ - EPI too low for safe contraction
639
+ - Node already at critical density
640
+
641
+ Configuration Parameters
642
+ ------------------------
643
+ NUL_MIN_VF : float, default 0.1
644
+ Minimum structural frequency threshold
645
+ NUL_MIN_EPI : float, default 0.1
646
+ Minimum EPI for safe contraction
647
+ NUL_MAX_DENSITY : float, default 10.0
648
+ Maximum density threshold (ρ = |ΔNFR| / max(EPI, ε))
649
+
650
+ Examples
651
+ --------
652
+ >>> from tnfr.structural import create_nfr
653
+ >>> from tnfr.operators.preconditions import validate_contraction
654
+ >>>
655
+ >>> # Valid node for contraction
656
+ >>> G, node = create_nfr("contracting", epi=0.5, vf=1.0)
657
+ >>> G.nodes[node]['delta_nfr'] = 0.2
658
+ >>> validate_contraction(G, node) # Passes
659
+ >>>
660
+ >>> # Invalid: EPI too low
661
+ >>> G, node = create_nfr("too_small", epi=0.05, vf=1.0)
662
+ >>> validate_contraction(G, node) # Raises OperatorPreconditionError
663
+ >>>
664
+ >>> # Invalid: density too high
665
+ >>> G, node = create_nfr("over_compressed", epi=0.1, vf=1.0)
666
+ >>> G.nodes[node]['delta_nfr'] = 2.0 # High ΔNFR
667
+ >>> validate_contraction(G, node) # Raises OperatorPreconditionError
668
+
669
+ See Also
670
+ --------
671
+ Contraction : NUL operator implementation
672
+ validate_expansion : VAL preconditions (inverse operation)
673
+ """
674
+ vf = _get_node_attr(G, node, ALIAS_VF)
675
+ epi = _get_node_attr(G, node, ALIAS_EPI)
676
+ dnfr = _get_node_attr(G, node, ALIAS_DNFR)
677
+
678
+ # Check 1: νf must be above minimum
679
+ min_vf = float(G.graph.get("NUL_MIN_VF", 0.1))
680
+ if vf <= min_vf:
681
+ raise OperatorPreconditionError(
682
+ "Contraction",
683
+ f"Structural frequency at minimum (νf={vf:.3f} <= {min_vf:.3f})",
684
+ )
685
+
686
+ # Check 2: EPI must be above minimum for contraction
687
+ min_epi = float(G.graph.get("NUL_MIN_EPI", 0.1))
688
+ if epi < min_epi:
689
+ raise OperatorPreconditionError(
690
+ "Contraction",
691
+ f"EPI too low for safe contraction (EPI={epi:.3f} < {min_epi:.3f}). "
692
+ f"Cannot compress structure below minimum coherent form.",
693
+ )
694
+
695
+ # Check 3: Density must not exceed critical threshold
696
+ # Density ρ = |ΔNFR| / max(EPI, ε) - structural pressure per unit form
697
+ epsilon = 1e-9
698
+ density = abs(dnfr) / max(epi, epsilon)
699
+ max_density = float(G.graph.get("NUL_MAX_DENSITY", 10.0))
700
+ if density > max_density:
701
+ raise OperatorPreconditionError(
702
+ "Contraction",
703
+ f"Node already at critical density (ρ={density:.3f} > {max_density:.3f}). "
704
+ f"Further contraction risks structural collapse. "
705
+ f"Consider IL (Coherence) to stabilize or reduce ΔNFR first.",
706
+ )
707
+
708
+
709
+ def validate_self_organization(G: "TNFRGraph", node: "NodeId") -> None:
710
+ """THOL - Enhanced validation: connectivity, metabolic context, acceleration.
711
+
712
+ Self-organization requires:
713
+ 1. Sufficient EPI for bifurcation
714
+ 2. Positive reorganization pressure (ΔNFR > 0)
715
+ 3. Structural reorganization capacity (νf > 0)
716
+ 4. Network connectivity for metabolism (degree ≥ 1)
717
+ 5. EPI history for acceleration computation (≥3 points)
718
+ 6. **NEW**: Bifurcation threshold check (∂²EPI/∂t² vs τ) with telemetry
719
+
720
+ Also detects and records the destabilizer type that enabled this self-organization
721
+ for telemetry and structural tracing purposes.
722
+
723
+ **Bifurcation Threshold Validation (∂²EPI/∂t² > τ):**
724
+
725
+ According to TNFR.pdf §2.2.10, THOL bifurcation occurs only when structural
726
+ acceleration exceeds threshold τ. This function now explicitly validates this
727
+ condition and sets telemetry flags:
728
+
729
+ - If ∂²EPI/∂t² > τ: Bifurcation will occur (normal THOL behavior)
730
+ - If ∂²EPI/∂t² ≤ τ: THOL executes but no sub-EPIs generated (warning logged)
731
+
732
+ The validation is NON-BLOCKING (warning only) because THOL can meaningfully
733
+ execute without bifurcation - it still applies coherence and metabolic effects.
734
+
735
+ Parameters
736
+ ----------
737
+ G : TNFRGraph
738
+ Graph containing the node
739
+ node : NodeId
740
+ Node to validate
741
+
742
+ Raises
743
+ ------
744
+ OperatorPreconditionError
745
+ If any structural requirement is not met
746
+
747
+ Notes
748
+ -----
749
+ This function implements R4 Extended telemetry by analyzing the glyph_history
750
+ to determine which destabilizer (strong/moderate/weak) enabled the self-organization.
751
+
752
+ Configuration Parameters
753
+ ------------------------
754
+ THOL_MIN_EPI : float, default 0.2
755
+ Minimum EPI for bifurcation
756
+ THOL_MIN_VF : float, default 0.1
757
+ Minimum structural frequency for reorganization
758
+ THOL_MIN_DEGREE : int, default 1
759
+ Minimum network connectivity
760
+ THOL_MIN_HISTORY_LENGTH : int, default 3
761
+ Minimum EPI history for acceleration computation
762
+ THOL_ALLOW_ISOLATED : bool, default False
763
+ Allow isolated nodes for internal-only bifurcation
764
+ THOL_METABOLIC_ENABLED : bool, default True
765
+ Require metabolic network context
766
+ BIFURCATION_THRESHOLD_TAU : float, default 0.1
767
+ Bifurcation threshold for ∂²EPI/∂t² (see THOL_BIFURCATION_THRESHOLD)
768
+ THOL_BIFURCATION_THRESHOLD : float, default 0.1
769
+ Alias for BIFURCATION_THRESHOLD_TAU (operator-specific config)
770
+ """
771
+ import logging
772
+
773
+ logger = logging.getLogger(__name__)
774
+
775
+ epi = _get_node_attr(G, node, ALIAS_EPI)
776
+ dnfr = _get_node_attr(G, node, ALIAS_DNFR)
777
+ vf = _get_node_attr(G, node, ALIAS_VF)
778
+
779
+ # 1. EPI sufficiency
780
+ min_epi = float(G.graph.get("THOL_MIN_EPI", 0.2))
781
+ if epi < min_epi:
782
+ raise OperatorPreconditionError(
783
+ "Self-organization",
784
+ f"EPI too low for bifurcation (EPI={epi:.3f} < {min_epi:.3f})",
785
+ )
786
+
787
+ # 2. Reorganization pressure
788
+ if dnfr <= 0:
789
+ raise OperatorPreconditionError(
790
+ "Self-organization",
791
+ f"ΔNFR non-positive, no reorganization pressure (ΔNFR={dnfr:.3f})",
792
+ )
793
+
794
+ # 3. Structural frequency validation
795
+ min_vf = float(G.graph.get("THOL_MIN_VF", 0.1))
796
+ if vf < min_vf:
797
+ raise OperatorPreconditionError(
798
+ "Self-organization",
799
+ f"Structural frequency too low for reorganization (νf={vf:.3f} < {min_vf:.3f})",
800
+ )
801
+
802
+ # 4. Connectivity requirement (ELEVATED FROM WARNING)
803
+ min_degree = int(G.graph.get("THOL_MIN_DEGREE", 1))
804
+ node_degree = G.degree(node)
805
+
806
+ # Allow isolated THOL if explicitly enabled
807
+ allow_isolated = bool(G.graph.get("THOL_ALLOW_ISOLATED", False))
808
+
809
+ if node_degree < min_degree and not allow_isolated:
810
+ raise OperatorPreconditionError(
811
+ "Self-organization",
812
+ f"Node insufficiently connected for network metabolism "
813
+ f"(degree={node_degree} < {min_degree}). "
814
+ f"Set THOL_ALLOW_ISOLATED=True to enable internal-only bifurcation.",
815
+ )
816
+
817
+ # 5. EPI history validation (for d²EPI/dt² computation)
818
+ epi_history = G.nodes[node].get("epi_history", [])
819
+ min_history_length = int(G.graph.get("THOL_MIN_HISTORY_LENGTH", 3))
820
+
821
+ if len(epi_history) < min_history_length:
822
+ raise OperatorPreconditionError(
823
+ "Self-organization",
824
+ f"Insufficient EPI history for acceleration computation "
825
+ f"(have {len(epi_history)}, need ≥{min_history_length}). "
826
+ f"Apply operators to build history before THOL.",
827
+ )
828
+
829
+ # 6. Metabolic context validation (if metabolism enabled)
830
+ if G.graph.get("THOL_METABOLIC_ENABLED", True):
831
+ # If network metabolism is expected, verify neighbors exist
832
+ if node_degree == 0:
833
+ raise OperatorPreconditionError(
834
+ "Self-organization",
835
+ "Metabolic mode enabled but node is isolated. "
836
+ "Disable THOL_METABOLIC_ENABLED or add network connections.",
837
+ )
838
+
839
+ # R4 Extended: Detect and record destabilizer type for telemetry
840
+ _record_destabilizer_context(G, node, logger)
841
+
842
+ # NEW: Bifurcation threshold validation (∂²EPI/∂t² > τ)
843
+ # This is NON-BLOCKING - THOL can execute without bifurcation
844
+ # Note: SelfOrganization uses its own _compute_epi_acceleration which looks at 'epi_history'
845
+ # while compute_d2epi_dt2 looks at '_epi_history'. We check both for compatibility.
846
+
847
+ # Get EPI history from node (try both keys for compatibility)
848
+ history = G.nodes[node].get("_epi_history") or G.nodes[node].get("epi_history", [])
849
+
850
+ # Compute d²EPI/dt² directly from history (same logic as both functions)
851
+ if len(history) >= 3:
852
+ epi_t = float(history[-1])
853
+ epi_t1 = float(history[-2])
854
+ epi_t2 = float(history[-3])
855
+ d2_epi_signed = epi_t - 2.0 * epi_t1 + epi_t2
856
+ d2_epi = abs(d2_epi_signed)
857
+ else:
858
+ # Insufficient history - should have been caught earlier, but handle gracefully
859
+ d2_epi = 0.0
860
+
861
+ # Get bifurcation threshold from graph configuration
862
+ # Try BIFURCATION_THRESHOLD_TAU first (canonical), then THOL_BIFURCATION_THRESHOLD
863
+ tau = G.graph.get("BIFURCATION_THRESHOLD_TAU")
864
+ if tau is None:
865
+ tau = float(G.graph.get("THOL_BIFURCATION_THRESHOLD", 0.1))
866
+ else:
867
+ tau = float(tau)
868
+
869
+ # Check if bifurcation threshold will be exceeded
870
+ if d2_epi <= tau:
871
+ # Log warning but allow execution - THOL can be meaningful without bifurcation
872
+ logger.warning(
873
+ f"Node {node}: THOL applied with ∂²EPI/∂t²={d2_epi:.3f} ≤ τ={tau:.3f}. "
874
+ f"No bifurcation will occur (empty THOL window expected). "
875
+ f"Sub-EPIs will not be generated. "
876
+ f"Consider stronger destabilizer (OZ, VAL) to increase acceleration."
877
+ )
878
+ # Set telemetry flag for post-hoc analysis
879
+ G.nodes[node]["_thol_no_bifurcation_expected"] = True
880
+ else:
881
+ # Clear flag if previously set
882
+ G.nodes[node]["_thol_no_bifurcation_expected"] = False
883
+ logger.debug(
884
+ f"Node {node}: THOL bifurcation threshold exceeded "
885
+ f"(∂²EPI/∂t²={d2_epi:.3f} > τ={tau:.3f}). "
886
+ f"Sub-EPI generation expected."
887
+ )
888
+
889
+
890
+ # Moved to mutation.py module for modularity
891
+ # Import here for backward compatibility
892
+ try:
893
+ from .mutation import record_destabilizer_context as _record_destabilizer_context
894
+ except ImportError:
895
+ # Fallback if mutation.py not available (shouldn't happen)
896
+ def _record_destabilizer_context(
897
+ G: "TNFRGraph", node: "NodeId", logger: "logging.Logger"
898
+ ) -> None:
899
+ """Fallback implementation - see mutation.py for canonical version."""
900
+ G.nodes[node]["_mutation_context"] = {
901
+ "destabilizer_type": None,
902
+ "destabilizer_operator": None,
903
+ "destabilizer_distance": None,
904
+ "recent_history": [],
905
+ }
906
+
907
+
908
+ def validate_mutation(G: "TNFRGraph", node: "NodeId") -> None:
909
+ """ZHIR - Mutation requires node to be in valid structural state.
910
+
911
+ Implements canonical TNFR requirements for mutation (AGENTS.md §11, TNFR.pdf §2.2.11):
912
+
913
+ 1. Minimum νf for phase transformation capacity
914
+ 2. **∂EPI/∂t > ξ: Structural change velocity exceeds threshold**
915
+ 3. **U4b Part 1: Prior IL (Coherence) for stable transformation base**
916
+ 4. **U4b Part 2: Recent destabilizer (~3 ops) for threshold energy**
917
+
918
+ Also detects and records the destabilizer type that enabled this mutation
919
+ for telemetry and structural tracing purposes.
920
+
921
+ Parameters
922
+ ----------
923
+ G : TNFRGraph
924
+ Graph containing the node
925
+ node : NodeId
926
+ Node to validate
927
+
928
+ Raises
929
+ ------
930
+ OperatorPreconditionError
931
+ If node state is unsuitable for mutation or U4b requirements not met
932
+
933
+ Configuration Parameters
934
+ ------------------------
935
+ ZHIR_MIN_VF : float, default 0.05
936
+ Minimum structural frequency for phase transformation
937
+ ZHIR_THRESHOLD_XI : float, default 0.1
938
+ Threshold for ∂EPI/∂t velocity check
939
+ VALIDATE_OPERATOR_PRECONDITIONS : bool, default False
940
+ Enable strict U4b validation (IL precedence + destabilizer requirement)
941
+ ZHIR_REQUIRE_IL_PRECEDENCE : bool, default False
942
+ Require prior IL even if VALIDATE_OPERATOR_PRECONDITIONS=False
943
+ ZHIR_REQUIRE_DESTABILIZER : bool, default False
944
+ Require recent destabilizer even if VALIDATE_OPERATOR_PRECONDITIONS=False
945
+
946
+ Notes
947
+ -----
948
+ **Canonical threshold verification (∂EPI/∂t > ξ)**:
949
+
950
+ ZHIR is a phase transformation that requires sufficient structural reorganization
951
+ velocity to justify the transition. The threshold ξ represents the minimum rate
952
+ of structural change needed for a phase shift to be physically meaningful.
953
+
954
+ - If ∂EPI/∂t < ξ: Logs warning (soft check for backward compatibility)
955
+ - If ∂EPI/∂t ≥ ξ: Logs success, sets validation flag
956
+ - If insufficient history: Logs warning, cannot verify
957
+
958
+ **U4b Validation (Grammar Rule)**:
959
+
960
+ When strict validation enabled (VALIDATE_OPERATOR_PRECONDITIONS=True):
961
+ - **Part 1**: Prior IL (Coherence) required for stable base
962
+ - **Part 2**: Recent destabilizer (OZ/VAL/etc) required within ~3 ops
963
+
964
+ Without strict validation: Only telemetry/warnings logged.
965
+
966
+ This function implements R4 Extended telemetry by analyzing the glyph_history
967
+ to determine which destabilizer (strong/moderate/weak) enabled the mutation.
968
+ The destabilizer context is stored in node metadata for structural tracing.
969
+ """
970
+ import logging
971
+
972
+ logger = logging.getLogger(__name__)
973
+
974
+ # Mutation is a phase change, require minimum vf for meaningful transition
975
+ vf = _get_node_attr(G, node, ALIAS_VF)
976
+ min_vf = float(G.graph.get("ZHIR_MIN_VF", 0.05))
977
+ if vf < min_vf:
978
+ raise OperatorPreconditionError(
979
+ "Mutation",
980
+ f"Structural frequency too low for mutation (νf={vf:.3f} < {min_vf:.3f})",
981
+ )
982
+
983
+ # NEW: Threshold crossing validation (∂EPI/∂t > ξ)
984
+ # Get EPI history - check both keys for compatibility
985
+ epi_history = G.nodes[node].get("epi_history") or G.nodes[node].get("_epi_history", [])
986
+
987
+ if len(epi_history) >= 2:
988
+ # Compute ∂EPI/∂t (discrete approximation using last two points)
989
+ # For discrete operator applications with Δt=1: ∂EPI/∂t ≈ EPI_t - EPI_{t-1}
990
+ depi_dt = abs(epi_history[-1] - epi_history[-2])
991
+
992
+ # Get threshold from configuration
993
+ xi_threshold = float(G.graph.get("ZHIR_THRESHOLD_XI", 0.1))
994
+
995
+ # Verify threshold crossed
996
+ if depi_dt < xi_threshold:
997
+ # Allow mutation but log warning (soft check for backward compatibility)
998
+ logger.warning(
999
+ f"Node {node}: ZHIR applied with ∂EPI/∂t={depi_dt:.3f} < ξ={xi_threshold}. "
1000
+ f"Mutation may lack structural justification. "
1001
+ f"Consider increasing dissonance (OZ) first."
1002
+ )
1003
+ G.nodes[node]["_zhir_threshold_warning"] = True
1004
+ else:
1005
+ # Threshold met - log success
1006
+ logger.info(
1007
+ f"Node {node}: ZHIR threshold crossed (∂EPI/∂t={depi_dt:.3f} > ξ={xi_threshold})"
1008
+ )
1009
+ G.nodes[node]["_zhir_threshold_met"] = True
1010
+ else:
1011
+ # Insufficient history - cannot verify threshold
1012
+ logger.warning(
1013
+ f"Node {node}: ZHIR applied without sufficient EPI history "
1014
+ f"(need ≥2 points, have {len(epi_history)}). Cannot verify threshold."
1015
+ )
1016
+ G.nodes[node]["_zhir_threshold_unknown"] = True
1017
+
1018
+ # U4b Part 1: IL Precedence Check (stable base for transformation)
1019
+ # Check if strict validation enabled
1020
+ strict_validation = bool(G.graph.get("VALIDATE_OPERATOR_PRECONDITIONS", False))
1021
+ require_il = strict_validation or bool(G.graph.get("ZHIR_REQUIRE_IL_PRECEDENCE", False))
1022
+
1023
+ if require_il:
1024
+ # Get glyph history
1025
+ glyph_history = G.nodes[node].get("glyph_history", [])
1026
+
1027
+ # Import glyph_function_name to convert glyphs to operator names
1028
+ from ..grammar import glyph_function_name
1029
+
1030
+ # Convert history to operator names
1031
+ history_names = [glyph_function_name(g) for g in glyph_history]
1032
+
1033
+ # Check for prior IL (coherence)
1034
+ il_found = "coherence" in history_names
1035
+
1036
+ if not il_found:
1037
+ raise OperatorPreconditionError(
1038
+ "Mutation",
1039
+ "U4b violation: ZHIR requires prior IL (Coherence) for stable transformation base. "
1040
+ "Apply Coherence before mutation sequence. "
1041
+ f"Recent history: {history_names[-5:] if len(history_names) > 5 else history_names}"
1042
+ )
1043
+
1044
+ logger.debug(f"Node {node}: ZHIR IL precedence satisfied (prior Coherence found)")
1045
+
1046
+ # U4b Part 2: Recent Destabilizer Check (threshold energy for bifurcation)
1047
+ # R4 Extended: Detect and record destabilizer type for telemetry
1048
+ _record_destabilizer_context(G, node, logger)
1049
+
1050
+ # If strict validation enabled, enforce destabilizer requirement
1051
+ require_destabilizer = strict_validation or bool(G.graph.get("ZHIR_REQUIRE_DESTABILIZER", False))
1052
+
1053
+ if require_destabilizer:
1054
+ context = G.nodes[node].get("_mutation_context", {})
1055
+ destabilizer_found = context.get("destabilizer_operator")
1056
+
1057
+ if destabilizer_found is None:
1058
+ recent_history = context.get("recent_history", [])
1059
+ raise OperatorPreconditionError(
1060
+ "Mutation",
1061
+ "U4b violation: ZHIR requires recent destabilizer (OZ/VAL/etc) within ~3 ops. "
1062
+ f"Recent history: {recent_history}. "
1063
+ "Apply Dissonance or Expansion to elevate ΔNFR first."
1064
+ )
1065
+
1066
+
1067
+ def validate_transition(G: "TNFRGraph", node: "NodeId") -> None:
1068
+ """NAV - Comprehensive canonical preconditions for transition.
1069
+
1070
+ Validates comprehensive preconditions for NAV (Transition) operator according
1071
+ to TNFR.pdf §2.3.11:
1072
+
1073
+ 1. **Minimum νf**: Structural frequency must exceed threshold
1074
+ 2. **Controlled ΔNFR**: |ΔNFR| must be below maximum for stable transition
1075
+ 3. **Regime validation**: Warns if transitioning from deep latency (EPI < 0.05)
1076
+ 4. **Sequence compatibility** (optional): Warns if NAV applied after incompatible operators
1077
+
1078
+ Configuration Parameters
1079
+ ------------------------
1080
+ NAV_MIN_VF : float, default 0.01
1081
+ Minimum structural frequency for transition
1082
+ NAV_MAX_DNFR : float, default 1.0
1083
+ Maximum |ΔNFR| for stable transition
1084
+ NAV_STRICT_SEQUENCE_CHECK : bool, default False
1085
+ Enable strict sequence compatibility checking
1086
+ NAV_MIN_EPI_FROM_LATENCY : float, default 0.05
1087
+ Minimum EPI for smooth transition from latency
1088
+
1089
+ Parameters
1090
+ ----------
1091
+ G : TNFRGraph
1092
+ Graph containing the node
1093
+ node : NodeId
1094
+ Node to validate
1095
+
1096
+ Raises
1097
+ ------
1098
+ OperatorPreconditionError
1099
+ If node lacks necessary dynamics for transition:
1100
+ - νf below minimum threshold
1101
+ - |ΔNFR| exceeds maximum threshold (unstable state)
1102
+
1103
+ Warnings
1104
+ --------
1105
+ UserWarning
1106
+ For suboptimal but valid conditions:
1107
+ - Transitioning from deep latency (EPI < 0.05)
1108
+ - NAV applied after incompatible operator (when strict checking enabled)
1109
+
1110
+ Notes
1111
+ -----
1112
+ NAV requires controlled ΔNFR to prevent instability during regime transitions.
1113
+ High |ΔNFR| indicates significant reorganization pressure that could cause
1114
+ structural disruption during transition. Apply IL (Coherence) first to reduce
1115
+ ΔNFR before attempting regime transition.
1116
+
1117
+ Deep latency transitions (EPI < 0.05 after SHA) benefit from prior AL (Emission)
1118
+ to provide smoother reactivation path.
1119
+
1120
+ Examples
1121
+ --------
1122
+ >>> from tnfr.structural import create_nfr
1123
+ >>> from tnfr.operators.preconditions import validate_transition
1124
+ >>>
1125
+ >>> # Valid transition - controlled state
1126
+ >>> G, node = create_nfr("test", epi=0.5, vf=0.6)
1127
+ >>> G.nodes[node]['delta_nfr'] = 0.3 # Controlled ΔNFR
1128
+ >>> validate_transition(G, node) # Passes
1129
+ >>>
1130
+ >>> # Invalid - ΔNFR too high
1131
+ >>> G.nodes[node]['delta_nfr'] = 1.5
1132
+ >>> validate_transition(G, node) # Raises OperatorPreconditionError
1133
+
1134
+ See Also
1135
+ --------
1136
+ Transition : NAV operator implementation
1137
+ validate_coherence : IL preconditions for ΔNFR reduction
1138
+ TNFR.pdf : §2.3.11 for NAV canonical requirements
1139
+ """
1140
+ import warnings
1141
+
1142
+ # 1. νf minimum (existing check - preserved for backward compatibility)
1143
+ vf = _get_node_attr(G, node, ALIAS_VF)
1144
+ min_vf = float(G.graph.get("NAV_MIN_VF", 0.01))
1145
+ if vf < min_vf:
1146
+ raise OperatorPreconditionError(
1147
+ "Transition",
1148
+ f"Structural frequency too low (νf={vf:.3f} < {min_vf:.3f})",
1149
+ )
1150
+
1151
+ # 2. ΔNFR positivity and bounds check (NEW - CRITICAL)
1152
+ dnfr = _get_node_attr(G, node, ALIAS_DNFR)
1153
+ max_dnfr = float(G.graph.get("NAV_MAX_DNFR", 1.0))
1154
+ if abs(dnfr) > max_dnfr:
1155
+ raise OperatorPreconditionError(
1156
+ "Transition",
1157
+ f"ΔNFR too high for stable transition (|ΔNFR|={abs(dnfr):.3f} > {max_dnfr}). "
1158
+ f"Apply IL (Coherence) first to reduce reorganization pressure.",
1159
+ )
1160
+
1161
+ # 3. Regime origin validation (NEW - WARNING)
1162
+ latent = G.nodes[node].get("latent", False)
1163
+ epi = _get_node_attr(G, node, ALIAS_EPI)
1164
+ min_epi_from_latency = float(G.graph.get("NAV_MIN_EPI_FROM_LATENCY", 0.05))
1165
+
1166
+ if latent and epi < min_epi_from_latency:
1167
+ # Warning: transitioning from deep latency
1168
+ warnings.warn(
1169
+ f"Node {node} in deep latency (EPI={epi:.3f} < {min_epi_from_latency:.3f}). "
1170
+ f"Consider AL (Emission) before NAV for smoother activation.",
1171
+ UserWarning,
1172
+ stacklevel=2,
1173
+ )
1174
+
1175
+ # 4. Sequence compatibility check (NEW - OPTIONAL)
1176
+ if G.graph.get("NAV_STRICT_SEQUENCE_CHECK", False):
1177
+ history = G.nodes[node].get("glyph_history", [])
1178
+ if history:
1179
+ from ..grammar import glyph_function_name
1180
+
1181
+ last_op = glyph_function_name(history[-1]) if history else None
1182
+
1183
+ # NAV works best after stabilizers or generators
1184
+ # Valid predecessors per TNFR.pdf §2.3.11 and AGENTS.md
1185
+ valid_predecessors = {
1186
+ "emission", # AL → NAV (activation-transition)
1187
+ "coherence", # IL → NAV (stable-transition)
1188
+ "silence", # SHA → NAV (latency-transition)
1189
+ "self_organization", # THOL → NAV (bifurcation-transition)
1190
+ }
1191
+
1192
+ if last_op and last_op not in valid_predecessors:
1193
+ warnings.warn(
1194
+ f"NAV applied after {last_op}. "
1195
+ f"More coherent after: {', '.join(sorted(valid_predecessors))}",
1196
+ UserWarning,
1197
+ stacklevel=2,
1198
+ )
1199
+
1200
+
1201
+ def validate_recursivity(G: "TNFRGraph", node: "NodeId") -> None:
1202
+ """REMESH - Recursivity requires global network coherence threshold.
1203
+
1204
+ Parameters
1205
+ ----------
1206
+ G : TNFRGraph
1207
+ Graph containing the node
1208
+ node : NodeId
1209
+ Node to validate
1210
+
1211
+ Raises
1212
+ ------
1213
+ OperatorPreconditionError
1214
+ If network is not ready for remesh operation
1215
+ """
1216
+ # REMESH is a network-scale operation, check graph state
1217
+ min_nodes = int(G.graph.get("REMESH_MIN_NODES", 2))
1218
+ if G.number_of_nodes() < min_nodes:
1219
+ raise OperatorPreconditionError(
1220
+ "Recursivity",
1221
+ f"Network too small for remesh (n={G.number_of_nodes()} < {min_nodes})",
1222
+ )
1223
+
1224
+
1225
+ # Import diagnostic functions from modular implementations
1226
+ from .mutation import diagnose_mutation_readiness # noqa: E402