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
@@ -1,41 +1,458 @@
1
- """Sense index helpers."""
1
+ r"""Sense Index computation for TNFR networks.
2
+
3
+ The **Sense Index** (:math:`\text{Si}`) quantifies a node's capacity for stable
4
+ structural reorganization. It blends three structural signals: frequency :math:`\nu_f`
5
+ (reorganization rate), phase coupling :math:`\theta` (network synchrony), and
6
+ reorganization pressure :math:`\Delta\text{NFR}`.
7
+
8
+ Mathematical Foundation
9
+ -----------------------
10
+
11
+ The Sense Index is defined as a weighted combination:
12
+
13
+ .. math::
14
+ \text{Si} = \alpha \cdot \nu_{f,\text{norm}}
15
+ + \beta \cdot (1 - \text{disp}_\theta)
16
+ + \gamma \cdot (1 - |\Delta\text{NFR}|_{\text{norm}})
17
+
18
+ **Component definitions**:
19
+
20
+ 1. **Normalized frequency** :math:`\nu_{f,\text{norm}}`:
21
+
22
+ .. math::
23
+ \nu_{f,\text{norm}} = \frac{|\nu_f|}{\nu_{f,\max}}
24
+
25
+ Measures how fast a node reorganizes relative to network maximum.
26
+ Range: [0, 1] where 1 = maximum reorganization rate.
27
+
28
+ 2. **Phase dispersion** :math:`\text{disp}_\theta`:
29
+
30
+ .. math::
31
+ \text{disp}_\theta = \frac{|\theta - \bar{\theta}|}{\pi}
32
+
33
+ where :math:`\bar{\theta}` is the circular mean of neighbor phases:
34
+
35
+ .. math::
36
+ \bar{\theta} = \text{atan2}\left(\sum_{j \in N(i)} \sin\theta_j, \sum_{j \in N(i)} \cos\theta_j\right)
37
+
38
+ Measures phase misalignment with neighbors.
39
+ Range: [0, 1] where 0 = perfect synchrony, 1 = maximum dispersion.
40
+
41
+ 3. **Normalized reorganization magnitude** :math:`|\Delta\text{NFR}|_{\text{norm}}`:
42
+
43
+ .. math::
44
+ |\Delta\text{NFR}|_{\text{norm}} = \frac{|\Delta\text{NFR}|}{\Delta\text{NFR}_{\max}}
45
+
46
+ Measures structural pressure relative to network maximum.
47
+ Range: [0, 1] where 0 = equilibrium, 1 = maximum pressure.
48
+
49
+ **Structural weights**:
50
+
51
+ - :math:`\alpha`: Frequency weight (default: 0.4) - emphasizes reorganization capacity
52
+ - :math:`\beta`: Phase weight (default: 0.3) - emphasizes network synchrony
53
+ - :math:`\gamma`: ΔNFR weight (default: 0.3) - emphasizes pressure damping
54
+ - Constraint: :math:`\alpha + \beta + \gamma = 1`
55
+
56
+ **Final clamping**: :math:`\text{Si}_{\text{final}} = \max(0, \min(1, \text{Si}))`
57
+
58
+ Physical Interpretation
59
+ ------------------------
60
+
61
+ **High Si (> 0.7)**:
62
+ - Node reorganizes efficiently (:math:`\nu_f` high)
63
+ - Stays synchronized with network (:math:`\text{disp}_\theta` low)
64
+ - Experiences manageable pressure (:math:`|\Delta\text{NFR}|` low)
65
+ - **Implication**: Stable, well-integrated node
66
+
67
+ **Low Si (< 0.3)**:
68
+ - Slow reorganization OR high phase dispersion OR high pressure
69
+ - **Implication**: Risk of structural instability or network decoupling
70
+
71
+ **Moderate Si (0.3-0.7)**:
72
+ - Trade-offs between frequency, synchrony, and pressure
73
+ - **Implication**: Balanced state, monitor for bifurcation
74
+
75
+ Implementation Map
76
+ ------------------
77
+
78
+ **Core Functions**:
79
+
80
+ - :func:`compute_Si` : Network-wide Si computation (vectorized when possible)
81
+ - :func:`compute_Si_node` : Single-node Si calculation
82
+ - :func:`get_Si_weights` : Extract or default Si weights from graph
83
+
84
+ **Helper Functions**:
85
+
86
+ - :func:`_compute_si_python_chunk` : Parallel worker for chunked computation
87
+ - :func:`_SiStructuralCache` : Cache for aligned :math:`\nu_f` and :math:`\Delta\text{NFR}` arrays
88
+
89
+ **Performance**:
90
+
91
+ - Uses NumPy vectorization for networks with >10 nodes
92
+ - Parallel computation for networks with >1000 nodes
93
+ - Trigonometric caching to avoid redundant phase calculations
94
+
95
+ Theoretical References
96
+ ----------------------
97
+
98
+ See the following for complete derivation:
99
+
100
+ - **Mathematical Foundations**: `docs/source/theory/mathematical_foundations.md`
101
+ - **Worked Example**: `docs/source/examples/worked_examples.md` Example 1 (full walkthrough)
102
+ - **Style Guide**: `docs/source/style_guide.md` for notation conventions
103
+
104
+ Examples
105
+ --------
106
+
107
+ **Basic network-wide computation**:
108
+
109
+ >>> import networkx as nx
110
+ >>> from tnfr.metrics.sense_index import compute_Si
111
+ >>> G = nx.Graph()
112
+ >>> G.add_edge("sensor", "relay")
113
+ >>> G.nodes["sensor"].update({"nu_f": 0.9, "delta_nfr": 0.3, "phase": 0.0})
114
+ >>> G.nodes["relay"].update({"nu_f": 0.4, "delta_nfr": 0.05, "phase": 0.1})
115
+ >>> G.graph["SI_WEIGHTS"] = {"alpha": 0.5, "beta": 0.3, "gamma": 0.2}
116
+ >>> result = compute_Si(G, inplace=False)
117
+ >>> round(result["sensor"], 3), round(result["relay"], 3)
118
+ (0.767, 0.857)
119
+
120
+ The heavier :math:`\alpha` weight privileges the sensor's fast :math:`\nu_f` even
121
+ though it suffers larger :math:`\Delta\text{NFR}`. The relay keeps Si high thanks
122
+ to calmer :math:`\Delta\text{NFR}` despite slower frequency.
123
+
124
+ **Single-node computation**:
125
+
126
+ >>> from tnfr.metrics.sense_index import compute_Si_node
127
+ >>> node_attrs = {
128
+ ... "nu_f": 0.8,
129
+ ... "delta_nfr": 0.2,
130
+ ... "phase": 0.5,
131
+ ... "neighbors": [{"phase": 0.4}, {"phase": 0.6}]
132
+ ... }
133
+ >>> Si = compute_Si_node(
134
+ ... "node_id",
135
+ ... node_attrs,
136
+ ... alpha=0.4, beta=0.3, gamma=0.3,
137
+ ... vfmax=1.0, dnfrmax=1.0,
138
+ ... phase_dispersion=0.0, # Already computed
139
+ ... inplace=False
140
+ ... )
141
+ >>> 0.8 < Si < 0.9 # High stability
142
+ True
143
+
144
+ **In-place update**:
145
+
146
+ >>> G = nx.Graph()
147
+ >>> G.add_node("a", nu_f=0.8, delta_nfr=0.2, phase=0.0)
148
+ >>> compute_Si(G, inplace=True) # Writes to G.nodes[n]['Si']
149
+ >>> "Si" in G.nodes["a"]
150
+ True
151
+
152
+ See Also
153
+ --------
154
+
155
+ coherence.compute_coherence : Total network coherence :math:`C(t)`
156
+ coherence.coherence_matrix : Coherence operator approximation :math:`W \approx \hat{C}`
157
+ observers.kuramoto_order : Kuramoto order parameter for phase synchrony
158
+ observers.phase_sync : Phase synchronization metrics
159
+
160
+ Notes
161
+ -----
162
+
163
+ **Sensitivity analysis**:
164
+
165
+ The module can compute partial derivatives :math:`\frac{\partial \text{Si}}{\partial x}`
166
+ for :math:`x \in \{\nu_{f,\text{norm}}, \text{disp}_\theta, |\Delta\text{NFR}|_{\text{norm}}\}`
167
+ when `return_sensitivities=True` is passed to `compute_Si`.
168
+
169
+ **Edge cases**:
170
+
171
+ - If a node has no neighbors, :math:`\bar{\theta} = \theta` (zero dispersion)
172
+ - If :math:`\nu_{f,\max} = 0`, normalization defaults to 0 (frozen network)
173
+ - If :math:`\Delta\text{NFR}_{\max} = 0`, normalization defaults to 0 (equilibrium network)
174
+ """
2
175
 
