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,580 @@
1
+ """Canonical precondition validators for ZHIR (Mutation) operator.
2
+
3
+ Implements comprehensive validation of mutation prerequisites including
4
+ threshold verification, grammar U4b compliance, and structural readiness.
5
+
6
+ This module provides strict, modular validation for the Mutation (ZHIR) operator,
7
+ aligning with the architectural pattern used by Coherence (IL) and Dissonance (OZ).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from ...types import NodeId, TNFRGraph
16
+ import logging
17
+
18
+ from ...alias import get_attr
19
+ from ...config.operator_names import (
20
+ BIFURCATION_WINDOWS,
21
+ DESTABILIZERS_MODERATE,
22
+ DESTABILIZERS_STRONG,
23
+ DESTABILIZERS_WEAK,
24
+ )
25
+ from ...constants.aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_VF
26
+ from . import OperatorPreconditionError
27
+
28
+ __all__ = [
29
+ "validate_mutation_strict",
30
+ "validate_threshold_crossing",
31
+ "validate_grammar_u4b",
32
+ "record_destabilizer_context",
33
+ "diagnose_mutation_readiness",
34
+ ]
35
+
36
+
37
+ def validate_mutation_strict(G: TNFRGraph, node: NodeId) -> None:
38
+ """Comprehensive canonical validation for ZHIR.
39
+
40
+ Validates all TNFR requirements for mutation (AGENTS.md §11, TNFR.pdf §2.2.11):
41
+
42
+ 1. **Minimum νf**: Reorganization capacity for phase transformation
43
+ 2. **Threshold crossing**: ∂EPI/∂t > ξ (structural velocity sufficient)
44
+ 3. **Grammar U4b Part 1**: Prior IL (Coherence) for stable base
45
+ 4. **Grammar U4b Part 2**: Recent destabilizer (~3 ops) for threshold energy
46
+ 5. **Sufficient history**: EPI history for velocity calculation
47
+
48
+ Parameters
49
+ ----------
50
+ G : TNFRGraph
51
+ Graph containing the node
52
+ node : NodeId
53
+ Node to validate
54
+
55
+ Raises
56
+ ------
57
+ OperatorPreconditionError
58
+ If any canonical requirement not met
59
+
60
+ Notes
61
+ -----
62
+ This function implements strict validation when:
63
+ - ``VALIDATE_OPERATOR_PRECONDITIONS=True`` (global strict mode)
64
+ - Individual flags enabled (ZHIR_REQUIRE_IL_PRECEDENCE, etc.)
65
+
66
+ For backward compatibility, threshold and U4b checks may be soft
67
+ (warnings only) when strict validation disabled.
68
+
69
+ Examples
70
+ --------
71
+ >>> from tnfr.structural import create_nfr
72
+ >>> from tnfr.operators.preconditions.mutation import validate_mutation_strict
73
+ >>> G, node = create_nfr("test", epi=0.5, vf=1.0)
74
+ >>> G.nodes[node]["epi_history"] = [0.4, 0.5]
75
+ >>> G.graph["VALIDATE_OPERATOR_PRECONDITIONS"] = True
76
+ >>> # This would raise if U4b not satisfied
77
+ >>> # validate_mutation_strict(G, node) # doctest: +SKIP
78
+ """
79
+ import logging
80
+
81
+ logger = logging.getLogger(__name__)
82
+
83
+ # 1. Minimum νf validation
84
+ _validate_minimum_vf(G, node)
85
+
86
+ # 2. Threshold crossing validation (∂EPI/∂t > ξ)
87
+ validate_threshold_crossing(G, node, logger)
88
+
89
+ # 3. Grammar U4b validation
90
+ strict_validation = bool(G.graph.get("VALIDATE_OPERATOR_PRECONDITIONS", False))
91
+ if strict_validation:
92
+ validate_grammar_u4b(G, node, logger)
93
+
94
+ # 4. History length validation
95
+ _validate_history_length(G, node)
96
+
97
+
98
+ def _validate_minimum_vf(G: TNFRGraph, node: NodeId) -> None:
99
+ """Validate minimum structural frequency for phase transformation."""
100
+ vf = float(get_attr(G.nodes[node], ALIAS_VF, 0.0))
101
+ min_vf = float(G.graph.get("ZHIR_MIN_VF", 0.05))
102
+
103
+ if vf < min_vf:
104
+ raise OperatorPreconditionError(
105
+ "Mutation",
106
+ f"Structural frequency too low for mutation (νf={vf:.3f} < {min_vf:.3f})",
107
+ )
108
+
109
+
110
+ def _validate_history_length(G: TNFRGraph, node: NodeId) -> None:
111
+ """Validate sufficient EPI history for velocity calculation."""
112
+ epi_history = G.nodes[node].get("epi_history") or G.nodes[node].get("_epi_history", [])
113
+ min_length = int(G.graph.get("ZHIR_MIN_HISTORY_LENGTH", 2))
114
+
115
+ if len(epi_history) < min_length:
116
+ import logging
117
+ logger = logging.getLogger(__name__)
118
+ logger.warning(
119
+ f"Node {node}: ZHIR applied without sufficient EPI history "
120
+ f"(need ≥{min_length} points, have {len(epi_history)}). "
121
+ f"Threshold verification may be inaccurate."
122
+ )
123
+
124
+
125
+ def validate_threshold_crossing(
126
+ G: TNFRGraph, node: NodeId, logger: logging.Logger | None = None
127
+ ) -> None:
128
+ """Validate ∂EPI/∂t > ξ requirement for phase transformation.
129
+
130
+ ZHIR is a phase transformation that requires sufficient structural reorganization
131
+ velocity to justify the transition. The threshold ξ represents the minimum rate
132
+ of structural change needed for a phase shift to be physically meaningful.
133
+
134
+ Parameters
135
+ ----------
136
+ G : TNFRGraph
137
+ Graph containing the node
138
+ node : NodeId
139
+ Node to validate
140
+ logger : logging.Logger, optional
141
+ Logger for telemetry output
142
+
143
+ Notes
144
+ -----
145
+ - If ∂EPI/∂t < ξ: Logs warning (soft check for backward compatibility)
146
+ - If ∂EPI/∂t ≥ ξ: Logs success, sets validation flag
147
+ - If insufficient history: Logs warning, cannot verify
148
+
149
+ The check is soft (warning only) unless ZHIR_STRICT_THRESHOLD_CHECK=True,
150
+ maintaining backward compatibility with existing code.
151
+
152
+ Examples
153
+ --------
154
+ >>> from tnfr.structural import create_nfr
155
+ >>> G, node = create_nfr("test", epi=0.5, vf=1.0)
156
+ >>> G.nodes[node]["epi_history"] = [0.3, 0.5] # velocity = 0.2
157
+ >>> G.graph["ZHIR_THRESHOLD_XI"] = 0.1
158
+ >>> validate_threshold_crossing(G, node) # Should pass (0.2 > 0.1)
159
+ """
160
+ if logger is None:
161
+ import logging
162
+ logger = logging.getLogger(__name__)
163
+
164
+ # Get EPI history - check both keys for compatibility
165
+ epi_history = G.nodes[node].get("epi_history") or G.nodes[node].get("_epi_history", [])
166
+
167
+ if len(epi_history) < 2:
168
+ # Insufficient history - cannot verify threshold
169
+ logger.warning(
170
+ f"Node {node}: ZHIR applied without sufficient EPI history "
171
+ f"(need ≥2 points, have {len(epi_history)}). Cannot verify threshold."
172
+ )
173
+ G.nodes[node]["_zhir_threshold_unknown"] = True
174
+ return
175
+
176
+ # Compute ∂EPI/∂t (discrete approximation using last two points)
177
+ # For discrete operator applications with Δt=1: ∂EPI/∂t ≈ EPI_t - EPI_{t-1}
178
+ depi_dt = abs(epi_history[-1] - epi_history[-2])
179
+
180
+ # Get threshold from configuration
181
+ xi_threshold = float(G.graph.get("ZHIR_THRESHOLD_XI", 0.1))
182
+
183
+ # Verify threshold crossed
184
+ if depi_dt < xi_threshold:
185
+ # Allow mutation but log warning (soft check for backward compatibility)
186
+ logger.warning(
187
+ f"Node {node}: ZHIR applied with ∂EPI/∂t={depi_dt:.3f} < ξ={xi_threshold}. "
188
+ f"Mutation may lack structural justification. "
189
+ f"Consider increasing dissonance (OZ) first."
190
+ )
191
+ G.nodes[node]["_zhir_threshold_warning"] = True
192
+
193
+ # Strict check if configured
194
+ if bool(G.graph.get("ZHIR_STRICT_THRESHOLD_CHECK", False)):
195
+ raise OperatorPreconditionError(
196
+ "Mutation",
197
+ f"Threshold not crossed: ∂EPI/∂t={depi_dt:.3f} < ξ={xi_threshold}. "
198
+ f"Apply Dissonance (OZ) or Expansion (VAL) to increase structural velocity first."
199
+ )
200
+ else:
201
+ # Threshold met - log success
202
+ logger.info(
203
+ f"Node {node}: ZHIR threshold crossed (∂EPI/∂t={depi_dt:.3f} > ξ={xi_threshold})"
204
+ )
205
+ G.nodes[node]["_zhir_threshold_met"] = True
206
+
207
+
208
+ def validate_grammar_u4b(
209
+ G: TNFRGraph, node: NodeId, logger: logging.Logger | None = None
210
+ ) -> None:
211
+ """Validate U4b: IL precedence + recent destabilizer.
212
+
213
+ Grammar rule U4b (BIFURCATION DYNAMICS - Transformers Need Context) requires:
214
+
215
+ 1. **Prior IL (Coherence)**: Stable base for transformation
216
+ 2. **Recent destabilizer**: OZ/VAL/etc within ~3 operations for threshold energy
217
+
218
+ This is a STRONG canonicity rule derived from bifurcation theory - phase
219
+ transformations need both stability (IL) and elevated energy (destabilizer).
220
+
221
+ Parameters
222
+ ----------
223
+ G : TNFRGraph
224
+ Graph containing the node
225
+ node : NodeId
226
+ Node to validate
227
+ logger : logging.Logger, optional
228
+ Logger for telemetry output
229
+
230
+ Raises
231
+ ------
232
+ OperatorPreconditionError
233
+ If U4b requirements not met when strict validation enabled
234
+
235
+ Notes
236
+ -----
237
+ Validation is strict when:
238
+ - ``VALIDATE_OPERATOR_PRECONDITIONS=True`` (global)
239
+ - ``ZHIR_REQUIRE_IL_PRECEDENCE=True`` (Part 1)
240
+ - ``ZHIR_REQUIRE_DESTABILIZER=True`` (Part 2)
241
+
242
+ Examples
243
+ --------
244
+ >>> from tnfr.structural import create_nfr
245
+ >>> from tnfr.operators import Coherence, Dissonance
246
+ >>> G, node = create_nfr("test", epi=0.5, vf=1.0)
247
+ >>> G.graph["VALIDATE_OPERATOR_PRECONDITIONS"] = True
248
+ >>> # Apply required sequence
249
+ >>> Coherence()(G, node) # IL for stable base
250
+ >>> Dissonance()(G, node) # OZ for destabilization
251
+ >>> # Now validate_grammar_u4b would pass
252
+ """
253
+ if logger is None:
254
+ import logging
255
+ logger = logging.getLogger(__name__)
256
+
257
+ # Get glyph history
258
+ glyph_history = G.nodes[node].get("glyph_history", [])
259
+ if not glyph_history:
260
+ # No history - cannot validate U4b
261
+ logger.warning(
262
+ f"Node {node}: No glyph history available. Cannot verify U4b compliance."
263
+ )
264
+ return
265
+
266
+ # Import glyph_function_name to convert glyphs to operator names
267
+ from ..grammar import glyph_function_name
268
+
269
+ # Convert history to operator names
270
+ history_names = [glyph_function_name(g) for g in glyph_history]
271
+
272
+ # Part 1: Check for prior IL (Coherence)
273
+ require_il = bool(G.graph.get("ZHIR_REQUIRE_IL_PRECEDENCE", False))
274
+ il_found = "coherence" in history_names
275
+
276
+ if require_il and not il_found:
277
+ raise OperatorPreconditionError(
278
+ "Mutation",
279
+ "U4b violation: ZHIR requires prior IL (Coherence) for stable transformation base. "
280
+ "Apply Coherence before mutation sequence. "
281
+ f"Recent history: {history_names[-5:] if len(history_names) > 5 else history_names}"
282
+ )
283
+
284
+ if il_found:
285
+ logger.debug(f"Node {node}: ZHIR IL precedence satisfied (prior Coherence found)")
286
+
287
+ # Part 2: Check for recent destabilizer
288
+ # This also records destabilizer context for telemetry
289
+ context = record_destabilizer_context(G, node, logger)
290
+
291
+ require_destabilizer = bool(G.graph.get("ZHIR_REQUIRE_DESTABILIZER", False))
292
+ destabilizer_found = context.get("destabilizer_operator")
293
+
294
+ if require_destabilizer and destabilizer_found is None:
295
+ recent_history = context.get("recent_history", [])
296
+ raise OperatorPreconditionError(
297
+ "Mutation",
298
+ "U4b violation: ZHIR requires recent destabilizer (OZ/VAL/etc) within ~3 ops. "
299
+ f"Recent history: {recent_history}. "
300
+ "Apply Dissonance or Expansion to elevate ΔNFR first."
301
+ )
302
+
303
+
304
+ def record_destabilizer_context(
305
+ G: TNFRGraph, node: NodeId, logger: logging.Logger | None = None
306
+ ) -> dict:
307
+ """Detect and record which destabilizer enabled the current mutation.
308
+
309
+ This implements R4 Extended telemetry by analyzing the glyph_history
310
+ to determine which destabilizer type (strong/moderate/weak) is within
311
+ its appropriate bifurcation window.
312
+
313
+ Parameters
314
+ ----------
315
+ G : TNFRGraph
316
+ Graph containing the node
317
+ node : NodeId
318
+ Node being mutated
319
+ logger : logging.Logger, optional
320
+ Logger for telemetry output
321
+
322
+ Returns
323
+ -------
324
+ dict
325
+ Destabilizer context with keys:
326
+ - destabilizer_type: "strong"/"moderate"/"weak"/None
327
+ - destabilizer_operator: Name of destabilizer glyph
328
+ - destabilizer_distance: Operations since destabilizer
329
+ - recent_history: Last N operator names
330
+
331
+ Notes
332
+ -----
333
+ The destabilizer context is stored in node['_mutation_context'] for
334
+ structural tracing and post-hoc analysis. This enables understanding
335
+ of bifurcation pathways without breaking TNFR structural invariants.
336
+
337
+ **Bifurcation Windows (from BIFURCATION_WINDOWS)**:
338
+ - Strong destabilizers (OZ, VAL): window = 4 operations
339
+ - Moderate destabilizers: window = 2 operations
340
+ - Weak destabilizers: window = 1 operation (immediate only)
341
+
342
+ Examples
343
+ --------
344
+ >>> from tnfr.structural import create_nfr
345
+ >>> from tnfr.operators import Dissonance
346
+ >>> G, node = create_nfr("test", epi=0.5, vf=1.0)
347
+ >>> Dissonance()(G, node) # Apply OZ (strong destabilizer)
348
+ >>> context = record_destabilizer_context(G, node)
349
+ >>> context["destabilizer_type"] # doctest: +SKIP
350
+ 'strong'
351
+ >>> context["destabilizer_operator"] # doctest: +SKIP
352
+ 'dissonance'
353
+ """
354
+ if logger is None:
355
+ import logging
356
+ logger = logging.getLogger(__name__)
357
+
358
+ # Get glyph history from node
359
+ history = G.nodes[node].get("glyph_history", [])
360
+ if not history:
361
+ # No history available, mutation enabled by external factors
362
+ context = {
363
+ "destabilizer_type": None,
364
+ "destabilizer_operator": None,
365
+ "destabilizer_distance": None,
366
+ "recent_history": [],
367
+ }
368
+ G.nodes[node]["_mutation_context"] = context
369
+ return context
370
+
371
+ # Import glyph_function_name to convert glyphs to operator names
372
+ from ..grammar import glyph_function_name
373
+
374
+ # Get recent history (up to max window size)
375
+ max_window = BIFURCATION_WINDOWS["strong"]
376
+ recent = list(history)[-max_window:] if len(history) > max_window else list(history)
377
+ recent_names = [glyph_function_name(g) for g in recent]
378
+
379
+ # Search backwards for destabilizers, checking window constraints
380
+ destabilizer_found = None
381
+ destabilizer_type = None
382
+ destabilizer_distance = None
383
+
384
+ for i, op_name in enumerate(reversed(recent_names)):
385
+ distance = i + 1 # Distance from mutation (1 = immediate predecessor)
386
+
387
+ # Check strong destabilizers (window = 4)
388
+ if (
389
+ op_name in DESTABILIZERS_STRONG
390
+ and distance <= BIFURCATION_WINDOWS["strong"]
391
+ ):
392
+ destabilizer_found = op_name
393
+ destabilizer_type = "strong"
394
+ destabilizer_distance = distance
395
+ break
396
+
397
+ # Check moderate destabilizers (window = 2)
398
+ if (
399
+ op_name in DESTABILIZERS_MODERATE
400
+ and distance <= BIFURCATION_WINDOWS["moderate"]
401
+ ):
402
+ destabilizer_found = op_name
403
+ destabilizer_type = "moderate"
404
+ destabilizer_distance = distance
405
+ break
406
+
407
+ # Check weak destabilizers (window = 1, immediate only)
408
+ if op_name in DESTABILIZERS_WEAK and distance == 1:
409
+ destabilizer_found = op_name
410
+ destabilizer_type = "weak"
411
+ destabilizer_distance = distance
412
+ break
413
+
414
+ # Store context in node metadata for telemetry
415
+ context = {
416
+ "destabilizer_type": destabilizer_type,
417
+ "destabilizer_operator": destabilizer_found,
418
+ "destabilizer_distance": destabilizer_distance,
419
+ "recent_history": recent_names,
420
+ }
421
+ G.nodes[node]["_mutation_context"] = context
422
+
423
+ # Log telemetry for structural tracing
424
+ if destabilizer_found:
425
+ logger.info(
426
+ f"Node {node}: ZHIR enabled by {destabilizer_type} destabilizer "
427
+ f"({destabilizer_found}) at distance {destabilizer_distance}"
428
+ )
429
+ else:
430
+ logger.warning(
431
+ f"Node {node}: ZHIR without detectable destabilizer in history. "
432
+ f"Recent operators: {recent_names}"
433
+ )
434
+
435
+ return context
436
+
437
+
438
+ def diagnose_mutation_readiness(G: TNFRGraph, node: NodeId) -> dict:
439
+ """Comprehensive diagnostic for ZHIR readiness.
440
+
441
+ Analyzes node state and returns detailed readiness report with:
442
+ - Overall readiness boolean
443
+ - Individual check results
444
+ - Recommendations for corrections
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 structure:
457
+ {
458
+ "ready": bool,
459
+ "checks": {
460
+ "minimum_vf": {"passed": bool, "value": float, "threshold": float},
461
+ "threshold_crossing": {"passed": bool, "depi_dt": float, "xi": float},
462
+ "il_precedence": {"passed": bool, "found": bool},
463
+ "recent_destabilizer": {"passed": bool, "type": str|None, "distance": int|None},
464
+ "history_length": {"passed": bool, "length": int, "required": int},
465
+ },
466
+ "recommendations": [str, ...]
467
+ }
468
+
469
+ Examples
470
+ --------
471
+ >>> from tnfr.structural import create_nfr
472
+ >>> G, node = create_nfr("test", epi=0.5, vf=1.0)
473
+ >>> report = diagnose_mutation_readiness(G, node)
474
+ >>> report["ready"] # doctest: +SKIP
475
+ False
476
+ >>> report["recommendations"] # doctest: +SKIP
477
+ ['Apply IL (Coherence) for stable base', 'Apply OZ (Dissonance) to elevate ΔNFR', ...]
478
+ """
479
+ import logging
480
+
481
+ checks = {}
482
+ recommendations = []
483
+
484
+ # Check 1: Minimum νf
485
+ vf = float(get_attr(G.nodes[node], ALIAS_VF, 0.0))
486
+ min_vf = float(G.graph.get("ZHIR_MIN_VF", 0.05))
487
+ vf_passed = vf >= min_vf
488
+ checks["minimum_vf"] = {
489
+ "passed": vf_passed,
490
+ "value": vf,
491
+ "threshold": min_vf,
492
+ }
493
+ if not vf_passed:
494
+ recommendations.append(
495
+ f"Increase νf: current={vf:.3f}, required={min_vf:.3f}. "
496
+ f"Apply AL (Emission) or NAV (Transition) to boost structural frequency."
497
+ )
498
+
499
+ # Check 2: Threshold crossing
500
+ epi_history = G.nodes[node].get("epi_history") or G.nodes[node].get("_epi_history", [])
501
+ xi_threshold = float(G.graph.get("ZHIR_THRESHOLD_XI", 0.1))
502
+
503
+ if len(epi_history) >= 2:
504
+ depi_dt = abs(epi_history[-1] - epi_history[-2])
505
+ threshold_passed = depi_dt >= xi_threshold
506
+ checks["threshold_crossing"] = {
507
+ "passed": threshold_passed,
508
+ "depi_dt": depi_dt,
509
+ "xi": xi_threshold,
510
+ }
511
+ if not threshold_passed:
512
+ recommendations.append(
513
+ f"Increase structural velocity: ∂EPI/∂t={depi_dt:.3f} < ξ={xi_threshold}. "
514
+ f"Apply OZ (Dissonance) or VAL (Expansion) to elevate reorganization."
515
+ )
516
+ else:
517
+ checks["threshold_crossing"] = {
518
+ "passed": False,
519
+ "depi_dt": None,
520
+ "xi": xi_threshold,
521
+ "reason": "Insufficient history"
522
+ }
523
+ recommendations.append(
524
+ f"Build EPI history: only {len(epi_history)} points available (need ≥2). "
525
+ f"Apply several operators to establish history."
526
+ )
527
+
528
+ # Check 3: IL precedence
529
+ glyph_history = G.nodes[node].get("glyph_history", [])
530
+ if glyph_history:
531
+ from ..grammar import glyph_function_name
532
+ history_names = [glyph_function_name(g) for g in glyph_history]
533
+ il_found = "coherence" in history_names
534
+ else:
535
+ il_found = False
536
+ history_names = []
537
+
538
+ checks["il_precedence"] = {
539
+ "passed": il_found,
540
+ "found": il_found,
541
+ }
542
+ if not il_found:
543
+ recommendations.append(
544
+ "Apply IL (Coherence) for stable transformation base (U4b Part 1)."
545
+ )
546
+
547
+ # Check 4: Recent destabilizer
548
+ logger = logging.getLogger(__name__)
549
+ context = record_destabilizer_context(G, node, logger)
550
+ destabilizer_found = context.get("destabilizer_operator") is not None
551
+
552
+ checks["recent_destabilizer"] = {
553
+ "passed": destabilizer_found,
554
+ "type": context.get("destabilizer_type"),
555
+ "distance": context.get("destabilizer_distance"),
556
+ "operator": context.get("destabilizer_operator"),
557
+ }
558
+ if not destabilizer_found:
559
+ recommendations.append(
560
+ "Apply destabilizer (OZ/VAL) within last ~3 operations to elevate ΔNFR (U4b Part 2)."
561
+ )
562
+
563
+ # Check 5: History length
564
+ min_history = int(G.graph.get("ZHIR_MIN_HISTORY_LENGTH", 2))
565
+ history_passed = len(epi_history) >= min_history
566
+ checks["history_length"] = {
567
+ "passed": history_passed,
568
+ "length": len(epi_history),
569
+ "required": min_history,
570
+ }
571
+ # Already covered by threshold check recommendations
572
+
573
+ # Overall readiness
574
+ all_passed = all(check.get("passed", False) for check in checks.values())
575
+
576
+ return {
577
+ "ready": all_passed,
578
+ "checks": checks,
579
+ "recommendations": recommendations,
580
+ }
@@ -0,0 +1,125 @@
1
+ """Strict precondition validation for EN (Reception) operator.
2
+
3
+ This module implements canonical precondition validation for the Reception (EN)
4
+ structural operator according to TNFR.pdf §2.2.1. EN requires specific structural
5
+ conditions to maintain TNFR operational fidelity:
6
+
7
+ 1. **Receptive capacity**: EPI must be below saturation threshold (node not saturated)
8
+ 2. **Minimal dissonance**: DNFR must be below threshold (low reorganization pressure)
9
+ 3. **Emission sources**: Network should have active emission sources (warning for isolated nodes)
10
+
11
+ These validations protect structural integrity by ensuring EN is only applied to
12
+ nodes in the appropriate state for coherence integration.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ if TYPE_CHECKING:
20
+ from ...types import TNFRGraph
21
+
22
+ __all__ = ["validate_reception_strict"]
23
+
24
+
25
+ def validate_reception_strict(G: TNFRGraph, node: Any) -> None:
26
+ """Validate strict canonical preconditions for EN (Reception) operator.
27
+
28
+ According to TNFR.pdf §2.2.1, Reception (EN - Recepción estructural) requires:
29
+
30
+ 1. **Receptive capacity**: EPI < saturation threshold (node has capacity to receive)
31
+ 2. **Minimal dissonance**: DNFR < threshold (low reorganization pressure for stable integration)
32
+ 3. **Emission sources**: Network connectivity with active sources (warning if isolated)
33
+
34
+ Parameters
35
+ ----------
36
+ G : TNFRGraph
37
+ Graph containing the node to validate
38
+ node : Any
39
+ Node identifier for validation
40
+
41
+ Raises
42
+ ------
43
+ ValueError
44
+ If EPI >= saturation threshold (node saturated - cannot receive more coherence)
45
+ If DNFR >= threshold (excessive dissonance - consider IL/Coherence first)
46
+
47
+ Warnings
48
+ --------
49
+ UserWarning
50
+ If node is isolated in a multi-node network (no emission sources available)
51
+
52
+ Notes
53
+ -----
54
+ Thresholds are configurable via:
55
+ - Graph metadata: ``G.graph["EPI_SATURATION_MAX"]``, ``G.graph["DNFR_RECEPTION_MAX"]``
56
+ - Module defaults: :data:`tnfr.config.thresholds.EPI_SATURATION_MAX`, etc.
57
+
58
+ Examples
59
+ --------
60
+ >>> from tnfr.structural import create_nfr
61
+ >>> from tnfr.operators.preconditions.reception import validate_reception_strict
62
+ >>> G, node = create_nfr("test", epi=0.5, vf=0.9)
63
+ >>> G.nodes[node]["dnfr"] = 0.08
64
+ >>> validate_reception_strict(G, node) # OK - receptive capacity available
65
+
66
+ >>> G2, node2 = create_nfr("saturated", epi=0.95, vf=1.0)
67
+ >>> validate_reception_strict(G2, node2) # doctest: +SKIP
68
+ Traceback (most recent call last):
69
+ ...
70
+ ValueError: EN precondition failed: EPI=0.950 >= 0.9. Node saturated, cannot receive more coherence.
71
+
72
+ See Also
73
+ --------
74
+ tnfr.config.thresholds : Configurable threshold constants
75
+ tnfr.operators.preconditions : Base precondition validators
76
+ tnfr.operators.definitions.Reception : Reception operator implementation
77
+ """
78
+ import warnings
79
+
80
+ from ...alias import get_attr
81
+ from ...constants.aliases import ALIAS_DNFR, ALIAS_EPI
82
+ from ...config.thresholds import DNFR_RECEPTION_MAX, EPI_SATURATION_MAX
83
+
84
+ # Get current node state
85
+ epi = float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
86
+ dnfr = float(get_attr(G.nodes[node], ALIAS_DNFR, 0.0))
87
+
88
+ # Get configurable thresholds (allow override via graph metadata)
89
+ epi_threshold = float(G.graph.get("EPI_SATURATION_MAX", EPI_SATURATION_MAX))
90
+ dnfr_threshold = float(G.graph.get("DNFR_RECEPTION_MAX", DNFR_RECEPTION_MAX))
91
+
92
+ # Precondition 1: EPI must be below saturation threshold (receptive capacity available)
93
+ # Reception integrates external coherence into local structure.
94
+ # If EPI is saturated, node cannot accommodate more coherence.
95
+ if epi >= epi_threshold:
96
+ raise ValueError(
97
+ f"EN precondition failed: EPI={epi:.3f} >= {epi_threshold:.3f}. "
98
+ f"Node saturated, cannot receive more coherence. "
99
+ f"Apply IL (Coherence) first to stabilize and compress structure, "
100
+ f"or apply NUL (Contraction) to reduce complexity if appropriate."
101
+ )
102
+
103
+ # Precondition 2: DNFR must be below threshold (minimal dissonance for stable integration)
104
+ # Excessive reorganization pressure prevents effective integration of external coherence.
105
+ # Node must first stabilize before receiving more information.
106
+ if dnfr >= dnfr_threshold:
107
+ raise ValueError(
108
+ f"EN precondition failed: DNFR={dnfr:.3f} >= {dnfr_threshold:.3f}. "
109
+ f"Excessive dissonance prevents reception. "
110
+ f"Consider IL (Coherence) first to reduce reorganization pressure."
111
+ )
112
+
113
+ # Precondition 3: Emission sources check (warning only - not a hard failure)
114
+ # Isolated nodes can still apply EN, but there are no external sources to receive from
115
+ node_degree = G.degree(node)
116
+ network_size = len(G)
117
+
118
+ if node_degree == 0 and network_size > 1:
119
+ warnings.warn(
120
+ f"EN warning: Node {node!r} isolated. No emission sources available. "
121
+ f"Reception possible but no external coherence to integrate. "
122
+ f"Consider UM (Coupling) to establish network connections first.",
123
+ UserWarning,
124
+ stacklevel=3,
125
+ )