tnfr 4.5.1__py3-none-any.whl → 6.0.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.
Files changed (170) hide show
  1. tnfr/__init__.py +270 -90
  2. tnfr/__init__.pyi +40 -0
  3. tnfr/_compat.py +11 -0
  4. tnfr/_version.py +7 -0
  5. tnfr/_version.pyi +7 -0
  6. tnfr/alias.py +631 -0
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +732 -0
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +381 -0
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +89 -0
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +199 -0
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +322 -0
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +34 -0
  19. tnfr/cli/utils.pyi +8 -0
  20. tnfr/config/__init__.py +12 -0
  21. tnfr/config/__init__.pyi +8 -0
  22. tnfr/config/constants.py +104 -0
  23. tnfr/config/constants.pyi +12 -0
  24. tnfr/config/init.py +36 -0
  25. tnfr/config/init.pyi +8 -0
  26. tnfr/config/operator_names.py +106 -0
  27. tnfr/config/operator_names.pyi +28 -0
  28. tnfr/config/presets.py +104 -0
  29. tnfr/config/presets.pyi +7 -0
  30. tnfr/constants/__init__.py +228 -0
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +158 -0
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.py +31 -0
  35. tnfr/constants/init.pyi +12 -0
  36. tnfr/constants/metric.py +102 -0
  37. tnfr/constants/metric.pyi +19 -0
  38. tnfr/constants_glyphs.py +16 -0
  39. tnfr/constants_glyphs.pyi +12 -0
  40. tnfr/dynamics/__init__.py +136 -0
  41. tnfr/dynamics/__init__.pyi +83 -0
  42. tnfr/dynamics/adaptation.py +201 -0
  43. tnfr/dynamics/aliases.py +22 -0
  44. tnfr/dynamics/coordination.py +343 -0
  45. tnfr/dynamics/dnfr.py +2315 -0
  46. tnfr/dynamics/dnfr.pyi +33 -0
  47. tnfr/dynamics/integrators.py +561 -0
  48. tnfr/dynamics/integrators.pyi +35 -0
  49. tnfr/dynamics/runtime.py +521 -0
  50. tnfr/dynamics/sampling.py +34 -0
  51. tnfr/dynamics/sampling.pyi +7 -0
  52. tnfr/dynamics/selectors.py +680 -0
  53. tnfr/execution.py +216 -0
  54. tnfr/execution.pyi +65 -0
  55. tnfr/flatten.py +283 -0
  56. tnfr/flatten.pyi +28 -0
  57. tnfr/gamma.py +320 -89
  58. tnfr/gamma.pyi +40 -0
  59. tnfr/glyph_history.py +337 -0
  60. tnfr/glyph_history.pyi +53 -0
  61. tnfr/grammar.py +23 -153
  62. tnfr/grammar.pyi +13 -0
  63. tnfr/helpers/__init__.py +151 -0
  64. tnfr/helpers/__init__.pyi +66 -0
  65. tnfr/helpers/numeric.py +88 -0
  66. tnfr/helpers/numeric.pyi +12 -0
  67. tnfr/immutable.py +214 -0
  68. tnfr/immutable.pyi +37 -0
  69. tnfr/initialization.py +199 -0
  70. tnfr/initialization.pyi +73 -0
  71. tnfr/io.py +311 -0
  72. tnfr/io.pyi +11 -0
  73. tnfr/locking.py +37 -0
  74. tnfr/locking.pyi +7 -0
  75. tnfr/metrics/__init__.py +41 -0
  76. tnfr/metrics/__init__.pyi +20 -0
  77. tnfr/metrics/coherence.py +1469 -0
  78. tnfr/metrics/common.py +149 -0
  79. tnfr/metrics/common.pyi +15 -0
  80. tnfr/metrics/core.py +259 -0
  81. tnfr/metrics/core.pyi +13 -0
  82. tnfr/metrics/diagnosis.py +840 -0
  83. tnfr/metrics/diagnosis.pyi +89 -0
  84. tnfr/metrics/export.py +151 -0
  85. tnfr/metrics/glyph_timing.py +369 -0
  86. tnfr/metrics/reporting.py +152 -0
  87. tnfr/metrics/reporting.pyi +12 -0
  88. tnfr/metrics/sense_index.py +294 -0
  89. tnfr/metrics/sense_index.pyi +9 -0
  90. tnfr/metrics/trig.py +216 -0
  91. tnfr/metrics/trig.pyi +12 -0
  92. tnfr/metrics/trig_cache.py +105 -0
  93. tnfr/metrics/trig_cache.pyi +10 -0
  94. tnfr/node.py +255 -177
  95. tnfr/node.pyi +161 -0
  96. tnfr/observers.py +154 -150
  97. tnfr/observers.pyi +46 -0
  98. tnfr/ontosim.py +135 -134
  99. tnfr/ontosim.pyi +33 -0
  100. tnfr/operators/__init__.py +452 -0
  101. tnfr/operators/__init__.pyi +31 -0
  102. tnfr/operators/definitions.py +181 -0
  103. tnfr/operators/definitions.pyi +92 -0
  104. tnfr/operators/jitter.py +266 -0
  105. tnfr/operators/jitter.pyi +11 -0
  106. tnfr/operators/registry.py +80 -0
  107. tnfr/operators/registry.pyi +15 -0
  108. tnfr/operators/remesh.py +569 -0
  109. tnfr/presets.py +10 -23
  110. tnfr/presets.pyi +7 -0
  111. tnfr/py.typed +0 -0
  112. tnfr/rng.py +440 -0
  113. tnfr/rng.pyi +14 -0
  114. tnfr/selector.py +217 -0
  115. tnfr/selector.pyi +19 -0
  116. tnfr/sense.py +307 -142
  117. tnfr/sense.pyi +30 -0
  118. tnfr/structural.py +69 -164
  119. tnfr/structural.pyi +46 -0
  120. tnfr/telemetry/__init__.py +13 -0
  121. tnfr/telemetry/verbosity.py +37 -0
  122. tnfr/tokens.py +61 -0
  123. tnfr/tokens.pyi +41 -0
  124. tnfr/trace.py +520 -95
  125. tnfr/trace.pyi +68 -0
  126. tnfr/types.py +382 -17
  127. tnfr/types.pyi +145 -0
  128. tnfr/utils/__init__.py +158 -0
  129. tnfr/utils/__init__.pyi +133 -0
  130. tnfr/utils/cache.py +755 -0
  131. tnfr/utils/cache.pyi +156 -0
  132. tnfr/utils/data.py +267 -0
  133. tnfr/utils/data.pyi +73 -0
  134. tnfr/utils/graph.py +87 -0
  135. tnfr/utils/graph.pyi +10 -0
  136. tnfr/utils/init.py +746 -0
  137. tnfr/utils/init.pyi +85 -0
  138. tnfr/utils/io.py +157 -0
  139. tnfr/utils/io.pyi +10 -0
  140. tnfr/utils/validators.py +130 -0
  141. tnfr/utils/validators.pyi +19 -0
  142. tnfr/validation/__init__.py +25 -0
  143. tnfr/validation/__init__.pyi +17 -0
  144. tnfr/validation/compatibility.py +59 -0
  145. tnfr/validation/compatibility.pyi +8 -0
  146. tnfr/validation/grammar.py +149 -0
  147. tnfr/validation/grammar.pyi +11 -0
  148. tnfr/validation/rules.py +194 -0
  149. tnfr/validation/rules.pyi +18 -0
  150. tnfr/validation/syntax.py +151 -0
  151. tnfr/validation/syntax.pyi +7 -0
  152. tnfr-6.0.0.dist-info/METADATA +135 -0
  153. tnfr-6.0.0.dist-info/RECORD +157 -0
  154. tnfr/cli.py +0 -322
  155. tnfr/config.py +0 -41
  156. tnfr/constants.py +0 -277
  157. tnfr/dynamics.py +0 -814
  158. tnfr/helpers.py +0 -264
  159. tnfr/main.py +0 -47
  160. tnfr/metrics.py +0 -597
  161. tnfr/operators.py +0 -525
  162. tnfr/program.py +0 -176
  163. tnfr/scenarios.py +0 -34
  164. tnfr/validators.py +0 -38
  165. tnfr-4.5.1.dist-info/METADATA +0 -221
  166. tnfr-4.5.1.dist-info/RECORD +0 -28
  167. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  168. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  169. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/dynamics/dnfr.pyi ADDED