3
176
  from __future__ import annotations
4
177
 
5
178
  import math
179
+ from concurrent.futures import ProcessPoolExecutor
6
180
  from functools import partial
7
- from typing import Any
181
+ from time import perf_counter
182
+ from typing import Any, Callable, Iterable, Iterator, Mapping, MutableMapping, cast
8
183
 
9
184
  from ..alias import get_attr, set_attr
10
- from ..collections_utils import normalize_weights
11
- from ..constants import get_aliases
12
- from ..cache import edge_version_cache, stable_json
13
- from ..helpers.numeric import angle_diff, clamp01
14
- from .trig import neighbor_phase_mean_list
15
- from ..import_utils import get_numpy
16
- from ..types import GraphLike
17
-
185
+ from ..constants.aliases import ALIAS_DNFR, ALIAS_SI, ALIAS_VF
186
+ from ..utils import angle_diff, angle_diff_array, clamp01
187
+ from ..types import GraphLike, NodeAttrMap
188
+ from ..utils import (
189
+ edge_version_cache,
190
+ get_numpy,
191
+ normalize_weights,
192
+ resolve_chunk_size,
193
+ stable_json,
194
+ )
195
+ from .buffer_cache import ensure_numpy_buffers
18
196
  from .common import (
197
+ _coerce_jobs,
198
+ _get_vf_dnfr_max,
19
199
  ensure_neighbors_map,
20
200
  merge_graph_weights,
21
- _get_vf_dnfr_max,
22
201
  )
202
+ from .trig import neighbor_phase_mean_bulk, neighbor_phase_mean_list
23
203
  from .trig_cache import get_trig_cache
24
204
 
25
- ALIAS_VF = get_aliases("VF")
26
- ALIAS_DNFR = get_aliases("DNFR")
27
- ALIAS_SI = get_aliases("SI")
28
- ALIAS_THETA = get_aliases("THETA")
29
-
205
+ PHASE_DISPERSION_KEY = "dSi_dphase_disp"
206
+ _SI_APPROX_BYTES_PER_NODE = 64
207
+ _VALID_SENSITIVITY_KEYS = frozenset(
208
+ {"dSi_dvf_norm", PHASE_DISPERSION_KEY, "dSi_ddnfr_norm"}
209
+ )
30
210
  __all__ = ("get_Si_weights", "compute_Si_node", "compute_Si")
31
211
 
32
212
 
