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