@@ -0,0 +1,33 @@
1
+ from typing import Any
2
+
3
+ from tnfr.types import DeltaNFRHook, TNFRGraph
4
+
5
+ __all__: tuple[str, ...]
6
+
7
+ def default_compute_delta_nfr(
8
+ G: TNFRGraph,
9
+ *,
10
+ cache_size: int | None = ...,
11
+ n_jobs: int | None = ...,
12
+ ) -> None: ...
13
+
14
+
15
+ def dnfr_epi_vf_mixed(G: TNFRGraph, *, n_jobs: int | None = ...) -> None: ...
16
+
17
+
18
+ def dnfr_laplacian(G: TNFRGraph, *, n_jobs: int | None = ...) -> None: ...
19
+
20
+
21
+ def dnfr_phase_only(G: TNFRGraph, *, n_jobs: int | None = ...) -> None: ...
22
+
23
+
24
+ def set_delta_nfr_hook(
25
+ G: TNFRGraph,
26
+ func: DeltaNFRHook,
27
+ *,
28
+ name: str | None = ...,
29
+ note: str | None = ...,
30
+ ) -> None: ...
31
+
32
+
33
+ def __getattr__(name: str) -> Any: ...
@@ -0,0 +1,561 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Iterable, Mapping
6
+ from concurrent.futures import ProcessPoolExecutor
7
+ from multiprocessing import get_context
8
+ from typing import Any, Literal, cast
9
+
10
+ import networkx as nx
11
+
12
+ from ..constants import (
13
+ DEFAULTS,
14
+ get_aliases,
15
+ )
16
+ from ..gamma import _get_gamma_spec, eval_gamma
17
+ from ..alias import collect_attr, get_attr, get_attr_str, set_attr, set_attr_str
18
+ from ..utils import get_numpy
19
+ from ..types import NodeId, TNFRGraph
20
+ from .._compat import TypeAlias
21
+
22
+ ALIAS_VF = get_aliases("VF")
23
+ ALIAS_DNFR = get_aliases("DNFR")
24
+ ALIAS_DEPI = get_aliases("DEPI")
25
+ ALIAS_EPI = get_aliases("EPI")
26
+ ALIAS_EPI_KIND = get_aliases("EPI_KIND")
27
+ ALIAS_D2EPI = get_aliases("D2EPI")
28
+
29
+ __all__ = (
30
+ "AbstractIntegrator",
31
+ "DefaultIntegrator",
32
+ "prepare_integration_params",
33
+ "update_epi_via_nodal_equation",
34
+ )
35
+
36
+
37
+ GammaMap: TypeAlias = dict[NodeId, float]
38
+ """Γ evaluation cache keyed by node identifier."""
39
+
40
+ NodeIncrements: TypeAlias = dict[NodeId, tuple[float, ...]]
41
+ """Mapping of nodes to staged integration increments."""
42
+
43
+ NodalUpdate: TypeAlias = dict[NodeId, tuple[float, float, float]]
44
+ """Mapping of nodes to ``(EPI, dEPI/dt, ∂²EPI/∂t²)`` tuples."""
45
+
46
+ IntegratorMethod: TypeAlias = Literal["euler", "rk4"]
47
+ """Supported explicit integration schemes for nodal updates."""
48
+
49
+
50
+ _PARALLEL_GRAPH: TNFRGraph | None = None
51
+
52
+
53
+ def _gamma_worker_init(graph: TNFRGraph) -> None:
54
+ """Initialise process-local graph reference for Γ evaluation."""
55
+
56
+ global _PARALLEL_GRAPH
57
+ _PARALLEL_GRAPH = graph
58
+
59
+
60
+ def _gamma_worker(task: tuple[list[NodeId], float]) -> list[tuple[NodeId, float]]:
61
+ """Evaluate Γ for ``task`` chunk using process-local graph."""
62
+
63
+ chunk, t = task
64
+ if _PARALLEL_GRAPH is None:
65
+ raise RuntimeError("Parallel Γ worker initialised without graph reference")
66
+ return [
67
+ (node, float(eval_gamma(_PARALLEL_GRAPH, node, t))) for node in chunk
68
+ ]
69
+
70
+
71
+ def _normalise_jobs(n_jobs: int | None, total: int) -> int | None:
72
+ """Return an effective worker count respecting serial fallbacks."""
73
+
74
+ if n_jobs is None:
75
+ return None
76
+ try:
77
+ workers = int(n_jobs)
78
+ except (TypeError, ValueError):
79
+ return None
80
+ if workers <= 1 or total <= 1:
81
+ return None
82
+ return max(1, min(workers, total))
83
+
84
+
85
+ def _chunk_nodes(nodes: list[NodeId], chunk_size: int) -> Iterable[list[NodeId]]:
86
+ """Yield deterministic chunks from ``nodes`` respecting insertion order."""
87
+
88
+ for idx in range(0, len(nodes), chunk_size):
89
+ yield nodes[idx:idx + chunk_size]
90
+
91
+
92
+ def _apply_increment_chunk(
93
+ chunk: list[tuple[NodeId, float, float, tuple[float, ...]]],
94
+ dt_step: float,
95
+ method: str,
96
+ ) -> list[tuple[NodeId, tuple[float, float, float]]]:
97
+ """Compute updated states for ``chunk`` using scalar arithmetic."""
98
+
99
+ results: list[tuple[NodeId, tuple[float, float, float]]] = []
100
+ dt_nonzero = dt_step != 0
101
+
102
+ for node, epi_i, dEPI_prev, ks in chunk:
103
+ if method == "rk4":
104
+ k1, k2, k3, k4 = ks
105
+ epi = epi_i + (dt_step / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
106
+ dEPI_dt = k4
107
+ else:
108
+ (k1,) = ks
109
+ epi = epi_i + dt_step * k1
110
+ dEPI_dt = k1
111
+ d2epi = (dEPI_dt - dEPI_prev) / dt_step if dt_nonzero else 0.0
112
+ results.append((node, (float(epi), float(dEPI_dt), float(d2epi))))
113
+
114
+ return results
115
+
116
+
117
+ def _evaluate_gamma_map(
118
+ G: TNFRGraph,
119
+ nodes: list[NodeId],
120
+ t: float,
121
+ *,
122
+ n_jobs: int | None = None,
123
+ ) -> GammaMap:
124
+ """Return Γ evaluations for ``nodes`` at time ``t`` respecting parallelism."""
125
+
126
+ workers = _normalise_jobs(n_jobs, len(nodes))
127
+ if workers is None:
128
+ return {n: float(eval_gamma(G, n, t)) for n in nodes}
129
+
130
+ chunk_size = max(1, math.ceil(len(nodes) / (workers * 4)))
131
+ mp_ctx = get_context("spawn")
132
+ tasks = ((chunk, t) for chunk in _chunk_nodes(nodes, chunk_size))
133
+
134
+ results: GammaMap = {}
135
+ with ProcessPoolExecutor(
136
+ max_workers=workers,
137
+ mp_context=mp_ctx,
138
+ initializer=_gamma_worker_init,
139
+ initargs=(G,),
140
+ ) as executor:
141
+ futures = [executor.submit(_gamma_worker, task) for task in tasks]
142
+ for fut in futures:
143
+ for node, value in fut.result():
144
+ results[node] = value
145
+ return results
146
+
147
+
148
+ def prepare_integration_params(
149
+ G: TNFRGraph,
150
+ dt: float | None = None,
151
+ t: float | None = None,
152
+ method: Literal["euler", "rk4"] | None = None,
153
+ ) -> tuple[float, int, float, Literal["euler", "rk4"]]:
154
+ """Validate and normalise ``dt``, ``t`` and ``method`` for integration.
155
+
156
+ Returns ``(dt_step, steps, t0, method)`` where ``dt_step`` is the
157
+ effective step, ``steps`` the number of substeps and ``t0`` the prepared
158
+ initial time.
159
+ """
160
+ if dt is None:
161
+ dt = float(G.graph.get("DT", DEFAULTS["DT"]))
162
+ else:
163
+ if not isinstance(dt, (int, float)):
164
+ raise TypeError("dt must be a number")
165
+ if dt < 0:
166
+ raise ValueError("dt must be non-negative")
167
+ dt = float(dt)
168
+
169
+ if t is None:
170
+ t = float(G.graph.get("_t", 0.0))
171
+ else:
172
+ t = float(t)
173
+
174
+ method_value = (
175
+ method
176
+ or G.graph.get(
177
+ "INTEGRATOR_METHOD", DEFAULTS.get("INTEGRATOR_METHOD", "euler")
178
+ )
179
+ ).lower()
180
+ if method_value not in ("euler", "rk4"):
181
+ raise ValueError("method must be 'euler' or 'rk4'")
182
+
183
+ dt_min = float(G.graph.get("DT_MIN", DEFAULTS.get("DT_MIN", 0.0)))
184
+ if dt_min > 0 and dt > dt_min:
185
+ steps = int(math.ceil(dt / dt_min))
186
+ else:
187
+ steps = 1
188
+ # ``steps`` is guaranteed to be ≥1 at this point
189
+ dt_step = dt / steps
190
+
191
+ return dt_step, steps, t, cast(Literal["euler", "rk4"], method_value)
192
+
193
+
194
+ def _apply_increments(
195
+ G: TNFRGraph,
196
+ dt_step: float,
197
+ increments: NodeIncrements,
198
+ *,
199
+ method: str,
200
+ n_jobs: int | None = None,
201
+ ) -> NodalUpdate:
202
+ """Combine precomputed increments to update node states."""
203
+
204
+ nodes: list[NodeId] = list(G.nodes)
205
+ if not nodes:
206
+ return {}
207
+
208
+ np = get_numpy()
209
+
210
+ epi_initial: list[float] = []
211
+ dEPI_prev: list[float] = []
212
+ ordered_increments: list[tuple[float, ...]] = []
213
+
214
+ for node in nodes:
215
+ nd = G.nodes[node]
216
+ _, _, dEPI_dt_prev, epi_i = _node_state(nd)
217
+ epi_initial.append(float(epi_i))
218
+ dEPI_prev.append(float(dEPI_dt_prev))
219
+ ordered_increments.append(increments[node])
220
+
221
+ if np is not None:
222
+ epi_arr = np.asarray(epi_initial, dtype=float)
223
+ dEPI_prev_arr = np.asarray(dEPI_prev, dtype=float)
224
+ k_arr = np.asarray(ordered_increments, dtype=float)
225
+
226
+ if method == "rk4":
227
+ if k_arr.ndim != 2 or k_arr.shape[1] != 4:
228
+ raise ValueError("rk4 increments require four staged values")
229
+ dt_factor = dt_step / 6.0
230
+ k1 = k_arr[:, 0]
231
+ k2 = k_arr[:, 1]
232
+ k3 = k_arr[:, 2]
233
+ k4 = k_arr[:, 3]
234
+ epi = epi_arr + dt_factor * (k1 + 2 * k2 + 2 * k3 + k4)
235
+ dEPI_dt = k4
236
+ else:
237
+ if k_arr.ndim == 1:
238
+ k1 = k_arr
239
+ else:
240
+ k1 = k_arr[:, 0]
241
+ epi = epi_arr + dt_step * k1
242
+ dEPI_dt = k1
243
+
244
+ if dt_step != 0:
245
+ d2epi = (dEPI_dt - dEPI_prev_arr) / dt_step
246
+ else:
247
+ d2epi = np.zeros_like(dEPI_dt)
248
+
249
+ results: NodalUpdate = {}
250
+ for idx, node in enumerate(nodes):
251
+ results[node] = (
252
+ float(epi[idx]),
253
+ float(dEPI_dt[idx]),
254
+ float(d2epi[idx]),
255
+ )
256
+ return results
257
+
258
+ payload: list[tuple[NodeId, float, float, tuple[float, ...]]] = list(
259
+ zip(nodes, epi_initial, dEPI_prev, ordered_increments)
260
+ )
261
+
262
+ workers = _normalise_jobs(n_jobs, len(nodes))
263
+ if workers is None:
264
+ return dict(_apply_increment_chunk(payload, dt_step, method))
265
+
266
+ chunk_size = max(1, math.ceil(len(nodes) / (workers * 4)))
267
+ mp_ctx = get_context("spawn")
268
+
269
+ results: NodalUpdate = {}
270
+ with ProcessPoolExecutor(max_workers=workers, mp_context=mp_ctx) as executor:
271
+ futures = [
272
+ executor.submit(
273
+ _apply_increment_chunk,
274
+ chunk,
275
+ dt_step,
276
+ method,
277
+ )
278
+ for chunk in _chunk_nodes(payload, chunk_size)
279
+ ]
280
+ for fut in futures:
281
+ for node, value in fut.result():
282
+ results[node] = value
283
+
284
+ return {node: results[node] for node in nodes}
285
+
286
+
287
+ def _collect_nodal_increments(
288
+ G: TNFRGraph,
289
+ gamma_maps: tuple[GammaMap, ...],
290
+ *,
291
+ method: str,
292
+ ) -> NodeIncrements:
293
+ """Combine node base state with staged Γ contributions.
294
+
295
+ ``gamma_maps`` must contain one entry for Euler integration and four for
296
+ RK4. The helper merges the structural frequency/ΔNFR base contribution
297
+ with the supplied Γ evaluations.
298
+ """
299
+
300
+ nodes: list[NodeId] = list(G.nodes())
301
+ if not nodes:
302
+ return {}
303
+
304
+ if method == "rk4":
305
+ expected_maps = 4
306
+ elif method == "euler":
307
+ expected_maps = 1
308
+ else:
309
+ raise ValueError("method must be 'euler' or 'rk4'")
310
+
311
+ if len(gamma_maps) != expected_maps:
312
+ raise ValueError(f"{method} integration requires {expected_maps} gamma maps")
313
+
314
+ np = get_numpy()
315
+ if np is not None:
316
+ vf = collect_attr(G, nodes, ALIAS_VF, 0.0, np=np)
317
+ dnfr = collect_attr(G, nodes, ALIAS_DNFR, 0.0, np=np)
318
+ base = vf * dnfr
319
+
320
+ gamma_arrays = [
321
+ np.fromiter((gm.get(n, 0.0) for n in nodes), float, count=len(nodes))
322
+ for gm in gamma_maps
323
+ ]
324
+ if gamma_arrays:
325
+ gamma_stack = np.stack(gamma_arrays, axis=1)
326
+ combined = base[:, None] + gamma_stack
327
+ else:
328
+ combined = base[:, None]
329
+
330
+ return {
331
+ node: tuple(float(value) for value in combined[idx])
332
+ for idx, node in enumerate(nodes)
333
+ }
334
+
335
+ increments: NodeIncrements = {}
336
+ for node in nodes:
337
+ nd = G.nodes[node]
338
+ vf, dnfr, *_ = _node_state(nd)
339
+ base = vf * dnfr
340
+ gammas = [gm.get(node, 0.0) for gm in gamma_maps]
341
+
342
+ if method == "rk4":
343
+ k1, k2, k3, k4 = gammas
344
+ increments[node] = (
345
+ base + k1,
346
+ base + k2,
347
+ base + k3,
348
+ base + k4,
349
+ )
350
+ else:
351
+ (k1,) = gammas
352
+ increments[node] = (base + k1,)
353
+
354
+ return increments
355
+
356
+
357
+ def _build_gamma_increments(
358
+ G: TNFRGraph,
359
+ dt_step: float,
360
+ t_local: float,
361
+ *,
362
+ method: str,
363
+ n_jobs: int | None = None,
364
+ ) -> NodeIncrements:
365
+ """Evaluate Γ contributions and merge them with ``νf·ΔNFR`` base terms."""
366
+
367
+ if method == "rk4":
368
+ gamma_count = 4
369
+ elif method == "euler":
370
+ gamma_count = 1
371
+ else:
372
+ raise ValueError("method must be 'euler' or 'rk4'")
373
+
374
+ gamma_spec = G.graph.get("_gamma_spec")
375
+ if gamma_spec is None:
376
+ gamma_spec = _get_gamma_spec(G)
377
+
378
+ gamma_type = ""
379
+ if isinstance(gamma_spec, Mapping):
380
+ gamma_type = str(gamma_spec.get("type", "")).lower()
381
+
382
+ if gamma_type == "none":
383
+ gamma_maps: tuple[GammaMap, ...] = tuple(
384
+ cast(GammaMap, {}) for _ in range(gamma_count)
385
+ )
386
+ return _collect_nodal_increments(G, gamma_maps, method=method)
387
+
388
+ nodes: list[NodeId] = list(G.nodes)
389
+ if not nodes:
390
+ gamma_maps = tuple(cast(GammaMap, {}) for _ in range(gamma_count))
391
+ return _collect_nodal_increments(G, gamma_maps, method=method)
392
+
393
+ if method == "rk4":
394
+ t_mid = t_local + dt_step / 2.0
395
+ t_end = t_local + dt_step
396
+ g1_map = _evaluate_gamma_map(G, nodes, t_local, n_jobs=n_jobs)
397
+ g_mid_map = _evaluate_gamma_map(G, nodes, t_mid, n_jobs=n_jobs)
398
+ g4_map = _evaluate_gamma_map(G, nodes, t_end, n_jobs=n_jobs)
399
+ gamma_maps = (g1_map, g_mid_map, g_mid_map, g4_map)
400
+ else: # method == "euler"
401
+ gamma_maps = (
402
+ _evaluate_gamma_map(G, nodes, t_local, n_jobs=n_jobs),
403
+ )
404
+
405
+ return _collect_nodal_increments(G, gamma_maps, method=method)
406
+
407
+
408
+ def _integrate_euler(
409
+ G: TNFRGraph,
410
+ dt_step: float,
411
+ t_local: float,
412
+ *,
413
+ n_jobs: int | None = None,
414
+ ) -> NodalUpdate:
415
+ """One explicit Euler integration step."""
416
+ increments = _build_gamma_increments(
417
+ G,
418
+ dt_step,
419
+ t_local,
420
+ method="euler",
421
+ n_jobs=n_jobs,
422
+ )
423
+ return _apply_increments(
424
+ G,
425
+ dt_step,
426
+ increments,
427
+ method="euler",
428
+ n_jobs=n_jobs,
429
+ )
430
+
431
+
432
+ def _integrate_rk4(
433
+ G: TNFRGraph,
434
+ dt_step: float,
435
+ t_local: float,
436
+ *,
437
+ n_jobs: int | None = None,
438
+ ) -> NodalUpdate:
439
+ """One Runge–Kutta order-4 integration step."""
440
+ increments = _build_gamma_increments(
441
+ G,
442
+ dt_step,
443
+ t_local,
444
+ method="rk4",
445
+ n_jobs=n_jobs,
446
+ )
447
+ return _apply_increments(
448
+ G,
449
+ dt_step,
450
+ increments,
451
+ method="rk4",
452
+ n_jobs=n_jobs,
453
+ )
454
+
455
+
456
+ class AbstractIntegrator(ABC):
457
+ """Abstract base class encapsulating nodal equation integration."""
458
+
459
+ @abstractmethod
460
+ def integrate(
461
+ self,
462
+ graph: TNFRGraph,
463
+ *,
464
+ dt: float | None,
465
+ t: float | None,
466
+ method: str | None,
467
+ n_jobs: int | None,
468
+ ) -> None:
469
+ """Advance ``graph`` coherence states according to the nodal equation."""
470
+
471
+
472
+ class DefaultIntegrator(AbstractIntegrator):
473
+ """Explicit integrator combining Euler and RK4 step implementations."""
474
+
475
+ def integrate(
476
+ self,
477
+ graph: TNFRGraph,
478
+ *,
479
+ dt: float | None,
480
+ t: float | None,
481
+ method: str | None,
482
+ n_jobs: int | None,
483
+ ) -> None:
484
+ if not isinstance(
485
+ graph, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
486
+ ):
487
+ raise TypeError("G must be a networkx graph instance")
488
+
489
+ dt_step, steps, t0, resolved_method = prepare_integration_params(
490
+ graph, dt, t, cast(IntegratorMethod | None, method)
491
+ )
492
+
493
+ t_local = t0
494
+ for _ in range(steps):
495
+ if resolved_method == "rk4":
496
+ updates: NodalUpdate = _integrate_rk4(
497
+ graph, dt_step, t_local, n_jobs=n_jobs
498
+ )
499
+ else:
500
+ updates = _integrate_euler(graph, dt_step, t_local, n_jobs=n_jobs)
501
+
502
+ for n, (epi, dEPI_dt, d2epi) in updates.items():
503
+ nd = graph.nodes[n]
504
+ epi_kind = get_attr_str(nd, ALIAS_EPI_KIND, "")
505
+ set_attr(nd, ALIAS_EPI, epi)
506
+ if epi_kind:
507
+ set_attr_str(nd, ALIAS_EPI_KIND, epi_kind)
508
+ set_attr(nd, ALIAS_DEPI, dEPI_dt)
509
+ set_attr(nd, ALIAS_D2EPI, d2epi)
510
+
511
+ t_local += dt_step
512
+
513
+ graph.graph["_t"] = t_local
514
+
515
+
516
+ def update_epi_via_nodal_equation(
517
+ G: TNFRGraph,
518
+ *,
519
+ dt: float | None = None,
520
+ t: float | None = None,
521
+ method: Literal["euler", "rk4"] | None = None,
522
+ n_jobs: int | None = None,
523
+ ) -> None:
524
+ """TNFR nodal equation.
525
+
526
+ Implements the extended nodal equation:
527
+ ∂EPI/∂t = νf · ΔNFR(t) + Γi(R)
528
+
529
+ Where:
530
+ - EPI is the node's Primary Information Structure.
531
+ - νf is the node's structural frequency (Hz_str).
532
+ - ΔNFR(t) is the nodal gradient (reorganisation need), typically a mix
533
+ of components (e.g. phase θ, EPI, νf).
534
+ - Γi(R) is the optional network coupling as a function of Kuramoto order
535
+ ``R`` (see :mod:`gamma`), used to modulate network integration.
536
+
537
+ TNFR references: nodal equation (manual), νf/ΔNFR/EPI glossary, Γ operator.
538
+ Side effects: caches dEPI and updates EPI via explicit integration.
539
+ """
540
+ DefaultIntegrator().integrate(
541
+ G,
542
+ dt=dt,
543
+ t=t,
544
+ method=method,
545
+ n_jobs=n_jobs,
546
+ )
547
+
548
+
549
+ def _node_state(nd: dict[str, Any]) -> tuple[float, float, float, float]:
550
+ """Return common node state attributes.
551
+
552
+ Extracts ``νf``, ``ΔNFR``, previous ``dEPI/dt`` and current ``EPI``
553
+ using alias helpers, providing ``0.0`` defaults when attributes are
554
+ missing.
555
+ """
556
+
557
+ vf = get_attr(nd, ALIAS_VF, 0.0)
558
+ dnfr = get_attr(nd, ALIAS_DNFR, 0.0)
559
+ dEPI_dt_prev = get_attr(nd, ALIAS_DEPI, 0.0)
560
+ epi_i = get_attr(nd, ALIAS_EPI, 0.0)
561
+ return vf, dnfr, dEPI_dt_prev, epi_i
@@ -0,0 +1,35 @@
1
+ from typing import Literal
2
+
3
+ from tnfr.types import TNFRGraph
4
+
5
+ __all__: tuple[str, ...]
6
+
7
+ class AbstractIntegrator:
8
+ def integrate(
9
+ self,
10
+ graph: TNFRGraph,
11
+ *,
12
+ dt: float | None = ...,
13
+ t: float | None = ...,
14
+ method: str | None = ...,
15
+ n_jobs: int | None = ...,
16
+ ) -> None: ...
17
+
18
+ class DefaultIntegrator(AbstractIntegrator):
19
+ def __init__(self) -> None: ...
20
+
21
+ def prepare_integration_params(
22
+ G: TNFRGraph,
23
+ dt: float | None = ...,
24
+ t: float | None = ...,
25
+ method: Literal["euler", "rk4"] | None = ...,
26
+ ) -> tuple[float, int, float, Literal["euler", "rk4"]]: ...
27
+
28
+ def update_epi_via_nodal_equation(
29
+ G: TNFRGraph,
30
+ *,
31
+ dt: float | None = ...,
32
+ t: float | None = ...,
33
+ method: Literal["euler", "rk4"] | None = ...,
34
+ n_jobs: int | None = ...,
35
+ ) -> None: ...