213
+ class _SiStructuralCache:
214
+ """Cache aligned ``νf`` and ``ΔNFR`` arrays for vectorised Si."""
215
+
216
+ __slots__ = ("node_ids", "vf_values", "dnfr_values", "vf_snapshot", "dnfr_snapshot")
217
+
218
+ def __init__(self, node_ids: tuple[Any, ...]):
219
+ self.node_ids = node_ids
220
+ self.vf_values: Any | None = None
221
+ self.dnfr_values: Any | None = None
222
+ self.vf_snapshot: list[float] = []
223
+ self.dnfr_snapshot: list[float] = []
224
+
225
+ def rebuild(
226
+ self,
227
+ node_ids: Iterable[Any],
228
+ node_data: Mapping[Any, NodeAttrMap],
229
+ *,
230
+ np: Any,
231
+ ) -> tuple[Any, Any]:
232
+ node_tuple = tuple(node_ids)
233
+ count = len(node_tuple)
234
+ if count == 0:
235
+ self.node_ids = node_tuple
236
+ self.vf_values = np.zeros(0, dtype=float)
237
+ self.dnfr_values = np.zeros(0, dtype=float)
238
+ self.vf_snapshot = []
239
+ self.dnfr_snapshot = []
240
+ return self.vf_values, self.dnfr_values
241
+
242
+ vf_arr = np.fromiter(
243
+ (float(get_attr(node_data[n], ALIAS_VF, 0.0)) for n in node_tuple),
244
+ dtype=float,
245
+ count=count,
246
+ )
247
+ dnfr_arr = np.fromiter(
248
+ (float(get_attr(node_data[n], ALIAS_DNFR, 0.0)) for n in node_tuple),
249
+ dtype=float,
250
+ count=count,
251
+ )
252
+
253
+ self.node_ids = node_tuple
254
+ self.vf_values = vf_arr
255
+ self.dnfr_values = dnfr_arr
256
+ self.vf_snapshot = [float(value) for value in vf_arr]
257
+ self.dnfr_snapshot = [float(value) for value in dnfr_arr]
258
+ return self.vf_values, self.dnfr_values
259
+
260
+ def ensure_current(
261
+ self,
262
+ node_ids: Iterable[Any],
263
+ node_data: Mapping[Any, NodeAttrMap],
264
+ *,
265
+ np: Any,
266
+ ) -> tuple[Any, Any]:
267
+ node_tuple = tuple(node_ids)
268
+ if node_tuple != self.node_ids:
269
+ return self.rebuild(node_tuple, node_data, np=np)
270
+
271
+ for idx, node in enumerate(node_tuple):
272
+ nd = node_data[node]
273
+ vf = float(get_attr(nd, ALIAS_VF, 0.0))
274
+ if vf != self.vf_snapshot[idx]:
275
+ return self.rebuild(node_tuple, node_data, np=np)
276
+ dnfr = float(get_attr(nd, ALIAS_DNFR, 0.0))
277
+ if dnfr != self.dnfr_snapshot[idx]:
278
+ return self.rebuild(node_tuple, node_data, np=np)
279
+
280
+ return self.vf_values, self.dnfr_values
281
+
282
+
283
+ def _build_structural_cache(
284
+ node_ids: Iterable[Any],
285
+ node_data: Mapping[Any, NodeAttrMap],
286
+ *,
287
+ np: Any,
288
+ ) -> _SiStructuralCache:
289
+ cache = _SiStructuralCache(tuple(node_ids))
290
+ cache.rebuild(node_ids, node_data, np=np)
291
+ return cache
292
+
293
+
294
+ def _ensure_structural_arrays(
295
+ G: GraphLike,
296
+ node_ids: Iterable[Any],
297
+ node_data: Mapping[Any, NodeAttrMap],
298
+ *,
299
+ np: Any,
300
+ ) -> tuple[Any, Any]:
301
+ node_key = tuple(node_ids)
302
+
303
+ def builder() -> _SiStructuralCache:
304
+ return _build_structural_cache(node_key, node_data, np=np)
305
+
306
+ cache = edge_version_cache(G, ("_si_structural", node_key), builder)
307
+ return cache.ensure_current(node_key, node_data, np=np)
308
+
309
+
310
+ def _ensure_si_buffers(
311
+ G: GraphLike,
312
+ *,
313
+ count: int,
314
+ np: Any,
315
+ ) -> tuple[Any, Any, Any]:
316
+ """Return reusable NumPy buffers sized for ``count`` nodes.
317
+
318
+ Allocates three computation buffers used in Si vectorization:
319
+ 1. phase_dispersion: Phase alignment metric per node
320
+ 2. raw_si: Intermediate Si values before clamping
321
+ 3. si_values: Final Si values after normalization
322
+
323
+ These buffers are reused across computation steps to minimize allocation
324
+ overhead in the hot path. Cache key: ``("_si_buffers", count, 3)``
325
+ """
326
+ return ensure_numpy_buffers(
327
+ G, key_prefix="_si_buffers", count=count, buffer_count=3, np=np
328
+ )
329
+
330
+
331
+ def _ensure_chunk_workspace(
332
+ G: GraphLike,
333
+ *,
334
+ mask_count: int,
335
+ np: Any,
336
+ ) -> tuple[Any, Any]:
337
+ """Return reusable scratch buffers sized to the masked neighbours.
338
+
339
+ Allocates workspace for chunked phase dispersion computation:
340
+ 1. chunk_theta: Theta values for current chunk
341
+ 2. chunk_values: Intermediate values for current chunk
342
+
343
+ Used when processing large neighbor sets in chunks to manage memory.
344
+ Cache key: ``("_si_chunk_workspace", mask_count, 2)``
345
+ """
346
+ return ensure_numpy_buffers(
347
+ G, key_prefix="_si_chunk_workspace", count=mask_count, buffer_count=2, np=np
348
+ )
349
+
350
+
351
+ def _ensure_neighbor_bulk_buffers(
352
+ G: GraphLike,
353
+ *,
354
+ count: int,
355
+ np: Any,
356
+ ) -> tuple[Any, Any, Any, Any, Any]:
357
+ """Return reusable buffers for bulk neighbour phase aggregation.
358
+
359
+ Allocates five buffers for neighbor accumulation in vectorized Si:
360
+ 1. neighbor_cos_sum: Sum of cos(theta) from neighbors
361
+ 2. neighbor_sin_sum: Sum of sin(theta) from neighbors
362
+ 3. neighbor_counts: Number of neighbors per node
363
+ 4. mean_cos_buf: Mean cos(theta) per node
364
+ 5. mean_sin_buf: Mean sin(theta) per node
365
+
366
+ These enable efficient neighbor phase mean computation without Python loops.
367
+ Cache key: ``("_si_neighbor_buffers", count, 5)``
368
+ """
369
+ return ensure_numpy_buffers(
370
+ G, key_prefix="_si_neighbor_buffers", count=count, buffer_count=5, np=np
371
+ )
372
+
373
+
374
+ def _normalise_si_sensitivity_mapping(
375
+ mapping: Mapping[str, float], *, warn: bool
376
+ ) -> dict[str, float]:
377
+ """Preserve structural sensitivities compatible with the Si operator.
378
+
379
+ Parameters
380
+ ----------
381
+ mapping : Mapping[str, float]
382
+ Mapping of raw sensitivity weights keyed by structural derivatives.
383
+ warn : bool
384
+ Compatibility flag kept for trace helpers. It is not used directly but
385
+ retained so upstream logging keeps a consistent signature.
386
+
387
+ Returns
388
+ -------
389
+ dict[str, float]
390
+ Sanitised mapping containing only the supported sensitivity keys.
391
+
392
+ Raises
393
+ ------
394
+ ValueError
395
+ If the mapping defines keys outside of the supported sensitivity set.
396
+
397
+ Examples
398
+ --------
399
+ >>> _normalise_si_sensitivity_mapping({"dSi_dvf_norm": 1.0}, warn=False)
400
+ {'dSi_dvf_norm': 1.0}
401
+ >>> _normalise_si_sensitivity_mapping({"unknown": 1.0}, warn=False)
402
+ Traceback (most recent call last):
403
+ ...
404
+ ValueError: Si sensitivity mappings accept only {dSi_ddnfr_norm, dSi_dphase_disp, dSi_dvf_norm}; unexpected key(s): unknown
405
+ """
406
+
407
+ normalised = dict(mapping)
408
+ _ = warn # kept for API compatibility with trace helpers
409
+ unexpected = sorted(k for k in normalised if k not in _VALID_SENSITIVITY_KEYS)
410
+ if unexpected:
411
+ allowed = ", ".join(sorted(_VALID_SENSITIVITY_KEYS))
412
+ received = ", ".join(unexpected)
413
+ raise ValueError(
414
+ "Si sensitivity mappings accept only {%s}; unexpected key(s): %s"
415
+ % (allowed, received)
416
+ )
417
+ return normalised
418
+
419
+
33
420
  def _cache_weights(G: GraphLike) -> tuple[float, float, float]:
34
- """Normalise and cache Si weights, delegating persistence."""
421
+ """Normalise and persist Si weights attached to the graph coherence.
422
+
423
+ Parameters
424
+ ----------
425
+ G : GraphLike
426
+ Graph structure whose global Si sensitivities must be harmonised.
427
+
428
+ Returns
429
+ -------
430
+ tuple[float, float, float]
431
+ Ordered tuple ``(alpha, beta, gamma)`` with normalised Si weights.
432
+
433
+ Raises
434
+ ------
435
+ ValueError
436
+ Propagated if the graph stores unsupported sensitivity keys.
437
+
438
+ Examples
439
+ --------
440
+ >>> import networkx as nx
441
+ >>> G = nx.Graph()
442
+ >>> G.graph["SI_WEIGHTS"] = {"alpha": 0.2, "beta": 0.5, "gamma": 0.3}
443
+ >>> tuple(round(v, 2) for v in _cache_weights(G))
444
+ (0.2, 0.5, 0.3)
445
+ """
35
446
 
36
447
  w = merge_graph_weights(G, "SI_WEIGHTS")
37
448
  cfg_key = stable_json(w)
38
449
 
450
+ existing = G.graph.get("_Si_sensitivity")
451
+ if isinstance(existing, Mapping):
452
+ migrated = _normalise_si_sensitivity_mapping(existing, warn=True)
453
+ if migrated != existing:
454
+ G.graph["_Si_sensitivity"] = migrated
455
+
39
456
  def builder() -> tuple[float, float, float]:
40
457
  weights = normalize_weights(w, ("alpha", "beta", "gamma"), default=0.0)
41
458
  alpha = weights["alpha"]
@@ -45,7 +462,7 @@ def _cache_weights(G: GraphLike) -> tuple[float, float, float]:
45
462
  G.graph["_Si_weights_key"] = cfg_key
46
463
  G.graph["_Si_sensitivity"] = {
47
464
  "dSi_dvf_norm": alpha,
48
- "dSi_ddisp_fase": -beta,
465
+ PHASE_DISPERSION_KEY: -beta,
49
466
  "dSi_ddnfr_norm": -gamma,
50
467
  }
51
468
  return alpha, beta, gamma
@@ -54,7 +471,25 @@ def _cache_weights(G: GraphLike) -> tuple[float, float, float]:
54
471
 
55
472
 
56
473
  def get_Si_weights(G: GraphLike) -> tuple[float, float, float]:
57
- """Obtain and normalise weights for the sense index."""
474
+ """Expose the normalised Si weights associated with ``G``.
475
+
476
+ Parameters
477
+ ----------
478
+ G : GraphLike
479
+ Graph that carries optional ``SI_WEIGHTS`` metadata.
480
+
481
+ Returns
482
+ -------
483
+ tuple[float, float, float]
484
+ The ``(alpha, beta, gamma)`` weights after normalisation.
485
+
486
+ Examples
487
+ --------
488
+ >>> import networkx as nx
489
+ >>> G = nx.Graph()
490
+ >>> get_Si_weights(G)
491
+ (0.0, 0.0, 0.0)
492
+ """
58
493
 
59
494
  return _cache_weights(G)
60
495
 
@@ -68,10 +503,73 @@ def compute_Si_node(
68
503
  gamma: float,
69
504
  vfmax: float,
70
505
  dnfrmax: float,
71
- disp_fase: float,
506
+ phase_dispersion: float | None = None,
72
507
  inplace: bool,
508
+ **kwargs: Any,
73
509
  ) -> float:
74
- """Compute ``Si`` for a single node."""
510
+ """Evaluate how a node's structure tilts Si within its local resonance.
511
+
512
+ Parameters
513
+ ----------
514
+ n : Any
515
+ Node identifier whose structural perception is computed.
516
+ nd : dict[str, Any]
517
+ Mutable node attributes containing cached structural magnitudes.
518
+ alpha : float
519
+ Normalised weight applied to the node's structural frequency, boosting
520
+ Si when the node reorganises faster than the network baseline.
521
+ beta : float
522
+ Normalised weight applied to the phase alignment term so that tighter
523
+ synchrony raises the index.
524
+ gamma : float
525
+ Normalised weight applied to the ΔNFR attenuation term, rewarding nodes
526
+ that keep internal turbulence under control.
527
+ vfmax : float
528
+ Maximum structural frequency used for normalisation.
529
+ dnfrmax : float
530
+ Maximum |ΔNFR| used for normalisation.
531
+ phase_dispersion : float, optional
532
+ Phase dispersion ratio in ``[0, 1]`` for the node against its
533
+ neighbours. The value must be supplied by the caller.
534
+ inplace : bool
535
+ Whether to write the resulting Si back to ``nd``.
536
+ **kwargs : Any
537
+ Additional keyword arguments are not accepted and will raise.
538
+
539
+ Returns
540
+ -------
541
+ float
542
+ The clamped Si value in ``[0, 1]``.
543
+
544
+ Raises
545
+ ------
546
+ TypeError
547
+ If ``phase_dispersion`` is missing or unsupported keyword arguments
548
+ are provided.
549
+
550
+ Examples
551
+ --------
552
+ >>> nd = {"nu_f": 1.0, "delta_nfr": 0.1}
553
+ >>> compute_Si_node(
554
+ ... "n0",
555
+ ... nd,
556
+ ... alpha=0.4,
557
+ ... beta=0.3,
558
+ ... gamma=0.3,
559
+ ... vfmax=1.0,
560
+ ... dnfrmax=1.0,
561
+ ... phase_dispersion=0.2,
562
+ ... inplace=False,
563
+ ... )
564
+ 0.91
565
+ """
566
+
567
+ if kwargs:
568
+ unexpected = ", ".join(sorted(kwargs))
569
+ raise TypeError(f"Unexpected keyword argument(s): {unexpected}")
570
+
571
+ if phase_dispersion is None:
572
+ raise TypeError("Missing required keyword-only argument: 'phase_dispersion'")
75
573
 
76
574
  vf = get_attr(nd, ALIAS_VF, 0.0)
77
575
  vf_norm = clamp01(abs(vf) / vfmax)
