tnfr 4.5.2__py3-none-any.whl → 8.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tnfr might be problematic. Click here for more details.

Files changed (365) hide show
  1. tnfr/__init__.py +334 -50
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +214 -37
  8. tnfr/alias.pyi +108 -0
  9. tnfr/backends/__init__.py +354 -0
  10. tnfr/backends/jax_backend.py +173 -0
  11. tnfr/backends/numpy_backend.py +238 -0
  12. tnfr/backends/optimized_numpy.py +420 -0
  13. tnfr/backends/torch_backend.py +408 -0
  14. tnfr/cache.py +149 -556
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +51 -16
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +344 -32
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +676 -50
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +18 -3
  24. tnfr/cli/utils.pyi +7 -0
  25. tnfr/cli/validate.py +236 -0
  26. tnfr/compat/__init__.py +85 -0
  27. tnfr/compat/dataclass.py +136 -0
  28. tnfr/compat/jsonschema_stub.py +61 -0
  29. tnfr/compat/matplotlib_stub.py +73 -0
  30. tnfr/compat/numpy_stub.py +155 -0
  31. tnfr/config/__init__.py +224 -0
  32. tnfr/config/__init__.pyi +10 -0
  33. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/{constants/core.py → config/defaults_core.py} +59 -6
  37. tnfr/config/defaults_init.py +33 -0
  38. tnfr/config/defaults_metric.py +104 -0
  39. tnfr/config/feature_flags.py +81 -0
  40. tnfr/config/feature_flags.pyi +16 -0
  41. tnfr/config/glyph_constants.py +31 -0
  42. tnfr/config/init.py +77 -0
  43. tnfr/config/init.pyi +8 -0
  44. tnfr/config/operator_names.py +254 -0
  45. tnfr/config/operator_names.pyi +36 -0
  46. tnfr/config/physics_derivation.py +354 -0
  47. tnfr/config/presets.py +83 -0
  48. tnfr/config/presets.pyi +7 -0
  49. tnfr/config/security.py +927 -0
  50. tnfr/config/thresholds.py +114 -0
  51. tnfr/config/tnfr_config.py +498 -0
  52. tnfr/constants/__init__.py +51 -133
  53. tnfr/constants/__init__.pyi +92 -0
  54. tnfr/constants/aliases.py +33 -0
  55. tnfr/constants/aliases.pyi +27 -0
  56. tnfr/constants/init.py +3 -1
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +9 -15
  59. tnfr/constants/metric.pyi +19 -0
  60. tnfr/core/__init__.py +33 -0
  61. tnfr/core/container.py +226 -0
  62. tnfr/core/default_implementations.py +329 -0
  63. tnfr/core/interfaces.py +279 -0
  64. tnfr/dynamics/__init__.py +213 -633
  65. tnfr/dynamics/__init__.pyi +83 -0
  66. tnfr/dynamics/adaptation.py +267 -0
  67. tnfr/dynamics/adaptation.pyi +7 -0
  68. tnfr/dynamics/adaptive_sequences.py +189 -0
  69. tnfr/dynamics/adaptive_sequences.pyi +14 -0
  70. tnfr/dynamics/aliases.py +23 -0
  71. tnfr/dynamics/aliases.pyi +19 -0
  72. tnfr/dynamics/bifurcation.py +232 -0
  73. tnfr/dynamics/canonical.py +229 -0
  74. tnfr/dynamics/canonical.pyi +48 -0
  75. tnfr/dynamics/coordination.py +385 -0
  76. tnfr/dynamics/coordination.pyi +25 -0
  77. tnfr/dynamics/dnfr.py +2699 -398
  78. tnfr/dynamics/dnfr.pyi +26 -0
  79. tnfr/dynamics/dynamic_limits.py +225 -0
  80. tnfr/dynamics/feedback.py +252 -0
  81. tnfr/dynamics/feedback.pyi +24 -0
  82. tnfr/dynamics/fused_dnfr.py +454 -0
  83. tnfr/dynamics/homeostasis.py +157 -0
  84. tnfr/dynamics/homeostasis.pyi +14 -0
  85. tnfr/dynamics/integrators.py +496 -102
  86. tnfr/dynamics/integrators.pyi +36 -0
  87. tnfr/dynamics/learning.py +310 -0
  88. tnfr/dynamics/learning.pyi +33 -0
  89. tnfr/dynamics/metabolism.py +254 -0
  90. tnfr/dynamics/nbody.py +796 -0
  91. tnfr/dynamics/nbody_tnfr.py +783 -0
  92. tnfr/dynamics/propagation.py +326 -0
  93. tnfr/dynamics/runtime.py +908 -0
  94. tnfr/dynamics/runtime.pyi +77 -0
  95. tnfr/dynamics/sampling.py +10 -5
  96. tnfr/dynamics/sampling.pyi +7 -0
  97. tnfr/dynamics/selectors.py +711 -0
  98. tnfr/dynamics/selectors.pyi +85 -0
  99. tnfr/dynamics/structural_clip.py +207 -0
  100. tnfr/errors/__init__.py +37 -0
  101. tnfr/errors/contextual.py +492 -0
  102. tnfr/execution.py +77 -55
  103. tnfr/execution.pyi +45 -0
  104. tnfr/extensions/__init__.py +205 -0
  105. tnfr/extensions/__init__.pyi +18 -0
  106. tnfr/extensions/base.py +173 -0
  107. tnfr/extensions/base.pyi +35 -0
  108. tnfr/extensions/business/__init__.py +71 -0
  109. tnfr/extensions/business/__init__.pyi +11 -0
  110. tnfr/extensions/business/cookbook.py +88 -0
  111. tnfr/extensions/business/cookbook.pyi +8 -0
  112. tnfr/extensions/business/health_analyzers.py +202 -0
  113. tnfr/extensions/business/health_analyzers.pyi +9 -0
  114. tnfr/extensions/business/patterns.py +183 -0
  115. tnfr/extensions/business/patterns.pyi +8 -0
  116. tnfr/extensions/medical/__init__.py +73 -0
  117. tnfr/extensions/medical/__init__.pyi +11 -0
  118. tnfr/extensions/medical/cookbook.py +88 -0
  119. tnfr/extensions/medical/cookbook.pyi +8 -0
  120. tnfr/extensions/medical/health_analyzers.py +181 -0
  121. tnfr/extensions/medical/health_analyzers.pyi +9 -0
  122. tnfr/extensions/medical/patterns.py +163 -0
  123. tnfr/extensions/medical/patterns.pyi +8 -0
  124. tnfr/flatten.py +29 -50
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +66 -53
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +144 -57
  129. tnfr/glyph_history.pyi +35 -0
  130. tnfr/glyph_runtime.py +19 -0
  131. tnfr/glyph_runtime.pyi +8 -0
  132. tnfr/immutable.py +70 -30
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +22 -16
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +5 -241
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.pyi +7 -0
  139. tnfr/mathematics/__init__.py +79 -0
  140. tnfr/mathematics/backend.py +453 -0
  141. tnfr/mathematics/backend.pyi +99 -0
  142. tnfr/mathematics/dynamics.py +408 -0
  143. tnfr/mathematics/dynamics.pyi +90 -0
  144. tnfr/mathematics/epi.py +391 -0
  145. tnfr/mathematics/epi.pyi +65 -0
  146. tnfr/mathematics/generators.py +242 -0
  147. tnfr/mathematics/generators.pyi +29 -0
  148. tnfr/mathematics/metrics.py +119 -0
  149. tnfr/mathematics/metrics.pyi +16 -0
  150. tnfr/mathematics/operators.py +239 -0
  151. tnfr/mathematics/operators.pyi +59 -0
  152. tnfr/mathematics/operators_factory.py +124 -0
  153. tnfr/mathematics/operators_factory.pyi +11 -0
  154. tnfr/mathematics/projection.py +87 -0
  155. tnfr/mathematics/projection.pyi +33 -0
  156. tnfr/mathematics/runtime.py +182 -0
  157. tnfr/mathematics/runtime.pyi +64 -0
  158. tnfr/mathematics/spaces.py +256 -0
  159. tnfr/mathematics/spaces.pyi +83 -0
  160. tnfr/mathematics/transforms.py +305 -0
  161. tnfr/mathematics/transforms.pyi +62 -0
  162. tnfr/metrics/__init__.py +47 -9
  163. tnfr/metrics/__init__.pyi +20 -0
  164. tnfr/metrics/buffer_cache.py +163 -0
  165. tnfr/metrics/buffer_cache.pyi +24 -0
  166. tnfr/metrics/cache_utils.py +214 -0
  167. tnfr/metrics/coherence.py +1510 -330
  168. tnfr/metrics/coherence.pyi +129 -0
  169. tnfr/metrics/common.py +23 -16
  170. tnfr/metrics/common.pyi +35 -0
  171. tnfr/metrics/core.py +251 -36
  172. tnfr/metrics/core.pyi +13 -0
  173. tnfr/metrics/diagnosis.py +709 -110
  174. tnfr/metrics/diagnosis.pyi +86 -0
  175. tnfr/metrics/emergence.py +245 -0
  176. tnfr/metrics/export.py +60 -18
  177. tnfr/metrics/export.pyi +7 -0
  178. tnfr/metrics/glyph_timing.py +233 -43
  179. tnfr/metrics/glyph_timing.pyi +81 -0
  180. tnfr/metrics/learning_metrics.py +280 -0
  181. tnfr/metrics/learning_metrics.pyi +21 -0
  182. tnfr/metrics/phase_coherence.py +351 -0
  183. tnfr/metrics/phase_compatibility.py +349 -0
  184. tnfr/metrics/reporting.py +63 -28
  185. tnfr/metrics/reporting.pyi +25 -0
  186. tnfr/metrics/sense_index.py +1126 -43
  187. tnfr/metrics/sense_index.pyi +9 -0
  188. tnfr/metrics/trig.py +215 -23
  189. tnfr/metrics/trig.pyi +13 -0
  190. tnfr/metrics/trig_cache.py +148 -24
  191. tnfr/metrics/trig_cache.pyi +10 -0
  192. tnfr/multiscale/__init__.py +32 -0
  193. tnfr/multiscale/hierarchical.py +517 -0
  194. tnfr/node.py +646 -140
  195. tnfr/node.pyi +139 -0
  196. tnfr/observers.py +160 -45
  197. tnfr/observers.pyi +31 -0
  198. tnfr/ontosim.py +23 -19
  199. tnfr/ontosim.pyi +28 -0
  200. tnfr/operators/__init__.py +1358 -106
  201. tnfr/operators/__init__.pyi +31 -0
  202. tnfr/operators/algebra.py +277 -0
  203. tnfr/operators/canonical_patterns.py +420 -0
  204. tnfr/operators/cascade.py +267 -0
  205. tnfr/operators/cycle_detection.py +358 -0
  206. tnfr/operators/definitions.py +4108 -0
  207. tnfr/operators/definitions.pyi +78 -0
  208. tnfr/operators/grammar.py +1164 -0
  209. tnfr/operators/grammar.pyi +140 -0
  210. tnfr/operators/hamiltonian.py +710 -0
  211. tnfr/operators/health_analyzer.py +809 -0
  212. tnfr/operators/jitter.py +107 -38
  213. tnfr/operators/jitter.pyi +11 -0
  214. tnfr/operators/lifecycle.py +314 -0
  215. tnfr/operators/metabolism.py +618 -0
  216. tnfr/operators/metrics.py +2138 -0
  217. tnfr/operators/network_analysis/__init__.py +27 -0
  218. tnfr/operators/network_analysis/source_detection.py +186 -0
  219. tnfr/operators/nodal_equation.py +395 -0
  220. tnfr/operators/pattern_detection.py +660 -0
  221. tnfr/operators/patterns.py +669 -0
  222. tnfr/operators/postconditions/__init__.py +38 -0
  223. tnfr/operators/postconditions/mutation.py +236 -0
  224. tnfr/operators/preconditions/__init__.py +1226 -0
  225. tnfr/operators/preconditions/coherence.py +305 -0
  226. tnfr/operators/preconditions/dissonance.py +236 -0
  227. tnfr/operators/preconditions/emission.py +128 -0
  228. tnfr/operators/preconditions/mutation.py +580 -0
  229. tnfr/operators/preconditions/reception.py +125 -0
  230. tnfr/operators/preconditions/resonance.py +364 -0
  231. tnfr/operators/registry.py +74 -0
  232. tnfr/operators/registry.pyi +9 -0
  233. tnfr/operators/remesh.py +1415 -91
  234. tnfr/operators/remesh.pyi +26 -0
  235. tnfr/operators/structural_units.py +268 -0
  236. tnfr/operators/unified_grammar.py +105 -0
  237. tnfr/parallel/__init__.py +54 -0
  238. tnfr/parallel/auto_scaler.py +234 -0
  239. tnfr/parallel/distributed.py +384 -0
  240. tnfr/parallel/engine.py +238 -0
  241. tnfr/parallel/gpu_engine.py +420 -0
  242. tnfr/parallel/monitoring.py +248 -0
  243. tnfr/parallel/partitioner.py +459 -0
  244. tnfr/py.typed +0 -0
  245. tnfr/recipes/__init__.py +22 -0
  246. tnfr/recipes/cookbook.py +743 -0
  247. tnfr/rng.py +75 -151
  248. tnfr/rng.pyi +26 -0
  249. tnfr/schemas/__init__.py +8 -0
  250. tnfr/schemas/grammar.json +94 -0
  251. tnfr/sdk/__init__.py +107 -0
  252. tnfr/sdk/__init__.pyi +19 -0
  253. tnfr/sdk/adaptive_system.py +173 -0
  254. tnfr/sdk/adaptive_system.pyi +21 -0
  255. tnfr/sdk/builders.py +370 -0
  256. tnfr/sdk/builders.pyi +51 -0
  257. tnfr/sdk/fluent.py +1121 -0
  258. tnfr/sdk/fluent.pyi +74 -0
  259. tnfr/sdk/templates.py +342 -0
  260. tnfr/sdk/templates.pyi +41 -0
  261. tnfr/sdk/utils.py +341 -0
  262. tnfr/secure_config.py +46 -0
  263. tnfr/security/__init__.py +70 -0
  264. tnfr/security/database.py +514 -0
  265. tnfr/security/subprocess.py +503 -0
  266. tnfr/security/validation.py +290 -0
  267. tnfr/selector.py +59 -22
  268. tnfr/selector.pyi +19 -0
  269. tnfr/sense.py +92 -67
  270. tnfr/sense.pyi +23 -0
  271. tnfr/services/__init__.py +17 -0
  272. tnfr/services/orchestrator.py +325 -0
  273. tnfr/sparse/__init__.py +39 -0
  274. tnfr/sparse/representations.py +492 -0
  275. tnfr/structural.py +639 -263
  276. tnfr/structural.pyi +83 -0
  277. tnfr/telemetry/__init__.py +35 -0
  278. tnfr/telemetry/cache_metrics.py +226 -0
  279. tnfr/telemetry/cache_metrics.pyi +64 -0
  280. tnfr/telemetry/nu_f.py +422 -0
  281. tnfr/telemetry/nu_f.pyi +108 -0
  282. tnfr/telemetry/verbosity.py +36 -0
  283. tnfr/telemetry/verbosity.pyi +15 -0
  284. tnfr/tokens.py +2 -4
  285. tnfr/tokens.pyi +36 -0
  286. tnfr/tools/__init__.py +20 -0
  287. tnfr/tools/domain_templates.py +478 -0
  288. tnfr/tools/sequence_generator.py +846 -0
  289. tnfr/topology/__init__.py +13 -0
  290. tnfr/topology/asymmetry.py +151 -0
  291. tnfr/trace.py +300 -126
  292. tnfr/trace.pyi +42 -0
  293. tnfr/tutorials/__init__.py +38 -0
  294. tnfr/tutorials/autonomous_evolution.py +285 -0
  295. tnfr/tutorials/interactive.py +1576 -0
  296. tnfr/tutorials/structural_metabolism.py +238 -0
  297. tnfr/types.py +743 -12
  298. tnfr/types.pyi +357 -0
  299. tnfr/units.py +68 -0
  300. tnfr/units.pyi +13 -0
  301. tnfr/utils/__init__.py +282 -0
  302. tnfr/utils/__init__.pyi +215 -0
  303. tnfr/utils/cache.py +4223 -0
  304. tnfr/utils/cache.pyi +470 -0
  305. tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
  306. tnfr/utils/callbacks.pyi +49 -0
  307. tnfr/utils/chunks.py +108 -0
  308. tnfr/utils/chunks.pyi +22 -0
  309. tnfr/utils/data.py +428 -0
  310. tnfr/utils/data.pyi +74 -0
  311. tnfr/utils/graph.py +85 -0
  312. tnfr/utils/graph.pyi +10 -0
  313. tnfr/utils/init.py +821 -0
  314. tnfr/utils/init.pyi +80 -0
  315. tnfr/utils/io.py +559 -0
  316. tnfr/utils/io.pyi +66 -0
  317. tnfr/{helpers → utils}/numeric.py +51 -24
  318. tnfr/utils/numeric.pyi +21 -0
  319. tnfr/validation/__init__.py +257 -0
  320. tnfr/validation/__init__.pyi +85 -0
  321. tnfr/validation/compatibility.py +460 -0
  322. tnfr/validation/compatibility.pyi +6 -0
  323. tnfr/validation/config.py +73 -0
  324. tnfr/validation/graph.py +139 -0
  325. tnfr/validation/graph.pyi +18 -0
  326. tnfr/validation/input_validation.py +755 -0
  327. tnfr/validation/invariants.py +712 -0
  328. tnfr/validation/rules.py +253 -0
  329. tnfr/validation/rules.pyi +44 -0
  330. tnfr/validation/runtime.py +279 -0
  331. tnfr/validation/runtime.pyi +28 -0
  332. tnfr/validation/sequence_validator.py +162 -0
  333. tnfr/validation/soft_filters.py +170 -0
  334. tnfr/validation/soft_filters.pyi +32 -0
  335. tnfr/validation/spectral.py +164 -0
  336. tnfr/validation/spectral.pyi +42 -0
  337. tnfr/validation/validator.py +1266 -0
  338. tnfr/validation/window.py +39 -0
  339. tnfr/validation/window.pyi +1 -0
  340. tnfr/visualization/__init__.py +98 -0
  341. tnfr/visualization/cascade_viz.py +256 -0
  342. tnfr/visualization/hierarchy.py +284 -0
  343. tnfr/visualization/sequence_plotter.py +784 -0
  344. tnfr/viz/__init__.py +60 -0
  345. tnfr/viz/matplotlib.py +278 -0
  346. tnfr/viz/matplotlib.pyi +35 -0
  347. tnfr-8.5.0.dist-info/METADATA +573 -0
  348. tnfr-8.5.0.dist-info/RECORD +353 -0
  349. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
  350. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
  351. tnfr/collections_utils.py +0 -300
  352. tnfr/config.py +0 -32
  353. tnfr/grammar.py +0 -344
  354. tnfr/graph_utils.py +0 -84
  355. tnfr/helpers/__init__.py +0 -71
  356. tnfr/import_utils.py +0 -228
  357. tnfr/json_utils.py +0 -162
  358. tnfr/logging_utils.py +0 -116
  359. tnfr/presets.py +0 -60
  360. tnfr/validators.py +0 -84
  361. tnfr/value_utils.py +0 -59
  362. tnfr-4.5.2.dist-info/METADATA +0 -379
  363. tnfr-4.5.2.dist-info/RECORD +0 -67
  364. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  365. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,711 @@