@@ -79,42 +577,627 @@ def compute_Si_node(
79
577
  dnfr = get_attr(nd, ALIAS_DNFR, 0.0)
80
578
  dnfr_norm = clamp01(abs(dnfr) / dnfrmax)
81
579
 
82
- Si = alpha * vf_norm + beta * (1.0 - disp_fase) + gamma * (1.0 - dnfr_norm)
580
+ Si = alpha * vf_norm + beta * (1.0 - phase_dispersion) + gamma * (1.0 - dnfr_norm)
83
581
  Si = clamp01(Si)
84
582
  if inplace:
85
583
  set_attr(nd, ALIAS_SI, Si)
86
584
  return Si
87
585
 
88
586
 
89
- def compute_Si(G: GraphLike, *, inplace: bool = True) -> dict[Any, float]:
90
- """Compute ``Si`` per node and optionally store it on the graph."""
587
+ def _compute_si_python_chunk(
588
+ chunk: Iterable[tuple[Any, tuple[Any, ...], float, float, float]],
589
+ *,
590
+ cos_th: dict[Any, float],
591
+ sin_th: dict[Any, float],
592
+ alpha: float,
593
+ beta: float,
594
+ gamma: float,
595
+ vfmax: float,
596
+ dnfrmax: float,
597
+ ) -> dict[Any, float]:
598
+ """Propagate Si contributions for a node chunk using pure Python.
599
+
600
+ The fallback keeps the νf/phase/ΔNFR balance explicit so that structural
601
+ effects remain traceable even without vectorised support.
602
+
603
+ Parameters
604
+ ----------
605
+ chunk : Iterable[tuple[Any, tuple[Any, ...], float, float, float]]
606
+ Iterable of node payloads ``(node, neighbors, theta, vf, dnfr)``.
607
+ cos_th : dict[Any, float]
608
+ Cached cosine values keyed by node identifiers.
609
+ sin_th : dict[Any, float]
610
+ Cached sine values keyed by node identifiers.
611
+ alpha : float
612
+ Normalised weight for structural frequency.
613
+ beta : float
614
+ Normalised weight for phase dispersion.
615
+ gamma : float
616
+ Normalised weight for ΔNFR dispersion.
617
+ vfmax : float
618
+ Maximum |νf| reference for normalisation.
619
+ dnfrmax : float
620
+ Maximum |ΔNFR| reference for normalisation.
621
+
622
+ Returns
623
+ -------
624
+ dict[Any, float]
625
+ Mapping of node identifiers to their clamped Si values.
626
+
627
+ Examples
628
+ --------
629
+ >>> _compute_si_python_chunk(
630
+ ... [("n0", ("n1",), 0.0, 0.5, 0.1)],
631
+ ... cos_th={"n1": 1.0},
632
+ ... sin_th={"n1": 0.0},
633
+ ... alpha=0.5,
634
+ ... beta=0.3,
635
+ ... gamma=0.2,
636
+ ... vfmax=1.0,
637
+ ... dnfrmax=1.0,
638
+ ... )
639
+ {'n0': 0.73}
640
+ """
641
+
642
+ results: dict[Any, float] = {}
643
+ for n, neigh, theta, vf, dnfr in chunk:
644
+ th_bar = neighbor_phase_mean_list(
645
+ neigh, cos_th=cos_th, sin_th=sin_th, np=None, fallback=theta
646
+ )
647
+ phase_dispersion = abs(angle_diff(theta, th_bar)) / math.pi
648
+ vf_norm = clamp01(abs(vf) / vfmax)
649
+ dnfr_norm = clamp01(abs(dnfr) / dnfrmax)
650
+ Si = (
651
+ alpha * vf_norm
652
+ + beta * (1.0 - phase_dispersion)
653
+ + gamma * (1.0 - dnfr_norm)
654
+ )
655
+ results[n] = clamp01(Si)
656
+ return results
657
+
658
+
659
+ def _iter_python_payload_chunks(
660
+ nodes_data: Iterable[tuple[Any, NodeAttrMap]],
661
+ *,
662
+ neighbors: Mapping[Any, Iterable[Any]],
663
+ thetas: Mapping[Any, float],
664
+ chunk_size: int,
665
+ ) -> Iterator[tuple[tuple[Any, tuple[Any, ...], float, float, float], ...]]:
666
+ """Yield lazily constructed Si payload chunks for the Python fallback.
667
+
668
+ Each batch keeps the structural triad explicit—θ, νf, and ΔNFR—so that the
669
+ downstream worker preserves the coherence balance enforced by the Si
670
+ operator. Streaming prevents a single monolithic buffer that would skew
671
+ memory pressure on dense graphs while still producing deterministic ΔNFR
672
+ sampling. The iterator is consumed lazily by :func:`compute_Si` so that the
673
+ Python fallback can submit and harvest chunk results incrementally, keeping
674
+ both memory usage and profiling telemetry representative of the streamed
675
+ execution.
676
+ """
677
+
678
+ if chunk_size <= 0:
679
+ return
680
+
681
+ buffer: list[tuple[Any, tuple[Any, ...], float, float, float]] = []
682
+ for node, data in nodes_data:
683
+ theta = thetas.get(node, 0.0)
684
+ vf = float(get_attr(data, ALIAS_VF, 0.0))
685
+ dnfr = float(get_attr(data, ALIAS_DNFR, 0.0))
686
+ neigh = tuple(neighbors[node])
687
+ buffer.append((node, neigh, theta, vf, dnfr))
688
+ if len(buffer) >= chunk_size:
689
+ yield tuple(buffer)
690
+ buffer.clear()
691
+
692
+ if buffer:
693
+ yield tuple(buffer)
694
+
695
+
696
+ def compute_Si(
697
+ G: GraphLike,
698
+ *,
699
+ inplace: bool = True,
700
+ n_jobs: int | None = None,
701
+ chunk_size: int | None = None,
702
+ profile: MutableMapping[str, Any] | None = None,
703
+ ) -> dict[Any, float] | Any:
704
+ """Compute the Si metric for each node by integrating structural drivers.
705
+
706
+ Si (sense index) quantifies how effectively a node sustains coherent
707
+ reorganisation within the TNFR triad. The metric aggregates three
708
+ structural contributions: the node's structural frequency (weighted by
709
+ ``alpha``), its phase alignment with neighbours (weighted by ``beta``),
710
+ and the attenuation of disruptive ΔNFR (weighted by ``gamma``). The
711
+ weights therefore bias Si towards faster reorganisation, tighter phase
712
+ coupling, or reduced dissonance respectively, depending on the scenario.
713
+
714
+ Parameters
715
+ ----------
716
+ G : GraphLike
717
+ Graph that exposes ``νf`` (structural frequency), ``ΔNFR`` and phase
718
+ attributes for each node.
719
+ inplace : bool, default: True
720
+ If ``True`` the resulting Si values are written back to ``G``.
721
+ n_jobs : int or None, optional
722
+ Maximum number of worker processes for the pure-Python fallback. Use
723
+ ``None`` to auto-detect the configuration.
724
+ chunk_size : int or None, optional
725
+ Maximum number of nodes processed per batch when building the Si
726
+ mapping. ``None`` derives a safe value from the node count, the
727
+ available CPUs, and conservative memory heuristics. Non-positive values
728
+ fall back to the automatic mode. Graphs may also provide a default via
729
+ ``G.graph["SI_CHUNK_SIZE"]``.
730
+ profile : MutableMapping[str, Any] or None, optional
731
+ Mutable mapping that aggregates wall-clock durations for the internal
732
+ stages of the computation. The mapping receives the keys
733
+ ``"cache_rebuild"``, ``"neighbor_phase_mean_bulk"``,
734
+ ``"normalize_clamp"`` and ``"inplace_write"`` accumulating seconds for
735
+ each step, plus ``"path"`` describing whether the vectorised (NumPy)
736
+ or fallback implementation executed the call. When the Python fallback
737
+ streams chunk execution, ``"fallback_chunks"`` records how many payload
738
+ batches completed. Reusing the mapping across invocations accumulates
739
+ the timings and chunk counts.
740
+
741
+ Returns
742
+ -------
743
+ dict[Any, float] | numpy.ndarray
744
+ Mapping from node identifiers to their Si scores when ``inplace`` is
745
+ ``False``. When ``inplace`` is ``True`` and the NumPy accelerated path
746
+ is available the function updates the graph in place and returns the
747
+ vector of Si values as a :class:`numpy.ndarray`. The pure-Python
748
+ fallback always returns a mapping for compatibility.
749
+
750
+ Raises
751
+ ------
752
+ ValueError
753
+ Propagated if graph-level sensitivity settings include unsupported
754
+ keys or invalid weights.
755
+
756
+ Examples
757
+ --------
758
+ Build a minimal resonance graph with two nodes sharing a phase-locked
759
+ edge. The structural weights bias the result towards phase coherence.
760
+
761
+ >>> import networkx as nx
762
+ >>> from tnfr.metrics.sense_index import compute_Si
763
+ >>> G = nx.Graph()
764
+ >>> G.add_edge("a", "b")
765
+ >>> G.nodes["a"].update({"nu_f": 0.8, "delta_nfr": 0.2, "phase": 0.0})
766
+ >>> G.nodes["b"].update({"nu_f": 0.6, "delta_nfr": 0.1, "phase": 0.1})
767
+ >>> G.graph["SI_WEIGHTS"] = {"alpha": 0.3, "beta": 0.5, "gamma": 0.2}
768
+ >>> {k: round(v, 3) for k, v in compute_Si(G, inplace=False).items()}
769
+ {'a': 0.784, 'b': 0.809}
770
+ """
771
+
772
+ if profile is not None:
773
+ for key in (
774
+ "cache_rebuild",
775
+ "neighbor_phase_mean_bulk",
776
+ "normalize_clamp",
777
+ "inplace_write",
778
+ "fallback_chunks",
779
+ ):
780
+ profile.setdefault(key, 0.0)
781
+
782
+ def _profile_start() -> float:
783
+ return perf_counter()
784
+
785
+ def _profile_stop(key: str, start: float) -> None:
786
+ profile[key] = float(profile.get(key, 0.0)) + (perf_counter() - start)
787
+
788
+ def _profile_mark_path(path: str) -> None:
789
+ profile["path"] = path
790
+
791
+ else:
792
+
793
+ def _profile_start() -> float:
794
+ return 0.0
795
+
796
+ def _profile_stop(key: str, start: float) -> None:
797
+ return None
798
+
799
+ def _profile_mark_path(path: str) -> None:
800
+ return None
91
801
 
92
802
  neighbors = ensure_neighbors_map(G)
93
803
  alpha, beta, gamma = get_Si_weights(G)
94
- vfmax, dnfrmax = _get_vf_dnfr_max(G)
95
-
96
804
  np = get_numpy()
97
805
  trig = get_trig_cache(G, np=np)
98
806
  cos_th, sin_th, thetas = trig.cos, trig.sin, trig.theta
99
807
 
100
- pm_fn = partial(
101
- neighbor_phase_mean_list, cos_th=cos_th, sin_th=sin_th, np=np
808
+ pm_fn = partial(neighbor_phase_mean_list, cos_th=cos_th, sin_th=sin_th, np=np)
809
+
810
+ if n_jobs is None:
811
+ n_jobs = _coerce_jobs(G.graph.get("SI_N_JOBS"))
812
+ else:
813
+ n_jobs = _coerce_jobs(n_jobs)
814
+
815
+ supports_vector = (
816
+ np is not None
817
+ and hasattr(np, "ndarray")
818
+ and all(
819
+ hasattr(np, attr)
820
+ for attr in (
821
+ "fromiter",
822
+ "abs",
823
+ "clip",
824
+ "remainder",
825
+ "zeros",
826
+ "add",
827
+ "bincount",
828
+ "arctan2",
829
+ "where",
830
+ "divide",
831
+ "errstate",
832
+ "max",
833
+ )
834
+ )
102
835
  )
103
836
 
104
- out: dict[Any, float] = {}
105
- for n, nd in G.nodes(data=True):
106
- neigh = neighbors[n]
107
- th_bar = pm_fn(neigh, fallback=thetas[n])
108
- disp_fase = abs(angle_diff(thetas[n], th_bar)) / math.pi
109
- out[n] = compute_Si_node(
110
- n,
111
- nd,
112
- alpha=alpha,
113
- beta=beta,
114
- gamma=gamma,
115
- vfmax=vfmax,
116
- dnfrmax=dnfrmax,
117
- disp_fase=disp_fase,
118
- inplace=inplace,
837
+ nodes_view = G.nodes
838
+ nodes_data = list(nodes_view(data=True))
839
+ if not nodes_data:
840
+ return {}
841
+
842
+ node_mapping = cast(Mapping[Any, NodeAttrMap], nodes_view)
843
+ node_count = len(nodes_data)
844
+
845
+ trig_order = list(getattr(trig, "order", ()))
846
+ node_ids: list[Any]
847
+ node_idx: dict[Any, int]
848
+ using_cache_order = False
849
+ if trig_order and len(trig_order) == node_count:
850
+ node_ids = trig_order
851
+ node_idx = dict(getattr(trig, "index", {}))
852
+ using_cache_order = len(node_idx) == len(node_ids)
853
+ if not using_cache_order:
854
+ node_idx = {n: i for i, n in enumerate(node_ids)}
855
+ else:
856
+ node_ids = [n for n, _ in nodes_data]
857
+ node_idx = {n: i for i, n in enumerate(node_ids)}
858
+
859
+ chunk_pref = chunk_size if chunk_size is not None else G.graph.get("SI_CHUNK_SIZE")
860
+
861
+ if supports_vector:
862
+ _profile_mark_path("vectorized")
863
+ node_key = tuple(node_ids)
864
+ count = len(node_key)
865
+
866
+ cache_theta = getattr(trig, "theta_values", None)
867
+ cache_cos = getattr(trig, "cos_values", None)
868
+ cache_sin = getattr(trig, "sin_values", None)
869
+
870
+ trig_index_map = dict(getattr(trig, "index", {}) or {})
871
+ index_arr: Any | None = None
872
+ cached_mask = None
873
+ if trig_index_map and count:
874
+ index_values: list[int] = []
875
+ mask_values: list[bool] = []
876
+ for node in node_ids:
877
+ cached_idx = trig_index_map.get(node)
878
+ if cached_idx is None:
879
+ index_values.append(-1)
880
+ mask_values.append(False)
881
+ else:
882
+ index_values.append(int(cached_idx))
883
+ mask_values.append(True)
884
+ cached_mask = np.asarray(mask_values, dtype=bool)
885
+ if cached_mask.any():
886
+ index_arr = np.asarray(index_values, dtype=np.intp)
887
+ if cached_mask is None:
888
+ cached_mask = np.zeros(count, dtype=bool)
889
+
890
+ def _gather_values(
891
+ cache_values: Any | None, fallback_getter: Callable[[Any], float]
892
+ ) -> Any:
893
+ if (
894
+ index_arr is not None
895
+ and cache_values is not None
896
+ and cached_mask.size
897
+ and cached_mask.any()
898
+ ):
899
+ out = np.empty(count, dtype=float)
900
+ cached_indices = np.nonzero(cached_mask)[0]
901
+ if cached_indices.size:
902
+ out[cached_indices] = np.take(
903
+ np.asarray(cache_values, dtype=float), index_arr[cached_indices]
904
+ )
905
+ missing_indices = np.nonzero(~cached_mask)[0]
906
+ if missing_indices.size:
907
+ missing_nodes = [node_ids[i] for i in missing_indices]
908
+ out[missing_indices] = np.fromiter(
909
+ (fallback_getter(node) for node in missing_nodes),
910
+ dtype=float,
911
+ count=missing_indices.size,
912
+ )
913
+ return out
914
+ return np.fromiter(
915
+ (fallback_getter(node) for node in node_ids),
916
+ dtype=float,
917
+ count=count,
918
+ )
919
+
920
+ cache_timer = _profile_start()
921
+
922
+ if using_cache_order and cache_theta is not None:
923
+ theta_arr = np.asarray(cache_theta, dtype=float)
924
+ else:
925
+ theta_arr = _gather_values(cache_theta, lambda node: thetas.get(node, 0.0))
926
+
927
+ if using_cache_order and cache_cos is not None:
928
+ cos_arr = np.asarray(cache_cos, dtype=float)
929
+ else:
930
+ cos_arr = _gather_values(
931
+ cache_cos,
932
+ lambda node: cos_th.get(node, math.cos(thetas.get(node, 0.0))),
933
+ )
934
+
935
+ if using_cache_order and cache_sin is not None:
936
+ sin_arr = np.asarray(cache_sin, dtype=float)
937
+ else:
938
+ sin_arr = _gather_values(
939
+ cache_sin,
940
+ lambda node: sin_th.get(node, math.sin(thetas.get(node, 0.0))),
941
+ )
942
+
943
+ cached_edge_src = None
944
+ cached_edge_dst = None
945
+ if using_cache_order:
946
+ cached_edge_src = getattr(trig, "edge_src", None)
947
+ cached_edge_dst = getattr(trig, "edge_dst", None)
948
+ if cached_edge_src is not None and cached_edge_dst is not None:
949
+ cached_edge_src = np.asarray(cached_edge_src, dtype=np.intp)
950
+ cached_edge_dst = np.asarray(cached_edge_dst, dtype=np.intp)
951
+ if cached_edge_src.shape != cached_edge_dst.shape:
952
+ cached_edge_src = None
953
+ cached_edge_dst = None
954
+
955
+ if cached_edge_src is not None and cached_edge_dst is not None:
956
+ edge_src = cached_edge_src
957
+ edge_dst = cached_edge_dst
958
+ else:
959
+
960
+ def _build_edge_arrays() -> tuple[Any, Any]:
961
+ edge_src_list: list[int] = []
962
+ edge_dst_list: list[int] = []
963
+ for node in node_ids:
964
+ dst_idx = node_idx[node]
965
+ for neighbor in neighbors[node]:
966
+ src_idx = node_idx.get(neighbor)
967
+ if src_idx is None:
968
+ continue
969
+ edge_src_list.append(src_idx)
970
+ edge_dst_list.append(dst_idx)
971
+ src_arr = np.asarray(edge_src_list, dtype=np.intp)
972
+ dst_arr = np.asarray(edge_dst_list, dtype=np.intp)
973
+ return src_arr, dst_arr
974
+
975
+ edge_src, edge_dst = edge_version_cache(
976
+ G,
977
+ ("_si_edges", node_key),
978
+ _build_edge_arrays,
979
+ )
980
+ if using_cache_order:
981
+ trig.edge_src = edge_src
982
+ trig.edge_dst = edge_dst
983
+
984
+ (
985
+ neighbor_cos_sum,
986
+ neighbor_sin_sum,
987
+ neighbor_counts,
988
+ mean_cos_buf,
989
+ mean_sin_buf,
990
+ ) = _ensure_neighbor_bulk_buffers(
991
+ G,
992
+ count=count,
993
+ np=np,
994
+ )
995
+
996
+ vf_arr, dnfr_arr = _ensure_structural_arrays(
997
+ G,
998
+ node_ids,
999
+ node_mapping,
1000
+ np=np,
1001
+ )
1002
+ raw_vfmax = float(np.max(np.abs(vf_arr))) if getattr(vf_arr, "size", 0) else 0.0
1003
+ raw_dnfrmax = (
1004
+ float(np.max(np.abs(dnfr_arr))) if getattr(dnfr_arr, "size", 0) else 0.0
1005
+ )
1006
+ G.graph["_vfmax"] = raw_vfmax
1007
+ G.graph["_dnfrmax"] = raw_dnfrmax
1008
+ vfmax = 1.0 if raw_vfmax == 0.0 else raw_vfmax
1009
+ dnfrmax = 1.0 if raw_dnfrmax == 0.0 else raw_dnfrmax
1010
+
1011
+ (
1012
+ phase_dispersion,
1013
+ raw_si,
1014
+ si_values,
1015
+ ) = _ensure_si_buffers(
1016
+ G,
1017
+ count=count,
1018
+ np=np,
1019
+ )
1020
+
1021
+ _profile_stop("cache_rebuild", cache_timer)
1022
+
1023
+ neighbor_timer = _profile_start()
1024
+ mean_theta, has_neighbors = neighbor_phase_mean_bulk(
1025
+ edge_src,
1026
+ edge_dst,
1027
+ cos_values=cos_arr,
1028
+ sin_values=sin_arr,
1029
+ theta_values=theta_arr,
1030
+ node_count=count,
1031
+ np=np,
1032
+ neighbor_cos_sum=neighbor_cos_sum,
1033
+ neighbor_sin_sum=neighbor_sin_sum,
1034
+ neighbor_counts=neighbor_counts,
1035
+ mean_cos=mean_cos_buf,
1036
+ mean_sin=mean_sin_buf,
119
1037
  )
1038
+ _profile_stop("neighbor_phase_mean_bulk", neighbor_timer)
1039
+ norm_timer = _profile_start()
1040
+ # Reuse the Si buffers as scratch space to avoid transient allocations during
1041
+ # the normalization pass and keep the structural buffers coherent with the
1042
+ # cached layout.
1043
+ np.abs(vf_arr, out=raw_si)
1044
+ np.divide(raw_si, vfmax, out=raw_si)
1045
+ np.clip(raw_si, 0.0, 1.0, out=raw_si)
1046
+ vf_norm = raw_si
1047
+ np.abs(dnfr_arr, out=si_values)
1048
+ np.divide(si_values, dnfrmax, out=si_values)
1049
+ np.clip(si_values, 0.0, 1.0, out=si_values)
1050
+ dnfr_norm = si_values
1051
+ phase_dispersion.fill(0.0)
1052
+ neighbor_mask = np.asarray(has_neighbors, dtype=bool)
1053
+ neighbor_count = int(neighbor_mask.sum())
1054
+ use_chunked = False
1055
+ if neighbor_count:
1056
+ effective_chunk = resolve_chunk_size(
1057
+ chunk_pref,
1058
+ neighbor_count,
1059
+ approx_bytes_per_item=_SI_APPROX_BYTES_PER_NODE,
1060
+ )
1061
+ if effective_chunk <= 0 or effective_chunk >= neighbor_count:
1062
+ effective_chunk = neighbor_count
1063
+ else:
1064
+ use_chunked = True
1065
+
1066
+ if neighbor_count and not use_chunked:
1067
+ angle_diff_array(
1068
+ theta_arr,
1069
+ mean_theta,
1070
+ np=np,
1071
+ out=phase_dispersion,
1072
+ where=neighbor_mask,
1073
+ )
1074
+ np.abs(phase_dispersion, out=phase_dispersion, where=neighbor_mask)
1075
+ np.divide(
1076
+ phase_dispersion,
1077
+ math.pi,
1078
+ out=phase_dispersion,
1079
+ where=neighbor_mask,
1080
+ )
1081
+ elif neighbor_count and use_chunked:
1082
+ neighbor_indices = np.nonzero(neighbor_mask)[0]
1083
+ chunk_theta, chunk_values = _ensure_chunk_workspace(
1084
+ G,
1085
+ mask_count=neighbor_count,
1086
+ np=np,
1087
+ )
1088
+ for start in range(0, neighbor_count, effective_chunk):
1089
+ end = min(start + effective_chunk, neighbor_count)
1090
+ slice_indices = neighbor_indices[start:end]
1091
+ chunk_len = end - start
1092
+ theta_view = chunk_theta[:chunk_len]
1093
+ values_view = chunk_values[:chunk_len]
1094
+ np.take(theta_arr, slice_indices, out=theta_view)
1095
+ np.take(mean_theta, slice_indices, out=values_view)
1096
+ angle_diff_array(theta_view, values_view, np=np, out=values_view)
1097
+ np.abs(values_view, out=values_view)
1098
+ np.divide(values_view, math.pi, out=values_view)
1099
+ phase_dispersion[slice_indices] = values_view
1100
+ else:
1101
+ np.abs(phase_dispersion, out=phase_dispersion)
1102
+ np.divide(
1103
+ phase_dispersion,
1104
+ math.pi,
1105
+ out=phase_dispersion,
1106
+ where=neighbor_mask,
1107
+ )
1108
+
1109
+ np.multiply(vf_norm, alpha, out=raw_si)
1110
+ np.subtract(1.0, phase_dispersion, out=phase_dispersion)
1111
+ np.multiply(phase_dispersion, beta, out=phase_dispersion)
1112
+ np.add(raw_si, phase_dispersion, out=raw_si)
1113
+ np.subtract(1.0, dnfr_norm, out=si_values)
1114
+ np.multiply(si_values, gamma, out=si_values)
1115
+ np.add(raw_si, si_values, out=raw_si)
1116
+ np.clip(raw_si, 0.0, 1.0, out=si_values)
1117
+
1118
+ _profile_stop("normalize_clamp", norm_timer)
1119
+
1120
+ if inplace:
1121
+ write_timer = _profile_start()
1122
+ for idx, node in enumerate(node_ids):
1123
+ set_attr(G.nodes[node], ALIAS_SI, float(si_values[idx]))
1124
+ _profile_stop("inplace_write", write_timer)
1125
+ return np.copy(si_values)
1126
+
1127
+ return {node: float(value) for node, value in zip(node_ids, si_values)}
1128
+
1129
+ vfmax, dnfrmax = _get_vf_dnfr_max(G)
1130
+
1131
+ out: dict[Any, float] = {}
1132
+ _profile_mark_path("fallback")
1133
+ if n_jobs is not None and n_jobs > 1:
1134
+ node_count = len(nodes_data)
1135
+ if node_count:
1136
+ effective_chunk = resolve_chunk_size(
1137
+ chunk_pref,
1138
+ node_count,
1139
+ approx_bytes_per_item=_SI_APPROX_BYTES_PER_NODE,
1140
+ )
1141
+ if effective_chunk <= 0:
1142
+ effective_chunk = node_count
1143
+ payload_chunks = _iter_python_payload_chunks(
1144
+ nodes_data,
1145
+ neighbors=neighbors,
1146
+ thetas=thetas,
1147
+ chunk_size=effective_chunk,
1148
+ )
1149
+ chunk_count = 0
1150
+ with ProcessPoolExecutor(max_workers=n_jobs) as executor:
1151
+ worker = partial(
1152
+ _compute_si_python_chunk,
1153
+ cos_th=cos_th,
1154
+ sin_th=sin_th,
1155
+ alpha=alpha,
1156
+ beta=beta,
1157
+ gamma=gamma,
1158
+ vfmax=vfmax,
1159
+ dnfrmax=dnfrmax,
1160
+ )
1161
+ payload_iter = iter(payload_chunks)
1162
+ futures: list[Any] = []
1163
+ for chunk in payload_iter:
1164
+ futures.append(executor.submit(worker, chunk))
1165
+ if len(futures) >= n_jobs:
1166
+ future = futures.pop(0)
1167
+ chunk_result = future.result()
1168
+ chunk_count += 1
1169
+ out.update(chunk_result)
1170
+ for future in futures:
1171
+ chunk_result = future.result()
1172
+ chunk_count += 1
1173
+ out.update(chunk_result)
1174
+ if profile is not None:
1175
+ profile["fallback_chunks"] = float(
1176
+ profile.get("fallback_chunks", 0.0)
1177
+ ) + float(chunk_count)
1178
+ else:
1179
+ for n, nd in nodes_data:
1180
+ theta = thetas.get(n, 0.0)
1181
+ neigh = neighbors[n]
1182
+ th_bar = pm_fn(neigh, fallback=theta)
1183
+ phase_dispersion = abs(angle_diff(theta, th_bar)) / math.pi
1184
+ norm_timer = _profile_start()
1185
+ out[n] = compute_Si_node(
1186
+ n,
1187
+ nd,
1188
+ alpha=alpha,
1189
+ beta=beta,
1190
+ gamma=gamma,
1191
+ vfmax=vfmax,
1192
+ dnfrmax=dnfrmax,
1193
+ phase_dispersion=phase_dispersion,
1194
+ inplace=False,
1195
+ )
1196
+ _profile_stop("normalize_clamp", norm_timer)
1197
+
1198
+ if inplace:
1199
+ write_timer = _profile_start()
1200
+ for n, value in out.items():
1201
+ set_attr(G.nodes[n], ALIAS_SI, value)
1202
+ _profile_stop("inplace_write", write_timer)
120
1203
  return out