1
+ """Glyph selection helpers for TNFR dynamics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..compat.dataclass import dataclass
6
+ import math
7
+ import sys
8
+ from abc import ABC, abstractmethod
9
+ from collections.abc import Mapping, MutableMapping, Sequence
10
+ from concurrent.futures import ProcessPoolExecutor
11
+ from operator import itemgetter
12
+ from typing import Any, cast
13
+ from ..alias import collect_attr, get_attr
14
+ from ..constants import get_graph_param, get_param
15
+ from ..glyph_history import ensure_history
16
+ from ..utils import clamp01, resolve_chunk_size
17
+ from ..metrics.common import compute_dnfr_accel_max, merge_and_normalize_weights
18
+ from ..operators import apply_glyph
19
+ from ..validation import (
20
+ GrammarContext,
21
+ StructuralGrammarError,
22
+ enforce_canonical_grammar,
23
+ on_applied_glyph,
24
+ record_grammar_violation,
25
+ )
26
+ from ..selector import (
27
+ _apply_selector_hysteresis,
28
+ _calc_selector_score,
29
+ _selector_norms,
30
+ _selector_parallel_jobs,
31
+ _selector_thresholds,
32
+ )
33
+ from ..types import Glyph, GlyphCode, GlyphSelector, HistoryState, NodeId, TNFRGraph
34
+ from ..utils import get_numpy
35
+ from ..validation import soft_grammar_filters
36
+ from .aliases import ALIAS_D2EPI, ALIAS_DNFR, ALIAS_DSI, ALIAS_SI
37
+
38
+ __all__ = (
39
+ "GlyphCode",
40
+ "AbstractSelector",
41
+ "DefaultGlyphSelector",
42
+ "ParametricGlyphSelector",
43
+ "default_glyph_selector",
44
+ "parametric_glyph_selector",
45
+ "_SelectorPreselection",
46
+ "_configure_selector_weights",
47
+ "_apply_selector",
48
+ "_apply_glyphs",
49
+ "_selector_parallel_jobs",
50
+ "_prepare_selector_preselection",
51
+ "_resolve_preselected_glyph",
52
+ "_choose_glyph",
53
+ )
54
+
55
+
56
+ class AbstractSelector(ABC):
57
+ """Interface describing glyph selector lifecycle hooks."""
58
+
59
+ def prepare(
60
+ self, graph: TNFRGraph, nodes: Sequence[NodeId]
61
+ ) -> None: # pragma: no cover - default no-op
62
+ """Prepare selector state before evaluating a glyph batch."""
63
+
64
+ @abstractmethod
65
+ def select(self, graph: TNFRGraph, node: NodeId) -> GlyphCode:
66
+ """Return the glyph to apply for ``node`` within ``graph``."""
67
+
68
+ def __call__(self, graph: TNFRGraph, node: NodeId) -> GlyphCode:
69
+ """Allow selectors to be used as legacy callables."""
70
+
71
+ return self.select(graph, node)
72
+
73
+
74
+ def _default_selector_logic(G: TNFRGraph, n: NodeId) -> GlyphCode:
75
+ nd = G.nodes[n]
76
+ thr = _selector_thresholds(G)
77
+ hi, lo, dnfr_hi = itemgetter("si_hi", "si_lo", "dnfr_hi")(thr)
78
+
79
+ norms = G.graph.get("_sel_norms")
80
+ if norms is None:
81
+ norms = compute_dnfr_accel_max(G)
82
+ G.graph["_sel_norms"] = norms
83
+ dnfr_max = float(norms.get("dnfr_max", 1.0)) or 1.0
84
+
85
+ Si = clamp01(get_attr(nd, ALIAS_SI, 0.5))
86
+ dnfr = abs(get_attr(nd, ALIAS_DNFR, 0.0)) / dnfr_max
87
+
88
+ if Si >= hi:
89
+ return "IL"
90
+ if Si <= lo:
91
+ return "OZ" if dnfr > dnfr_hi else "ZHIR"
92
+ return "NAV" if dnfr > dnfr_hi else "RA"
93
+
94
+
95
+ def _soft_grammar_prefilter(
96
+ G: TNFRGraph,
97
+ n: NodeId,
98
+ cand: GlyphCode,
99
+ ) -> GlyphCode:
100
+ """Soft grammar: avoid repetitions before the canonical one."""
101
+
102
+ ctx = GrammarContext.from_graph(G)
103
+ filtered = soft_grammar_filters(ctx, n, cand)
104
+ return cast(GlyphCode, filtered)
105
+
106
+
107
+ def _selector_normalized_metrics(
108
+ nd: Mapping[str, Any], norms: Mapping[str, float]
109
+ ) -> tuple[float, float, float]:
110
+ dnfr_max = float(norms.get("dnfr_max", 1.0)) or 1.0
111
+ acc_max = float(norms.get("accel_max", 1.0)) or 1.0
112
+ Si = clamp01(get_attr(nd, ALIAS_SI, 0.5))
113
+ dnfr = abs(get_attr(nd, ALIAS_DNFR, 0.0)) / dnfr_max
114
+ accel = abs(get_attr(nd, ALIAS_D2EPI, 0.0)) / acc_max
115
+ return Si, dnfr, accel
116
+
117
+
118
+ def _selector_base_choice(
119
+ Si: float, dnfr: float, accel: float, thr: Mapping[str, float]
120
+ ) -> GlyphCode:
121
+ si_hi, si_lo, dnfr_hi, acc_hi = itemgetter("si_hi", "si_lo", "dnfr_hi", "accel_hi")(
122
+ thr
123
+ )
124
+ if Si >= si_hi:
125
+ return "IL"
126
+ if Si <= si_lo:
127
+ if accel >= acc_hi:
128
+ return "THOL"
129
+ return "OZ" if dnfr >= dnfr_hi else "ZHIR"
130
+ if dnfr >= dnfr_hi or accel >= acc_hi:
131
+ return "NAV"
132
+ return "RA"
133
+
134
+
135
+ def _configure_selector_weights(G: TNFRGraph) -> Mapping[str, float]:
136
+ """Load and cache selector weight configuration from graph parameters."""
137
+
138
+ weights = merge_and_normalize_weights(
139
+ G, "SELECTOR_WEIGHTS", ("w_si", "w_dnfr", "w_accel")
140
+ )
141
+ cast_weights = cast(Mapping[str, float], weights)
142
+ G.graph["_selector_weights"] = cast_weights
143
+ return cast_weights
144
+
145
+
146
+ def _compute_selector_score(
147
+ G: TNFRGraph,
148
+ nd: Mapping[str, Any],
149
+ Si: float,
150
+ dnfr: float,
151
+ accel: float,
152
+ cand: GlyphCode,
153
+ ) -> float:
154
+ W = G.graph.get("_selector_weights")
155
+ if W is None:
156
+ W = _configure_selector_weights(G)
157
+ score = _calc_selector_score(Si, dnfr, accel, cast(Mapping[str, float], W))
158
+ hist_prev = nd.get("glyph_history")
159
+ if hist_prev and hist_prev[-1] == cand:
160
+ delta_si = get_attr(nd, ALIAS_DSI, 0.0)
161
+ h = ensure_history(G)
162
+ sig = h.get("sense_sigma_mag", [])
163
+ delta_sigma = sig[-1] - sig[-2] if len(sig) >= 2 else 0.0
164
+ if delta_si <= 0.0 and delta_sigma <= 0.0:
165
+ score -= 0.05
166
+ return float(score)
167
+
168
+
169
+ def _apply_score_override(
170
+ cand: GlyphCode, score: float, dnfr: float, dnfr_lo: float
171
+ ) -> GlyphCode:
172
+ cand_key = str(cand)
173
+ if score >= 0.66 and cand_key in ("NAV", "RA", "ZHIR", "OZ"):
174
+ return "IL"
175
+ if score <= 0.33 and cand_key in ("NAV", "RA", "IL"):
176
+ return "OZ" if dnfr >= dnfr_lo else "ZHIR"
177
+ return cand
178
+
179
+
180
+ def _parametric_selector_logic(G: TNFRGraph, n: NodeId) -> GlyphCode:
181
+ nd = G.nodes[n]
182
+ thr = _selector_thresholds(G)
183
+ margin: float | None = get_graph_param(G, "GLYPH_SELECTOR_MARGIN")
184
+
185
+ norms = cast(Mapping[str, float] | None, G.graph.get("_sel_norms"))
186
+ if norms is None:
187
+ norms = _selector_norms(G)
188
+ Si, dnfr, accel = _selector_normalized_metrics(nd, norms)
189
+
190
+ cand = _selector_base_choice(Si, dnfr, accel, thr)
191
+
192
+ hist_cand = _apply_selector_hysteresis(nd, Si, dnfr, accel, thr, margin)
193
+ if hist_cand is not None:
194
+ return hist_cand
195
+
196
+ score = _compute_selector_score(G, nd, Si, dnfr, accel, cand)
197
+
198
+ cand = _apply_score_override(cand, score, dnfr, thr["dnfr_lo"])
199
+
200
+ return _soft_grammar_prefilter(G, n, cand)
201
+
202
+
203
+ @dataclass(slots=True)
204
+ class _SelectorPreselection:
205
+ """Precomputed selector context shared across glyph decisions."""
206
+
207
+ kind: str
208
+ metrics: Mapping[Any, tuple[float, float, float]]
209
+ base_choices: Mapping[Any, GlyphCode]
210
+ thresholds: Mapping[str, float] | None = None
211
+ margin: float | None = None
212
+
213
+
214
+ def _build_default_preselection(
215
+ G: TNFRGraph, nodes: Sequence[NodeId]
216
+ ) -> _SelectorPreselection:
217
+ node_list = list(nodes)
218
+ thresholds = _selector_thresholds(G)
219
+ if not node_list:
220
+ return _SelectorPreselection("default", {}, {}, thresholds=thresholds)
221
+
222
+ norms = G.graph.get("_sel_norms") or _selector_norms(G)
223
+ n_jobs = _selector_parallel_jobs(G)
224
+ metrics = _collect_selector_metrics(G, node_list, norms, n_jobs=n_jobs)
225
+ base_choices = _compute_default_base_choices(metrics, thresholds)
226
+ return _SelectorPreselection(
227
+ "default", metrics, base_choices, thresholds=thresholds
228
+ )
229
+
230
+
231
+ def _build_param_preselection(
232
+ G: TNFRGraph, nodes: Sequence[NodeId]
233
+ ) -> _SelectorPreselection:
234
+ node_list = list(nodes)
235
+ thresholds = _selector_thresholds(G)
236
+ margin: float | None = get_graph_param(G, "GLYPH_SELECTOR_MARGIN")
237
+ if not node_list:
238
+ return _SelectorPreselection(
239
+ "param", {}, {}, thresholds=thresholds, margin=margin
240
+ )
241
+
242
+ norms = G.graph.get("_sel_norms") or _selector_norms(G)
243
+ n_jobs = _selector_parallel_jobs(G)
244
+ metrics = _collect_selector_metrics(G, node_list, norms, n_jobs=n_jobs)
245
+ base_choices = _compute_param_base_choices(metrics, thresholds, n_jobs)
246
+ return _SelectorPreselection(
247
+ "param",
248
+ metrics,
249
+ base_choices,
250
+ thresholds=thresholds,
251
+ margin=margin,
252
+ )
253
+
254
+
255
+ class DefaultGlyphSelector(AbstractSelector):
256
+ """Selector implementing the legacy default glyph heuristic."""
257
+
258
+ __slots__ = ("_preselection", "_prepared_graph_id")
259
+
260
+ def __init__(self) -> None:
261
+ self._preselection: _SelectorPreselection | None = None
262
+ self._prepared_graph_id: int | None = None
263
+
264
+ def prepare(self, graph: TNFRGraph, nodes: Sequence[NodeId]) -> None:
265
+ """Precompute default selector metrics for ``nodes``."""
266
+
267
+ self._preselection = _build_default_preselection(graph, nodes)
268
+ self._prepared_graph_id = id(graph)
269
+
270
+ def select(self, graph: TNFRGraph, node: NodeId) -> GlyphCode:
271
+ """Return the canonical glyph for ``node`` using cached metrics when available."""
272
+
273
+ if self._prepared_graph_id == id(graph):
274
+ preselection = self._preselection
275
+ else:
276
+ preselection = None
277
+ return _resolve_preselected_glyph(
278
+ graph, node, _default_selector_logic, preselection
279
+ )
280
+
281
+
282
+ class ParametricGlyphSelector(AbstractSelector):
283
+ """Selector exposing the parametric scoring pipeline."""
284
+
285
+ __slots__ = ("_preselection", "_prepared_graph_id")
286
+
287
+ def __init__(self) -> None:
288
+ self._preselection: _SelectorPreselection | None = None
289
+ self._prepared_graph_id: int | None = None
290
+
291
+ def prepare(self, graph: TNFRGraph, nodes: Sequence[NodeId]) -> None:
292
+ """Precompute parametric selector metrics and hysteresis thresholds."""
293
+
294
+ _selector_norms(graph)
295
+ _configure_selector_weights(graph)
296
+ self._preselection = _build_param_preselection(graph, nodes)
297
+ self._prepared_graph_id = id(graph)
298
+
299
+ def select(self, graph: TNFRGraph, node: NodeId) -> GlyphCode:
300
+ """Return the parametric glyph decision for ``node``."""
301
+
302
+ if self._prepared_graph_id == id(graph):
303
+ preselection = self._preselection
304
+ else:
305
+ preselection = None
306
+ return _resolve_preselected_glyph(
307
+ graph, node, _parametric_selector_logic, preselection
308
+ )
309
+
310
+
311
+ default_glyph_selector = DefaultGlyphSelector()
312
+ parametric_glyph_selector = ParametricGlyphSelector()
313
+
314
+
315
+ def _choose_glyph(
316
+ G: TNFRGraph,
317
+ n: NodeId,
318
+ selector: GlyphSelector,
319
+ use_canon: bool,
320
+ h_al: MutableMapping[Any, int],
321
+ h_en: MutableMapping[Any, int],
322
+ al_max: int,
323
+ en_max: int,
324
+ ) -> GlyphCode:
325
+ """Return glyph for ``n`` considering forced lags and canonical grammar."""
326
+
327
+ if h_al[n] > al_max:
328
+ return Glyph.AL
329
+ if h_en[n] > en_max:
330
+ return Glyph.EN
331
+ g = selector(G, n)
332
+ if use_canon:
333
+ try:
334
+ g = enforce_canonical_grammar(G, n, g)
335
+ except StructuralGrammarError as err:
336
+ nd = G.nodes[n]
337
+ history = tuple(str(item) for item in nd.get("glyph_history", ()))
338
+ selector_name = getattr(selector, "__name__", selector.__class__.__name__)
339
+ err.attach_context(
340
+ node=n, selector=selector_name, history=history, stage="selector"
341
+ )
342
+ record_grammar_violation(G, n, err, stage="selector")
343
+ raise
344
+ return g
345
+
346
+
347
+ def _selector_metrics_chunk(
348
+ args: tuple[list[float], list[float], list[float], float, float],
349
+ ) -> tuple[list[float], list[float], list[float]]:
350
+ """Normalise metric chunk values for multiprocessing execution."""
351
+
352
+ si_values, dnfr_values, accel_values, dnfr_max, accel_max = args
353
+ si_seq = [clamp01(float(v)) for v in si_values]
354
+ dnfr_seq = [abs(float(v)) / dnfr_max for v in dnfr_values]
355
+ accel_seq = [abs(float(v)) / accel_max for v in accel_values]
356
+ return si_seq, dnfr_seq, accel_seq
357
+
358
+
359
+ def _collect_selector_metrics(
360
+ G: TNFRGraph,
361
+ nodes: list[Any],
362
+ norms: Mapping[str, float],
363
+ n_jobs: int | None = None,
364
+ ) -> dict[Any, tuple[float, float, float]]:
365
+ """Return normalised (Si, ΔNFR, acceleration) triples for ``nodes``."""
366
+
367
+ if not nodes:
368
+ return {}
369
+
370
+ dynamics_module = sys.modules.get("tnfr.dynamics")
371
+ get_numpy_fn = get_numpy
372
+ if dynamics_module is not None:
373
+ get_numpy_fn = getattr(dynamics_module, "get_numpy", get_numpy)
374
+
375
+ np_mod = get_numpy_fn()
376
+ dnfr_max = float(norms.get("dnfr_max", 1.0)) or 1.0
377
+ accel_max = float(norms.get("accel_max", 1.0)) or 1.0
378
+
379
+ if np_mod is not None:
380
+ si_seq_np = collect_attr(G, nodes, ALIAS_SI, 0.5, np=np_mod).astype(float)
381
+ si_seq_np = np_mod.clip(si_seq_np, 0.0, 1.0)
382
+ dnfr_seq_np = (
383
+ np_mod.abs(collect_attr(G, nodes, ALIAS_DNFR, 0.0, np=np_mod).astype(float))
384
+ / dnfr_max
385
+ )
386
+ accel_seq_np = (
387
+ np_mod.abs(
388
+ collect_attr(G, nodes, ALIAS_D2EPI, 0.0, np=np_mod).astype(float)
389
+ )
390
+ / accel_max
391
+ )
392
+
393
+ si_seq = si_seq_np.tolist()
394
+ dnfr_seq = dnfr_seq_np.tolist()
395
+ accel_seq = accel_seq_np.tolist()
396
+ else:
397
+ si_values = collect_attr(G, nodes, ALIAS_SI, 0.5)
398
+ dnfr_values = collect_attr(G, nodes, ALIAS_DNFR, 0.0)
399
+ accel_values = collect_attr(G, nodes, ALIAS_D2EPI, 0.0)
400
+
401
+ worker_count = n_jobs if n_jobs is not None and n_jobs > 1 else None
402
+ if worker_count is None:
403
+ si_seq = [clamp01(float(v)) for v in si_values]
404
+ dnfr_seq = [abs(float(v)) / dnfr_max for v in dnfr_values]
405
+ accel_seq = [abs(float(v)) / accel_max for v in accel_values]
406
+ else:
407
+ approx_chunk = (
408
+ math.ceil(len(nodes) / worker_count) if worker_count else None
409
+ )
410
+ chunk_size = resolve_chunk_size(
411
+ approx_chunk,
412
+ len(nodes),
413
+ minimum=1,
414
+ )
415
+ chunk_bounds = [
416
+ (start, min(start + chunk_size, len(nodes)))
417
+ for start in range(0, len(nodes), chunk_size)
418
+ ]
419
+
420
+ si_seq = []
421
+ dnfr_seq = []
422
+ accel_seq = []
423
+
424
+ def _args_iter() -> (
425
+ Sequence[tuple[list[float], list[float], list[float], float, float]]
426
+ ):
427
+ for start, end in chunk_bounds:
428
+ yield (
429
+ si_values[start:end],
430
+ dnfr_values[start:end],
431
+ accel_values[start:end],
432
+ dnfr_max,
433
+ accel_max,
434
+ )
435
+
436
+ executor_cls = ProcessPoolExecutor
437
+ if dynamics_module is not None:
438
+ executor_cls = getattr(
439
+ dynamics_module, "ProcessPoolExecutor", ProcessPoolExecutor
440
+ )
441
+ with executor_cls(max_workers=worker_count) as executor:
442
+ for si_chunk, dnfr_chunk, accel_chunk in executor.map(
443
+ _selector_metrics_chunk, _args_iter()
444
+ ):
445
+ si_seq.extend(si_chunk)
446
+ dnfr_seq.extend(dnfr_chunk)
447
+ accel_seq.extend(accel_chunk)
448
+
449
+ return {
450
+ node: (si_seq[idx], dnfr_seq[idx], accel_seq[idx])
451
+ for idx, node in enumerate(nodes)
452
+ }
453
+
454
+
455
+ def _compute_default_base_choices(
456
+ metrics: Mapping[Any, tuple[float, float, float]],
457
+ thresholds: Mapping[str, float],
458
+ ) -> dict[Any, str]:
459
+ si_hi = float(thresholds.get("si_hi", 0.66))
460
+ si_lo = float(thresholds.get("si_lo", 0.33))
461
+ dnfr_hi = float(thresholds.get("dnfr_hi", 0.50))
462
+
463
+ base: dict[Any, str] = {}
464
+ for node, (Si, dnfr, _) in metrics.items():
465
+ if Si >= si_hi:
466
+ base[node] = "IL"
467
+ elif Si <= si_lo:
468
+ base[node] = "OZ" if dnfr > dnfr_hi else "ZHIR"
469
+ else:
470
+ base[node] = "NAV" if dnfr > dnfr_hi else "RA"
471
+ return base
472
+
473
+
474
+ def _param_base_worker(
475
+ args: tuple[Mapping[str, float], list[tuple[Any, tuple[float, float, float]]]],
476
+ ) -> list[tuple[Any, str]]:
477
+ thresholds, chunk = args
478
+ return [
479
+ (node, _selector_base_choice(Si, dnfr, accel, thresholds))
480
+ for node, (Si, dnfr, accel) in chunk
481
+ ]
482
+
483
+
484
+ def _compute_param_base_choices(
485
+ metrics: Mapping[Any, tuple[float, float, float]],
486
+ thresholds: Mapping[str, float],
487
+ n_jobs: int | None,
488
+ ) -> dict[Any, str]:
489
+ if not metrics:
490
+ return {}
491
+
492
+ items = list(metrics.items())
493
+ if n_jobs is None or n_jobs <= 1:
494
+ return {
495
+ node: _selector_base_choice(Si, dnfr, accel, thresholds)
496
+ for node, (Si, dnfr, accel) in items
497
+ }
498
+
499
+ approx_chunk = math.ceil(len(items) / n_jobs) if n_jobs else None
500
+ chunk_size = resolve_chunk_size(
501
+ approx_chunk,
502
+ len(items),
503
+ minimum=1,
504
+ )
505
+ chunks = [items[i : i + chunk_size] for i in range(0, len(items), chunk_size)]
506
+ base: dict[Any, str] = {}
507
+ args = ((thresholds, chunk) for chunk in chunks)
508
+ executor_cls = ProcessPoolExecutor
509
+ dynamics_module = sys.modules.get("tnfr.dynamics")
510
+ if dynamics_module is not None:
511
+ executor_cls = getattr(
512
+ dynamics_module, "ProcessPoolExecutor", ProcessPoolExecutor
513
+ )
514
+ with executor_cls(max_workers=n_jobs) as executor:
515
+ for result in executor.map(_param_base_worker, args):
516
+ for node, cand in result:
517
+ base[node] = cand
518
+ return base
519
+
520
+
521
+ def _prepare_selector_preselection(
522
+ G: TNFRGraph,
523
+ selector: GlyphSelector,
524
+ nodes: Sequence[NodeId],
525
+ ) -> _SelectorPreselection | None:
526
+ """Build cached selector metrics when ``selector`` supports them."""
527
+
528
+ if selector is default_glyph_selector:
529
+ return _build_default_preselection(G, nodes)
530
+ if selector is parametric_glyph_selector:
531
+ return _build_param_preselection(G, nodes)
532
+ return None
533
+
534
+
535
+ def _resolve_preselected_glyph(
536
+ G: TNFRGraph,
537
+ n: NodeId,
538
+ selector: GlyphSelector,
539
+ preselection: _SelectorPreselection | None,
540
+ ) -> GlyphCode:
541
+ """Return glyph for ``n`` using ``preselection`` shortcuts when possible."""
542
+
543
+ if preselection is None:
544
+ return selector(G, n)
545
+
546
+ metrics = preselection.metrics.get(n)
547
+ if metrics is None:
548
+ return selector(G, n)
549
+
550
+ if preselection.kind == "default":
551
+ cand = preselection.base_choices.get(n)
552
+ return cand if cand is not None else selector(G, n)
553
+
554
+ if preselection.kind == "param":
555
+ Si, dnfr, accel = metrics
556
+ thresholds = preselection.thresholds or _selector_thresholds(G)
557
+ margin: float | None = preselection.margin
558
+ if margin is None:
559
+ margin = get_graph_param(G, "GLYPH_SELECTOR_MARGIN")
560
+
561
+ cand = preselection.base_choices.get(n)
562
+ if cand is None:
563
+ cand = _selector_base_choice(Si, dnfr, accel, thresholds)
564
+
565
+ nd = G.nodes[n]
566
+ hist_cand = _apply_selector_hysteresis(nd, Si, dnfr, accel, thresholds, margin)
567
+ if hist_cand is not None:
568
+ return hist_cand
569
+
570
+ score = _compute_selector_score(G, nd, Si, dnfr, accel, cand)
571
+ cand = _apply_score_override(cand, score, dnfr, thresholds["dnfr_lo"])
572
+ return _soft_grammar_prefilter(G, n, cand)
573
+
574
+ return selector(G, n)
575
+
576
+
577
+ def _glyph_proposal_worker(
578
+ args: tuple[
579
+ list[NodeId],
580
+ TNFRGraph,
581
+ GlyphSelector,
582
+ _SelectorPreselection | None,
583
+ ],
584
+ ) -> list[tuple[NodeId, GlyphCode]]:
585
+ nodes, G, selector, preselection = args
586
+ return [
587
+ (n, _resolve_preselected_glyph(G, n, selector, preselection)) for n in nodes
588
+ ]
589
+
590
+
591
+ def _apply_glyphs(G: TNFRGraph, selector: GlyphSelector, hist: HistoryState) -> None:
592
+ """Apply glyph decisions across the graph updating hysteresis trackers."""
593
+
594
+ window = int(get_param(G, "GLYPH_HYSTERESIS_WINDOW"))
595
+ use_canon = bool(get_graph_param(G, "GRAMMAR_CANON", dict).get("enabled", False))
596
+ al_max = get_graph_param(G, "AL_MAX_LAG", int)
597
+ en_max = get_graph_param(G, "EN_MAX_LAG", int)
598
+
599
+ nodes_data = list(G.nodes(data=True))
600
+ nodes = [n for n, _ in nodes_data]
601
+ if isinstance(selector, AbstractSelector):
602
+ selector.prepare(G, nodes)
603
+ preselection: _SelectorPreselection | None = None
604
+ else:
605
+ preselection = _prepare_selector_preselection(G, selector, nodes)
606
+
607
+ h_al = hist.setdefault("since_AL", {})
608
+ h_en = hist.setdefault("since_EN", {})
609
+ forced: dict[Any, str | Glyph] = {}
610
+ to_select: list[Any] = []
611
+
612
+ for n, _ in nodes_data:
613
+ h_al[n] = int(h_al.get(n, 0)) + 1
614
+ h_en[n] = int(h_en.get(n, 0)) + 1
615
+
616
+ if h_al[n] > al_max:
617
+ forced[n] = Glyph.AL
618
+ elif h_en[n] > en_max:
619
+ forced[n] = Glyph.EN
620
+ else:
621
+ to_select.append(n)
622
+
623
+ decisions: dict[Any, str | Glyph] = dict(forced)
624
+ forced_al_nodes = {n for n, choice in forced.items() if choice == Glyph.AL}
625
+ forced_en_nodes = {n for n, choice in forced.items() if choice == Glyph.EN}
626
+ if to_select:
627
+ n_jobs = _selector_parallel_jobs(G)
628
+ if n_jobs is None:
629
+ for n in to_select:
630
+ decisions[n] = _resolve_preselected_glyph(G, n, selector, preselection)
631
+ else:
632
+ approx_chunk = math.ceil(len(to_select) / n_jobs) if n_jobs else None
633
+ chunk_size = resolve_chunk_size(
634
+ approx_chunk,
635
+ len(to_select),
636
+ minimum=1,
637
+ )
638
+ chunks = [
639
+ to_select[idx : idx + chunk_size]
640
+ for idx in range(0, len(to_select), chunk_size)
641
+ ]
642
+ dynamics_module = sys.modules.get("tnfr.dynamics")
643
+ executor_cls = ProcessPoolExecutor
644
+ if dynamics_module is not None:
645
+ executor_cls = getattr(
646
+ dynamics_module, "ProcessPoolExecutor", ProcessPoolExecutor
647
+ )
648
+ with executor_cls(max_workers=n_jobs) as executor:
649
+ args_iter = ((chunk, G, selector, preselection) for chunk in chunks)
650
+ for results in executor.map(_glyph_proposal_worker, args_iter):
651
+ for node, glyph in results:
652
+ decisions[node] = glyph
653
+
654
+ for n, _ in nodes_data:
655
+ g = decisions.get(n)
656
+ if g is None:
657
+ continue
658
+
659
+ if use_canon:
660
+ g = enforce_canonical_grammar(G, n, g)
661
+
662
+ apply_glyph(G, n, g, window=window)
663
+ if use_canon:
664
+ on_applied_glyph(G, n, g)
665
+
666
+ if n in forced_al_nodes:
667
+ h_al[n] = 0
668
+ h_en[n] = min(h_en[n], en_max)
669
+ continue
670
+ if n in forced_en_nodes:
671
+ h_en[n] = 0
672
+ continue
673
+
674
+ try:
675
+ glyph_enum = g if isinstance(g, Glyph) else Glyph(str(g))
676
+ except ValueError:
677
+ glyph_enum = None
678
+
679
+ if glyph_enum is Glyph.AL:
680
+ h_al[n] = 0
681
+ h_en[n] = min(h_en[n], en_max)
682
+ elif glyph_enum is Glyph.EN:
683
+ h_en[n] = 0
684
+
685
+
686
+ def _apply_selector(G: TNFRGraph) -> GlyphSelector:
687
+ """Resolve the glyph selector callable configured on ``G``."""
688
+
689
+ raw_selector = G.graph.get("glyph_selector")
690
+
691
+ selector: GlyphSelector
692
+ if isinstance(raw_selector, AbstractSelector):
693
+ selector = raw_selector
694
+ elif isinstance(raw_selector, type) and issubclass(raw_selector, AbstractSelector):
695
+ selector_obj = cast(AbstractSelector, raw_selector())
696
+ G.graph["glyph_selector"] = selector_obj
697
+ selector = selector_obj
698
+ elif raw_selector is None:
699
+ selector = default_glyph_selector
700
+ elif callable(raw_selector):
701
+ selector = cast(GlyphSelector, raw_selector)
702
+ else:
703
+ selector = default_glyph_selector
704
+
705
+ if (
706
+ isinstance(selector, ParametricGlyphSelector)
707
+ or selector is parametric_glyph_selector
708
+ ):
709
+ _selector_norms(G)
710
+ _configure_selector_weights(G)
711
+ return selector