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.py
CHANGED
|
@@ -2,51 +2,294 @@
|
|
|
2
2
|
|
|
3
3
|
This module provides helper functions to configure, cache and apply ΔNFR
|
|
4
4
|
components such as phase, epidemiological state and vortex fields during
|
|
5
|
-
simulations.
|
|
5
|
+
simulations. The neighbour accumulation helpers reuse cached edge indices
|
|
6
|
+
and NumPy workspaces whenever available so cosine, sine, EPI, νf and topology
|
|
7
|
+
means remain faithful to the canonical ΔNFR reorganisation without redundant
|
|
8
|
+
allocations.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
9
12
|
|
|
10
13
|
import math
|
|
11
|
-
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
from
|
|
18
|
-
|
|
19
|
-
from ..alias import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
23
|
-
from ..metrics.trig_cache import compute_theta_trig
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequence
|
|
16
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
19
|
+
|
|
20
|
+
from time import perf_counter
|
|
21
|
+
|
|
22
|
+
from ..alias import get_attr, get_theta_attr, set_dnfr
|
|
23
|
+
from ..constants import DEFAULTS, get_param
|
|
24
|
+
from ..constants.aliases import ALIAS_EPI, ALIAS_VF
|
|
24
25
|
from ..metrics.common import merge_and_normalize_weights
|
|
25
|
-
from ..
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
from ..metrics.trig import neighbor_phase_mean_list
|
|
27
|
+
from ..metrics.trig_cache import compute_theta_trig
|
|
28
|
+
from ..types import (
|
|
29
|
+
DeltaNFRHook,
|
|
30
|
+
DnfrCacheVectors,
|
|
31
|
+
DnfrVectorMap,
|
|
32
|
+
NeighborStats,
|
|
33
|
+
NodeId,
|
|
34
|
+
TNFRGraph,
|
|
35
|
+
)
|
|
36
|
+
from ..utils import (
|
|
37
|
+
DNFR_PREP_STATE_KEY,
|
|
38
|
+
DnfrPrepState,
|
|
39
|
+
DnfrCache,
|
|
40
|
+
CacheManager,
|
|
41
|
+
_graph_cache_manager,
|
|
42
|
+
angle_diff,
|
|
43
|
+
angle_diff_array,
|
|
44
|
+
cached_node_list,
|
|
45
|
+
cached_nodes_and_A,
|
|
46
|
+
get_numpy,
|
|
47
|
+
normalize_weights,
|
|
48
|
+
resolve_chunk_size,
|
|
49
|
+
new_dnfr_cache,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING: # pragma: no cover - import-time typing hook
|
|
53
|
+
import numpy as np
|
|
54
|
+
|
|
55
|
+
_MEAN_VECTOR_EPS = 1e-12
|
|
56
|
+
_SPARSE_DENSITY_THRESHOLD = 0.25
|
|
57
|
+
_DNFR_APPROX_BYTES_PER_EDGE = 48
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _should_vectorize(G: TNFRGraph, np_module: ModuleType | None) -> bool:
|
|
61
|
+
"""Return ``True`` when NumPy is available unless the graph disables it."""
|
|
62
|
+
|
|
63
|
+
if np_module is None:
|
|
64
|
+
return False
|
|
65
|
+
flag = G.graph.get("vectorized_dnfr")
|
|
66
|
+
if flag is None:
|
|
67
|
+
return True
|
|
68
|
+
return bool(flag)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_NUMPY_CACHE_ATTRS = (
|
|
72
|
+
"theta_np",
|
|
73
|
+
"epi_np",
|
|
74
|
+
"vf_np",
|
|
75
|
+
"cos_theta_np",
|
|
76
|
+
"sin_theta_np",
|
|
77
|
+
"deg_array",
|
|
78
|
+
"neighbor_x_np",
|
|
79
|
+
"neighbor_y_np",
|
|
80
|
+
"neighbor_epi_sum_np",
|
|
81
|
+
"neighbor_vf_sum_np",
|
|
82
|
+
"neighbor_count_np",
|
|
83
|
+
"neighbor_deg_sum_np",
|
|
84
|
+
"neighbor_inv_count_np",
|
|
85
|
+
"neighbor_cos_avg_np",
|
|
86
|
+
"neighbor_sin_avg_np",
|
|
87
|
+
"neighbor_mean_tmp_np",
|
|
88
|
+
"neighbor_mean_length_np",
|
|
89
|
+
"neighbor_accum_np",
|
|
90
|
+
"neighbor_edge_values_np",
|
|
91
|
+
"dense_components_np",
|
|
92
|
+
"dense_accum_np",
|
|
93
|
+
"dense_degree_np",
|
|
94
|
+
)
|
|
29
95
|
|
|
30
96
|
|
|
97
|
+
def _profile_start_stop(
|
|
98
|
+
profile: MutableMapping[str, float] | None,
|
|
99
|
+
*,
|
|
100
|
+
keys: Sequence[str] = (),
|
|
101
|
+
) -> tuple[Callable[[], float], Callable[[str, float], None]]:
|
|
102
|
+
"""Return helpers to measure wall-clock durations for ``profile`` keys."""
|
|
31
103
|
|
|
104
|
+
if profile is not None:
|
|
105
|
+
for key in keys:
|
|
106
|
+
profile.setdefault(key, 0.0)
|
|
32
107
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
108
|
+
def _start() -> float:
|
|
109
|
+
return perf_counter()
|
|
110
|
+
|
|
111
|
+
def _stop(metric: str, start: float) -> None:
|
|
112
|
+
profile[metric] = float(profile.get(metric, 0.0)) + (perf_counter() - start)
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
|
|
116
|
+
def _start() -> float:
|
|
117
|
+
return 0.0
|
|
118
|
+
|
|
119
|
+
def _stop(metric: str, start: float) -> None: # noqa: ARG001 - uniform signature
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
return _start, _stop
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _iter_chunk_offsets(total: int, jobs: int) -> Iterator[tuple[int, int]]:
|
|
126
|
+
"""Yield ``(start, end)`` offsets splitting ``total`` items across ``jobs``."""
|
|
127
|
+
|
|
128
|
+
if total <= 0 or jobs <= 1:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
jobs = max(1, min(int(jobs), total))
|
|
132
|
+
base, extra = divmod(total, jobs)
|
|
133
|
+
start = 0
|
|
134
|
+
for i in range(jobs):
|
|
135
|
+
size = base + (1 if i < extra else 0)
|
|
136
|
+
if size <= 0:
|
|
137
|
+
continue
|
|
138
|
+
end = start + size
|
|
139
|
+
yield start, end
|
|
140
|
+
start = end
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _neighbor_sums_worker(
|
|
144
|
+
start: int,
|
|
145
|
+
end: int,
|
|
146
|
+
neighbor_indices: Sequence[Sequence[int]],
|
|
147
|
+
cos_th: Sequence[float],
|
|
148
|
+
sin_th: Sequence[float],
|
|
149
|
+
epi: Sequence[float],
|
|
150
|
+
vf: Sequence[float],
|
|
151
|
+
x_base: Sequence[float],
|
|
152
|
+
y_base: Sequence[float],
|
|
153
|
+
epi_base: Sequence[float],
|
|
154
|
+
vf_base: Sequence[float],
|
|
155
|
+
count_base: Sequence[float],
|
|
156
|
+
deg_base: Sequence[float] | None,
|
|
157
|
+
deg_list: Sequence[float] | None,
|
|
158
|
+
degs_list: Sequence[float] | None,
|
|
159
|
+
) -> tuple[
|
|
160
|
+
int,
|
|
161
|
+
list[float],
|
|
162
|
+
list[float],
|
|
163
|
+
list[float],
|
|
164
|
+
list[float],
|
|
165
|
+
list[float],
|
|
166
|
+
list[float] | None,
|
|
167
|
+
]:
|
|
168
|
+
"""Return partial neighbour sums for the ``[start, end)`` range."""
|
|
169
|
+
|
|
170
|
+
chunk_x: list[float] = []
|
|
171
|
+
chunk_y: list[float] = []
|
|
172
|
+
chunk_epi: list[float] = []
|
|
173
|
+
chunk_vf: list[float] = []
|
|
174
|
+
chunk_count: list[float] = []
|
|
175
|
+
chunk_deg: list[float] | None = [] if deg_base is not None else None
|
|
176
|
+
|
|
177
|
+
for offset, idx in enumerate(range(start, end)):
|
|
178
|
+
neighbors = neighbor_indices[idx]
|
|
179
|
+
x_i = float(x_base[offset])
|
|
180
|
+
y_i = float(y_base[offset])
|
|
181
|
+
epi_i = float(epi_base[offset])
|
|
182
|
+
vf_i = float(vf_base[offset])
|
|
183
|
+
count_i = float(count_base[offset])
|
|
184
|
+
if deg_base is not None and chunk_deg is not None:
|
|
185
|
+
deg_i_acc = float(deg_base[offset])
|
|
186
|
+
else:
|
|
187
|
+
deg_i_acc = 0.0
|
|
188
|
+
deg_i = float(degs_list[idx]) if degs_list is not None else 0.0
|
|
189
|
+
|
|
190
|
+
for neighbor_idx in neighbors:
|
|
191
|
+
x_i += float(cos_th[neighbor_idx])
|
|
192
|
+
y_i += float(sin_th[neighbor_idx])
|
|
193
|
+
epi_i += float(epi[neighbor_idx])
|
|
194
|
+
vf_i += float(vf[neighbor_idx])
|
|
195
|
+
count_i += 1.0
|
|
196
|
+
if chunk_deg is not None:
|
|
197
|
+
if deg_list is not None:
|
|
198
|
+
deg_i_acc += float(deg_list[neighbor_idx])
|
|
199
|
+
else:
|
|
200
|
+
deg_i_acc += deg_i
|
|
201
|
+
|
|
202
|
+
chunk_x.append(x_i)
|
|
203
|
+
chunk_y.append(y_i)
|
|
204
|
+
chunk_epi.append(epi_i)
|
|
205
|
+
chunk_vf.append(vf_i)
|
|
206
|
+
chunk_count.append(count_i)
|
|
207
|
+
if chunk_deg is not None:
|
|
208
|
+
chunk_deg.append(deg_i_acc)
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
start,
|
|
212
|
+
chunk_x,
|
|
213
|
+
chunk_y,
|
|
214
|
+
chunk_epi,
|
|
215
|
+
chunk_vf,
|
|
216
|
+
chunk_count,
|
|
217
|
+
chunk_deg,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _dnfr_gradients_worker(
|
|
222
|
+
start: int,
|
|
223
|
+
end: int,
|
|
224
|
+
nodes: Sequence[NodeId],
|
|
225
|
+
theta: list[float],
|
|
226
|
+
epi: list[float],
|
|
227
|
+
vf: list[float],
|
|
228
|
+
th_bar: list[float],
|
|
229
|
+
epi_bar: list[float],
|
|
230
|
+
vf_bar: list[float],
|
|
231
|
+
deg_bar: list[float] | None,
|
|
232
|
+
degs: Mapping[Any, float] | Sequence[float] | None,
|
|
233
|
+
w_phase: float,
|
|
234
|
+
w_epi: float,
|
|
235
|
+
w_vf: float,
|
|
236
|
+
w_topo: float,
|
|
237
|
+
) -> tuple[int, list[float]]:
|
|
238
|
+
"""Return partial ΔNFR gradients for the ``[start, end)`` range."""
|
|
239
|
+
|
|
240
|
+
chunk: list[float] = []
|
|
241
|
+
for idx in range(start, end):
|
|
242
|
+
n = nodes[idx]
|
|
243
|
+
g_phase = -angle_diff(theta[idx], th_bar[idx]) / math.pi
|
|
244
|
+
g_epi = epi_bar[idx] - epi[idx]
|
|
245
|
+
g_vf = vf_bar[idx] - vf[idx]
|
|
246
|
+
if w_topo != 0.0 and deg_bar is not None and degs is not None:
|
|
247
|
+
if isinstance(degs, dict):
|
|
248
|
+
deg_i = float(degs.get(n, 0))
|
|
249
|
+
else:
|
|
250
|
+
deg_i = float(degs[idx])
|
|
251
|
+
g_topo = deg_bar[idx] - deg_i
|
|
252
|
+
else:
|
|
253
|
+
g_topo = 0.0
|
|
254
|
+
chunk.append(w_phase * g_phase + w_epi * g_epi + w_vf * g_vf + w_topo * g_topo)
|
|
255
|
+
return start, chunk
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _resolve_parallel_jobs(n_jobs: int | None, total: int) -> int | None:
|
|
259
|
+
"""Return an effective worker count for ``total`` items or ``None``."""
|
|
260
|
+
|
|
261
|
+
if n_jobs is None:
|
|
262
|
+
return None
|
|
263
|
+
try:
|
|
264
|
+
jobs = int(n_jobs)
|
|
265
|
+
except (TypeError, ValueError):
|
|
266
|
+
return None
|
|
267
|
+
if jobs <= 1 or total <= 1:
|
|
268
|
+
return None
|
|
269
|
+
return max(1, min(jobs, total))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _is_numpy_like(obj) -> bool:
|
|
273
|
+
return (
|
|
274
|
+
getattr(obj, "dtype", None) is not None
|
|
275
|
+
and getattr(obj, "shape", None) is not None
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _has_cached_numpy_buffers(data: dict, cache: DnfrCache | None) -> bool:
|
|
280
|
+
for attr in _NUMPY_CACHE_ATTRS:
|
|
281
|
+
arr = data.get(attr)
|
|
282
|
+
if _is_numpy_like(arr):
|
|
283
|
+
return True
|
|
284
|
+
if cache is not None:
|
|
285
|
+
for attr in _NUMPY_CACHE_ATTRS:
|
|
286
|
+
arr = getattr(cache, attr, None)
|
|
287
|
+
if _is_numpy_like(arr):
|
|
288
|
+
return True
|
|
289
|
+
A = data.get("A")
|
|
290
|
+
if _is_numpy_like(A):
|
|
291
|
+
return True
|
|
292
|
+
return False
|
|
50
293
|
|
|
51
294
|
|
|
52
295
|
__all__ = (
|
|
@@ -93,37 +336,130 @@ def _configure_dnfr_weights(G) -> dict:
|
|
|
93
336
|
return weights
|
|
94
337
|
|
|
95
338
|
|
|
96
|
-
def _init_dnfr_cache(
|
|
97
|
-
|
|
98
|
-
|
|
339
|
+
def _init_dnfr_cache(
|
|
340
|
+
G: TNFRGraph,
|
|
341
|
+
nodes: Sequence[NodeId],
|
|
342
|
+
cache_or_manager: CacheManager | DnfrCache | None = None,
|
|
343
|
+
checksum: Any | None = None,
|
|
344
|
+
force_refresh: bool = False,
|
|
345
|
+
*,
|
|
346
|
+
manager: CacheManager | None = None,
|
|
347
|
+
) -> tuple[
|
|
348
|
+
DnfrCache,
|
|
349
|
+
dict[NodeId, int],
|
|
350
|
+
list[float],
|
|
351
|
+
list[float],
|
|
352
|
+
list[float],
|
|
353
|
+
list[float],
|
|
354
|
+
list[float],
|
|
355
|
+
bool,
|
|
356
|
+
]:
|
|
357
|
+
"""Initialise or reuse cached ΔNFR arrays.
|
|
358
|
+
|
|
359
|
+
``manager`` telemetry became mandatory in TNFR 9.0 to expose cache hits,
|
|
360
|
+
misses and timings. Older callers still pass a ``cache`` instance as the
|
|
361
|
+
third positional argument; this helper supports both signatures by seeding
|
|
362
|
+
the manager-backed state with the provided cache when necessary.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
if manager is None and isinstance(cache_or_manager, CacheManager):
|
|
366
|
+
manager = cache_or_manager
|
|
367
|
+
cache_or_manager = None
|
|
368
|
+
|
|
369
|
+
if manager is None:
|
|
370
|
+
manager = _graph_cache_manager(G.graph)
|
|
371
|
+
|
|
372
|
+
graph = G.graph
|
|
373
|
+
state = manager.get(DNFR_PREP_STATE_KEY)
|
|
374
|
+
if not isinstance(state, DnfrPrepState):
|
|
375
|
+
manager.clear(DNFR_PREP_STATE_KEY)
|
|
376
|
+
state = manager.get(DNFR_PREP_STATE_KEY)
|
|
377
|
+
|
|
378
|
+
if isinstance(cache_or_manager, DnfrCache):
|
|
379
|
+
state.cache = cache_or_manager
|
|
380
|
+
if checksum is None:
|
|
381
|
+
checksum = cache_or_manager.checksum
|
|
382
|
+
|
|
383
|
+
cache = state.cache
|
|
384
|
+
reuse = (
|
|
385
|
+
not force_refresh
|
|
386
|
+
and isinstance(cache, DnfrCache)
|
|
387
|
+
and cache.checksum == checksum
|
|
388
|
+
and len(cache.theta) == len(nodes)
|
|
389
|
+
)
|
|
390
|
+
if reuse:
|
|
391
|
+
manager.increment_hit(DNFR_PREP_STATE_KEY)
|
|
392
|
+
graph["_dnfr_prep_cache"] = cache
|
|
99
393
|
return (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
394
|
+
cache,
|
|
395
|
+
cache.idx,
|
|
396
|
+
cache.theta,
|
|
397
|
+
cache.epi,
|
|
398
|
+
cache.vf,
|
|
399
|
+
cache.cos_theta,
|
|
400
|
+
cache.sin_theta,
|
|
107
401
|
False,
|
|
108
402
|
)
|
|
109
403
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
404
|
+
def _rebuild(current: DnfrPrepState | Any) -> DnfrPrepState:
|
|
405
|
+
if not isinstance(current, DnfrPrepState):
|
|
406
|
+
raise RuntimeError("ΔNFR prep state unavailable during rebuild")
|
|
407
|
+
prev_cache = current.cache if isinstance(current.cache, DnfrCache) else None
|
|
408
|
+
idx_local = {n: i for i, n in enumerate(nodes)}
|
|
409
|
+
size = len(nodes)
|
|
410
|
+
zeros = [0.0] * size
|
|
411
|
+
cache_new = prev_cache if prev_cache is not None else new_dnfr_cache()
|
|
412
|
+
cache_new.idx = idx_local
|
|
413
|
+
cache_new.theta = zeros.copy()
|
|
414
|
+
cache_new.epi = zeros.copy()
|
|
415
|
+
cache_new.vf = zeros.copy()
|
|
416
|
+
cache_new.cos_theta = [1.0] * size
|
|
417
|
+
cache_new.sin_theta = [0.0] * size
|
|
418
|
+
cache_new.neighbor_x = zeros.copy()
|
|
419
|
+
cache_new.neighbor_y = zeros.copy()
|
|
420
|
+
cache_new.neighbor_epi_sum = zeros.copy()
|
|
421
|
+
cache_new.neighbor_vf_sum = zeros.copy()
|
|
422
|
+
cache_new.neighbor_count = zeros.copy()
|
|
423
|
+
cache_new.neighbor_deg_sum = zeros.copy() if size else []
|
|
424
|
+
cache_new.degs = None
|
|
425
|
+
cache_new.edge_src = None
|
|
426
|
+
cache_new.edge_dst = None
|
|
427
|
+
cache_new.checksum = checksum
|
|
428
|
+
|
|
429
|
+
# Reset any numpy mirrors or aggregated buffers to avoid leaking
|
|
430
|
+
# state across refresh cycles (e.g. switching between vectorised
|
|
431
|
+
# and Python paths or reusing legacy caches).
|
|
432
|
+
if prev_cache is not None:
|
|
433
|
+
for attr in _NUMPY_CACHE_ATTRS:
|
|
434
|
+
setattr(cache_new, attr, None)
|
|
435
|
+
for attr in (
|
|
436
|
+
"th_bar_np",
|
|
437
|
+
"epi_bar_np",
|
|
438
|
+
"vf_bar_np",
|
|
439
|
+
"deg_bar_np",
|
|
440
|
+
"grad_phase_np",
|
|
441
|
+
"grad_epi_np",
|
|
442
|
+
"grad_vf_np",
|
|
443
|
+
"grad_topo_np",
|
|
444
|
+
"grad_total_np",
|
|
445
|
+
):
|
|
446
|
+
setattr(cache_new, attr, None)
|
|
447
|
+
cache_new.edge_src = None
|
|
448
|
+
cache_new.edge_dst = None
|
|
449
|
+
cache_new.edge_signature = None
|
|
450
|
+
cache_new.neighbor_accum_signature = None
|
|
451
|
+
cache_new.degs = prev_cache.degs if prev_cache else None
|
|
452
|
+
cache_new.checksum = checksum
|
|
453
|
+
current.cache = cache_new
|
|
454
|
+
graph["_dnfr_prep_cache"] = cache_new
|
|
455
|
+
return current
|
|
456
|
+
|
|
457
|
+
with manager.timer(DNFR_PREP_STATE_KEY):
|
|
458
|
+
state = manager.update(DNFR_PREP_STATE_KEY, _rebuild)
|
|
459
|
+
manager.increment_miss(DNFR_PREP_STATE_KEY)
|
|
460
|
+
cache = state.cache
|
|
461
|
+
if not isinstance(cache, DnfrCache): # pragma: no cover - defensive guard
|
|
462
|
+
raise RuntimeError("ΔNFR cache initialisation failed")
|
|
127
463
|
return (
|
|
128
464
|
cache,
|
|
129
465
|
cache.idx,
|
|
@@ -136,13 +472,14 @@ def _init_dnfr_cache(G, nodes, prev_cache: DnfrCache | None, checksum, dirty):
|
|
|
136
472
|
)
|
|
137
473
|
|
|
138
474
|
|
|
139
|
-
def _ensure_numpy_vectors(cache: DnfrCache, np):
|
|
475
|
+
def _ensure_numpy_vectors(cache: DnfrCache, np: ModuleType) -> DnfrCacheVectors:
|
|
140
476
|
"""Ensure NumPy copies of cached vectors are initialised and up to date."""
|
|
141
477
|
|
|
142
478
|
if cache is None:
|
|
143
479
|
return (None, None, None, None, None)
|
|
144
480
|
|
|
145
|
-
arrays = []
|
|
481
|
+
arrays: list[Any | None] = []
|
|
482
|
+
size = len(cache.theta)
|
|
146
483
|
for attr_np, source_attr in (
|
|
147
484
|
("theta_np", "theta"),
|
|
148
485
|
("epi_np", "epi"),
|
|
@@ -150,28 +487,36 @@ def _ensure_numpy_vectors(cache: DnfrCache, np):
|
|
|
150
487
|
("cos_theta_np", "cos_theta"),
|
|
151
488
|
("sin_theta_np", "sin_theta"),
|
|
152
489
|
):
|
|
153
|
-
src = getattr(cache, source_attr)
|
|
154
490
|
arr = getattr(cache, attr_np)
|
|
491
|
+
if arr is not None and getattr(arr, "shape", None) == (size,):
|
|
492
|
+
arrays.append(arr)
|
|
493
|
+
continue
|
|
494
|
+
src = getattr(cache, source_attr)
|
|
155
495
|
if src is None:
|
|
156
496
|
setattr(cache, attr_np, None)
|
|
157
497
|
arrays.append(None)
|
|
158
498
|
continue
|
|
159
|
-
|
|
499
|
+
arr = np.asarray(src, dtype=float)
|
|
500
|
+
if getattr(arr, "shape", None) != (size,):
|
|
160
501
|
arr = np.array(src, dtype=float)
|
|
161
|
-
else:
|
|
162
|
-
np.copyto(arr, src, casting="unsafe")
|
|
163
502
|
setattr(cache, attr_np, arr)
|
|
164
503
|
arrays.append(arr)
|
|
165
504
|
return tuple(arrays)
|
|
166
505
|
|
|
167
506
|
|
|
168
|
-
def _ensure_numpy_degrees(
|
|
507
|
+
def _ensure_numpy_degrees(
|
|
508
|
+
cache: DnfrCache,
|
|
509
|
+
deg_list: Sequence[float] | None,
|
|
510
|
+
np: ModuleType,
|
|
511
|
+
) -> np.ndarray | None:
|
|
169
512
|
"""Initialise/update NumPy array mirroring ``deg_list``."""
|
|
170
513
|
|
|
171
|
-
if
|
|
514
|
+
if deg_list is None:
|
|
172
515
|
if cache is not None:
|
|
173
516
|
cache.deg_array = None
|
|
174
517
|
return None
|
|
518
|
+
if cache is None:
|
|
519
|
+
return np.array(deg_list, dtype=float)
|
|
175
520
|
arr = cache.deg_array
|
|
176
521
|
if arr is None or len(arr) != len(deg_list):
|
|
177
522
|
arr = np.array(deg_list, dtype=float)
|
|
@@ -181,121 +526,498 @@ def _ensure_numpy_degrees(cache: DnfrCache, deg_list, np):
|
|
|
181
526
|
return arr
|
|
182
527
|
|
|
183
528
|
|
|
184
|
-
def
|
|
529
|
+
def _resolve_numpy_degree_array(
|
|
530
|
+
data: MutableMapping[str, Any],
|
|
531
|
+
count: np.ndarray | None,
|
|
532
|
+
*,
|
|
533
|
+
cache: DnfrCache | None,
|
|
534
|
+
np: ModuleType,
|
|
535
|
+
) -> np.ndarray | None:
|
|
536
|
+
"""Return the vector of node degrees required for topology gradients."""
|
|
537
|
+
|
|
538
|
+
if data["w_topo"] == 0.0:
|
|
539
|
+
return None
|
|
540
|
+
deg_array = data.get("deg_array")
|
|
541
|
+
if deg_array is not None:
|
|
542
|
+
return deg_array
|
|
543
|
+
deg_list = data.get("deg_list")
|
|
544
|
+
if deg_list is not None:
|
|
545
|
+
deg_array = np.array(deg_list, dtype=float)
|
|
546
|
+
data["deg_array"] = deg_array
|
|
547
|
+
if cache is not None:
|
|
548
|
+
cache.deg_array = deg_array
|
|
549
|
+
return deg_array
|
|
550
|
+
return count
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _ensure_cached_array(
|
|
554
|
+
cache: DnfrCache | None,
|
|
555
|
+
attr: str,
|
|
556
|
+
shape: tuple[int, ...],
|
|
557
|
+
np: ModuleType,
|
|
558
|
+
) -> np.ndarray:
|
|
559
|
+
"""Return a cached NumPy buffer with ``shape`` creating/reusing it."""
|
|
560
|
+
|
|
561
|
+
if np is None:
|
|
562
|
+
raise RuntimeError("NumPy is required to build cached arrays")
|
|
563
|
+
arr = getattr(cache, attr) if cache is not None else None
|
|
564
|
+
if arr is None or getattr(arr, "shape", None) != shape:
|
|
565
|
+
arr = np.empty(shape, dtype=float)
|
|
566
|
+
if cache is not None:
|
|
567
|
+
setattr(cache, attr, arr)
|
|
568
|
+
return arr
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _ensure_numpy_state_vectors(
|
|
572
|
+
data: MutableMapping[str, Any], np: ModuleType
|
|
573
|
+
) -> DnfrVectorMap:
|
|
574
|
+
"""Synchronise list-based state vectors with their NumPy counterparts."""
|
|
575
|
+
|
|
576
|
+
nodes = data.get("nodes") or ()
|
|
577
|
+
size = len(nodes)
|
|
578
|
+
cache: DnfrCache | None = data.get("cache")
|
|
579
|
+
|
|
580
|
+
cache_arrays: DnfrCacheVectors = (None, None, None, None, None)
|
|
581
|
+
if cache is not None:
|
|
582
|
+
cache_arrays = _ensure_numpy_vectors(cache, np)
|
|
583
|
+
|
|
584
|
+
result: dict[str, Any | None] = {}
|
|
585
|
+
for (plain_key, np_key, cached_arr, result_key) in (
|
|
586
|
+
("theta", "theta_np", cache_arrays[0], "theta"),
|
|
587
|
+
("epi", "epi_np", cache_arrays[1], "epi"),
|
|
588
|
+
("vf", "vf_np", cache_arrays[2], "vf"),
|
|
589
|
+
("cos_theta", "cos_theta_np", cache_arrays[3], "cos"),
|
|
590
|
+
("sin_theta", "sin_theta_np", cache_arrays[4], "sin"),
|
|
591
|
+
):
|
|
592
|
+
arr = data.get(np_key)
|
|
593
|
+
if arr is None:
|
|
594
|
+
arr = cached_arr
|
|
595
|
+
if arr is None or getattr(arr, "shape", None) != (size,):
|
|
596
|
+
src = data.get(plain_key)
|
|
597
|
+
if src is None and cache is not None:
|
|
598
|
+
src = getattr(cache, plain_key)
|
|
599
|
+
if src is None:
|
|
600
|
+
arr = None
|
|
601
|
+
else:
|
|
602
|
+
arr = np.asarray(src, dtype=float)
|
|
603
|
+
if getattr(arr, "shape", None) != (size,):
|
|
604
|
+
arr = np.array(src, dtype=float)
|
|
605
|
+
if arr is not None:
|
|
606
|
+
data[np_key] = arr
|
|
607
|
+
data[plain_key] = arr
|
|
608
|
+
if cache is not None:
|
|
609
|
+
setattr(cache, np_key, arr)
|
|
610
|
+
else:
|
|
611
|
+
data[np_key] = None
|
|
612
|
+
result[result_key] = arr
|
|
613
|
+
|
|
614
|
+
return result
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _build_edge_index_arrays(
|
|
618
|
+
G: TNFRGraph,
|
|
619
|
+
nodes: Sequence[NodeId],
|
|
620
|
+
idx: Mapping[NodeId, int],
|
|
621
|
+
np: ModuleType,
|
|
622
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
623
|
+
"""Create (src, dst) index arrays for ``G`` respecting ``nodes`` order."""
|
|
624
|
+
|
|
625
|
+
if np is None:
|
|
626
|
+
return None, None
|
|
627
|
+
if not nodes:
|
|
628
|
+
empty = np.empty(0, dtype=np.intp)
|
|
629
|
+
return empty, empty
|
|
630
|
+
|
|
631
|
+
src = []
|
|
632
|
+
dst = []
|
|
633
|
+
append_src = src.append
|
|
634
|
+
append_dst = dst.append
|
|
635
|
+
for node in nodes:
|
|
636
|
+
i = idx.get(node)
|
|
637
|
+
if i is None:
|
|
638
|
+
continue
|
|
639
|
+
for neighbor in G.neighbors(node):
|
|
640
|
+
j = idx.get(neighbor)
|
|
641
|
+
if j is None:
|
|
642
|
+
continue
|
|
643
|
+
append_src(i)
|
|
644
|
+
append_dst(j)
|
|
645
|
+
if not src:
|
|
646
|
+
empty = np.empty(0, dtype=np.intp)
|
|
647
|
+
return empty, empty
|
|
648
|
+
edge_src = np.asarray(src, dtype=np.intp)
|
|
649
|
+
edge_dst = np.asarray(dst, dtype=np.intp)
|
|
650
|
+
return edge_src, edge_dst
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _refresh_dnfr_vectors(
|
|
654
|
+
G: TNFRGraph, nodes: Sequence[NodeId], cache: DnfrCache
|
|
655
|
+
) -> None:
|
|
185
656
|
"""Update cached angle and state vectors for ΔNFR."""
|
|
186
|
-
|
|
187
|
-
trig = compute_theta_trig(((n, G.nodes[n]) for n in nodes), np=
|
|
188
|
-
use_numpy =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
657
|
+
np_module = get_numpy()
|
|
658
|
+
trig = compute_theta_trig(((n, G.nodes[n]) for n in nodes), np=np_module)
|
|
659
|
+
use_numpy = _should_vectorize(G, np_module)
|
|
660
|
+
node_count = len(nodes)
|
|
661
|
+
trig_theta = getattr(trig, "theta_values", None)
|
|
662
|
+
trig_cos = getattr(trig, "cos_values", None)
|
|
663
|
+
trig_sin = getattr(trig, "sin_values", None)
|
|
664
|
+
np_ready = (
|
|
665
|
+
use_numpy
|
|
666
|
+
and np_module is not None
|
|
667
|
+
and isinstance(trig_theta, getattr(np_module, "ndarray", tuple()))
|
|
668
|
+
and isinstance(trig_cos, getattr(np_module, "ndarray", tuple()))
|
|
669
|
+
and isinstance(trig_sin, getattr(np_module, "ndarray", tuple()))
|
|
670
|
+
and getattr(trig_theta, "shape", None) == getattr(trig_cos, "shape", None)
|
|
671
|
+
and getattr(trig_theta, "shape", None) == getattr(trig_sin, "shape", None)
|
|
672
|
+
and (trig_theta.shape[0] if getattr(trig_theta, "ndim", 0) else 0) == node_count
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
if np_ready:
|
|
676
|
+
if node_count:
|
|
677
|
+
epi_arr = np_module.fromiter(
|
|
678
|
+
(get_attr(G.nodes[node], ALIAS_EPI, 0.0) for node in nodes),
|
|
679
|
+
dtype=float,
|
|
680
|
+
count=node_count,
|
|
681
|
+
)
|
|
682
|
+
vf_arr = np_module.fromiter(
|
|
683
|
+
(get_attr(G.nodes[node], ALIAS_VF, 0.0) for node in nodes),
|
|
684
|
+
dtype=float,
|
|
685
|
+
count=node_count,
|
|
686
|
+
)
|
|
687
|
+
else:
|
|
688
|
+
epi_arr = np_module.empty(0, dtype=float)
|
|
689
|
+
vf_arr = np_module.empty(0, dtype=float)
|
|
690
|
+
|
|
691
|
+
theta_arr = np_module.asarray(trig_theta, dtype=float)
|
|
692
|
+
cos_arr = np_module.asarray(trig_cos, dtype=float)
|
|
693
|
+
sin_arr = np_module.asarray(trig_sin, dtype=float)
|
|
694
|
+
|
|
695
|
+
def _sync_numpy(attr: str, source: Any) -> Any:
|
|
696
|
+
dest = getattr(cache, attr)
|
|
697
|
+
if dest is None or getattr(dest, "shape", None) != source.shape:
|
|
698
|
+
dest = np_module.array(source, dtype=float)
|
|
699
|
+
else:
|
|
700
|
+
np_module.copyto(dest, source, casting="unsafe")
|
|
701
|
+
setattr(cache, attr, dest)
|
|
702
|
+
return dest
|
|
703
|
+
|
|
704
|
+
_sync_numpy("theta_np", theta_arr)
|
|
705
|
+
_sync_numpy("epi_np", epi_arr)
|
|
706
|
+
_sync_numpy("vf_np", vf_arr)
|
|
707
|
+
_sync_numpy("cos_theta_np", cos_arr)
|
|
708
|
+
_sync_numpy("sin_theta_np", sin_arr)
|
|
709
|
+
|
|
710
|
+
# Python mirrors remain untouched while the vectorised path is active.
|
|
711
|
+
# They will be rebuilt the next time the runtime falls back to lists.
|
|
712
|
+
if cache.theta is not None and len(cache.theta) != node_count:
|
|
713
|
+
cache.theta = [0.0] * node_count
|
|
714
|
+
if cache.epi is not None and len(cache.epi) != node_count:
|
|
715
|
+
cache.epi = [0.0] * node_count
|
|
716
|
+
if cache.vf is not None and len(cache.vf) != node_count:
|
|
717
|
+
cache.vf = [0.0] * node_count
|
|
718
|
+
if cache.cos_theta is not None and len(cache.cos_theta) != node_count:
|
|
719
|
+
cache.cos_theta = [1.0] * node_count
|
|
720
|
+
if cache.sin_theta is not None and len(cache.sin_theta) != node_count:
|
|
721
|
+
cache.sin_theta = [0.0] * node_count
|
|
198
722
|
else:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
723
|
+
for index, node in enumerate(nodes):
|
|
724
|
+
i: int = int(index)
|
|
725
|
+
node_id: NodeId = node
|
|
726
|
+
nd = G.nodes[node_id]
|
|
727
|
+
cache.theta[i] = trig.theta[node_id]
|
|
728
|
+
cache.epi[i] = get_attr(nd, ALIAS_EPI, 0.0)
|
|
729
|
+
cache.vf[i] = get_attr(nd, ALIAS_VF, 0.0)
|
|
730
|
+
cache.cos_theta[i] = trig.cos[node_id]
|
|
731
|
+
cache.sin_theta[i] = trig.sin[node_id]
|
|
732
|
+
if use_numpy and np_module is not None:
|
|
733
|
+
_ensure_numpy_vectors(cache, np_module)
|
|
734
|
+
else:
|
|
735
|
+
cache.theta_np = None
|
|
736
|
+
cache.epi_np = None
|
|
737
|
+
cache.vf_np = None
|
|
738
|
+
cache.cos_theta_np = None
|
|
739
|
+
cache.sin_theta_np = None
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _prepare_dnfr_data(
|
|
743
|
+
G: TNFRGraph,
|
|
744
|
+
*,
|
|
745
|
+
cache_size: int | None = 128,
|
|
746
|
+
profile: MutableMapping[str, float] | None = None,
|
|
747
|
+
) -> dict[str, Any]:
|
|
748
|
+
"""Precompute common data for ΔNFR strategies.
|
|
749
|
+
|
|
750
|
+
The helper decides between edge-wise and dense adjacency accumulation
|
|
751
|
+
heuristically. Graphs whose edge density exceeds
|
|
752
|
+
``_SPARSE_DENSITY_THRESHOLD`` receive a cached adjacency matrix so the
|
|
753
|
+
dense path can be exercised; callers may also force the dense mode by
|
|
754
|
+
setting ``G.graph['dnfr_force_dense']`` to a truthy value.
|
|
204
755
|
|
|
756
|
+
Parameters
|
|
757
|
+
----------
|
|
758
|
+
profile : MutableMapping[str, float] or None, optional
|
|
759
|
+
Mutable mapping that accumulates wall-clock timings for ΔNFR
|
|
760
|
+
preparation. When provided the helper increases the
|
|
761
|
+
``"dnfr_cache_rebuild"`` bucket with the time spent refreshing cached
|
|
762
|
+
node vectors and associated NumPy workspaces.
|
|
763
|
+
"""
|
|
764
|
+
start_timer, stop_timer = _profile_start_stop(
|
|
765
|
+
profile,
|
|
766
|
+
keys=("dnfr_cache_rebuild",),
|
|
767
|
+
)
|
|
205
768
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
weights = G.graph.get("_dnfr_weights")
|
|
769
|
+
graph = G.graph
|
|
770
|
+
weights = graph.get("_dnfr_weights")
|
|
209
771
|
if weights is None:
|
|
210
772
|
weights = _configure_dnfr_weights(G)
|
|
211
773
|
|
|
212
|
-
|
|
213
|
-
|
|
774
|
+
result: dict[str, Any] = {
|
|
775
|
+
"weights": weights,
|
|
776
|
+
"cache_size": cache_size,
|
|
777
|
+
}
|
|
214
778
|
|
|
215
|
-
|
|
216
|
-
|
|
779
|
+
np_module = get_numpy()
|
|
780
|
+
use_numpy = _should_vectorize(G, np_module)
|
|
781
|
+
|
|
782
|
+
nodes = cast(tuple[NodeId, ...], cached_node_list(G))
|
|
783
|
+
edge_count = G.number_of_edges()
|
|
784
|
+
prefer_sparse = False
|
|
785
|
+
dense_override = bool(G.graph.get("dnfr_force_dense"))
|
|
786
|
+
if use_numpy:
|
|
787
|
+
prefer_sparse = _prefer_sparse_accumulation(len(nodes), edge_count)
|
|
788
|
+
if dense_override:
|
|
789
|
+
prefer_sparse = False
|
|
790
|
+
nodes_cached, A_untyped = cached_nodes_and_A(
|
|
791
|
+
G,
|
|
792
|
+
cache_size=cache_size,
|
|
793
|
+
require_numpy=False,
|
|
794
|
+
prefer_sparse=prefer_sparse,
|
|
795
|
+
nodes=nodes,
|
|
796
|
+
)
|
|
797
|
+
nodes = cast(tuple[NodeId, ...], nodes_cached)
|
|
798
|
+
A: np.ndarray | None = A_untyped
|
|
799
|
+
result["nodes"] = nodes
|
|
800
|
+
result["A"] = A
|
|
801
|
+
manager = _graph_cache_manager(G.graph)
|
|
217
802
|
checksum = G.graph.get("_dnfr_nodes_checksum")
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
803
|
+
dirty_flag = bool(G.graph.pop("_dnfr_prep_dirty", False))
|
|
804
|
+
existing_cache = cast(DnfrCache | None, graph.get("_dnfr_prep_cache"))
|
|
805
|
+
cache_timer = start_timer()
|
|
806
|
+
cache, idx, theta, epi, vf, cos_theta, sin_theta, refreshed = _init_dnfr_cache(
|
|
807
|
+
G,
|
|
808
|
+
nodes,
|
|
809
|
+
existing_cache,
|
|
810
|
+
checksum,
|
|
811
|
+
force_refresh=dirty_flag,
|
|
812
|
+
manager=manager,
|
|
221
813
|
)
|
|
814
|
+
stop_timer("dnfr_cache_rebuild", cache_timer)
|
|
815
|
+
dirty = dirty_flag or refreshed
|
|
816
|
+
caching_enabled = cache is not None and (cache_size is None or cache_size > 0)
|
|
817
|
+
result["cache"] = cache
|
|
818
|
+
result["idx"] = idx
|
|
819
|
+
result["theta"] = theta
|
|
820
|
+
result["epi"] = epi
|
|
821
|
+
result["vf"] = vf
|
|
822
|
+
result["cos_theta"] = cos_theta
|
|
823
|
+
result["sin_theta"] = sin_theta
|
|
222
824
|
if cache is not None:
|
|
223
825
|
_refresh_dnfr_vectors(G, nodes, cache)
|
|
826
|
+
if np_module is None and not caching_enabled:
|
|
827
|
+
for attr in (
|
|
828
|
+
"neighbor_x_np",
|
|
829
|
+
"neighbor_y_np",
|
|
830
|
+
"neighbor_epi_sum_np",
|
|
831
|
+
"neighbor_vf_sum_np",
|
|
832
|
+
"neighbor_count_np",
|
|
833
|
+
"neighbor_deg_sum_np",
|
|
834
|
+
"neighbor_inv_count_np",
|
|
835
|
+
"neighbor_cos_avg_np",
|
|
836
|
+
"neighbor_sin_avg_np",
|
|
837
|
+
"neighbor_mean_tmp_np",
|
|
838
|
+
"neighbor_mean_length_np",
|
|
839
|
+
"neighbor_accum_np",
|
|
840
|
+
"neighbor_edge_values_np",
|
|
841
|
+
):
|
|
842
|
+
setattr(cache, attr, None)
|
|
843
|
+
cache.neighbor_accum_signature = None
|
|
844
|
+
for attr in (
|
|
845
|
+
"th_bar_np",
|
|
846
|
+
"epi_bar_np",
|
|
847
|
+
"vf_bar_np",
|
|
848
|
+
"deg_bar_np",
|
|
849
|
+
"grad_phase_np",
|
|
850
|
+
"grad_epi_np",
|
|
851
|
+
"grad_vf_np",
|
|
852
|
+
"grad_topo_np",
|
|
853
|
+
"grad_total_np",
|
|
854
|
+
):
|
|
855
|
+
setattr(cache, attr, None)
|
|
224
856
|
|
|
225
857
|
w_phase = float(weights.get("phase", 0.0))
|
|
226
858
|
w_epi = float(weights.get("epi", 0.0))
|
|
227
859
|
w_vf = float(weights.get("vf", 0.0))
|
|
228
860
|
w_topo = float(weights.get("topo", 0.0))
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
861
|
+
result["w_phase"] = w_phase
|
|
862
|
+
result["w_epi"] = w_epi
|
|
863
|
+
result["w_vf"] = w_vf
|
|
864
|
+
result["w_topo"] = w_topo
|
|
865
|
+
degree_map = cast(dict[NodeId, float] | None, cache.degs if cache else None)
|
|
866
|
+
if cache is not None and dirty:
|
|
867
|
+
cache.degs = None
|
|
868
|
+
cache.deg_list = None
|
|
869
|
+
cache.deg_array = None
|
|
870
|
+
cache.edge_src = None
|
|
871
|
+
cache.edge_dst = None
|
|
872
|
+
cache.edge_signature = None
|
|
873
|
+
cache.neighbor_accum_signature = None
|
|
874
|
+
cache.neighbor_accum_np = None
|
|
875
|
+
cache.neighbor_edge_values_np = None
|
|
876
|
+
degree_map = None
|
|
239
877
|
|
|
240
878
|
deg_list: list[float] | None = None
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
879
|
+
degs: dict[NodeId, float] | None = None
|
|
880
|
+
deg_array: np.ndarray | None = None
|
|
881
|
+
|
|
882
|
+
if w_topo != 0.0 or caching_enabled:
|
|
883
|
+
if degree_map is None or len(degree_map) != len(G):
|
|
884
|
+
degree_map = {cast(NodeId, node): float(deg) for node, deg in G.degree()}
|
|
885
|
+
if cache is not None:
|
|
886
|
+
cache.degs = degree_map
|
|
887
|
+
|
|
888
|
+
if (
|
|
889
|
+
cache is not None
|
|
890
|
+
and cache.deg_list is not None
|
|
891
|
+
and not dirty
|
|
892
|
+
and len(cache.deg_list) == len(nodes)
|
|
893
|
+
):
|
|
894
|
+
deg_list = cache.deg_list
|
|
895
|
+
else:
|
|
896
|
+
deg_list = [float(degree_map.get(node, 0.0)) for node in nodes]
|
|
897
|
+
if cache is not None:
|
|
898
|
+
cache.deg_list = deg_list
|
|
899
|
+
|
|
900
|
+
degs = degree_map
|
|
901
|
+
|
|
902
|
+
if np_module is not None and deg_list is not None:
|
|
903
|
+
if cache is not None:
|
|
904
|
+
deg_array = _ensure_numpy_degrees(cache, deg_list, np_module)
|
|
905
|
+
else:
|
|
906
|
+
deg_array = np_module.array(deg_list, dtype=float)
|
|
907
|
+
elif cache is not None:
|
|
908
|
+
cache.deg_array = None
|
|
909
|
+
elif cache is not None and dirty:
|
|
246
910
|
cache.deg_list = None
|
|
911
|
+
cache.deg_array = None
|
|
912
|
+
|
|
913
|
+
G.graph["_dnfr_prep_dirty"] = False
|
|
247
914
|
|
|
248
|
-
|
|
915
|
+
result["degs"] = degs
|
|
916
|
+
result["deg_list"] = deg_list
|
|
917
|
+
|
|
918
|
+
theta_np: np.ndarray | None
|
|
919
|
+
epi_np: np.ndarray | None
|
|
920
|
+
vf_np: np.ndarray | None
|
|
921
|
+
cos_theta_np: np.ndarray | None
|
|
922
|
+
sin_theta_np: np.ndarray | None
|
|
923
|
+
edge_src: np.ndarray | None
|
|
924
|
+
edge_dst: np.ndarray | None
|
|
925
|
+
if use_numpy:
|
|
249
926
|
theta_np, epi_np, vf_np, cos_theta_np, sin_theta_np = _ensure_numpy_vectors(
|
|
250
|
-
cache,
|
|
927
|
+
cache, np_module
|
|
251
928
|
)
|
|
252
|
-
|
|
929
|
+
edge_src = None
|
|
930
|
+
edge_dst = None
|
|
931
|
+
if cache is not None:
|
|
932
|
+
edge_src = cache.edge_src
|
|
933
|
+
edge_dst = cache.edge_dst
|
|
934
|
+
if edge_src is None or edge_dst is None or dirty:
|
|
935
|
+
edge_src, edge_dst = _build_edge_index_arrays(G, nodes, idx, np_module)
|
|
936
|
+
cache.edge_src = edge_src
|
|
937
|
+
cache.edge_dst = edge_dst
|
|
938
|
+
else:
|
|
939
|
+
edge_src, edge_dst = _build_edge_index_arrays(G, nodes, idx, np_module)
|
|
940
|
+
|
|
941
|
+
if cache is not None:
|
|
942
|
+
for attr in ("neighbor_accum_np", "neighbor_edge_values_np"):
|
|
943
|
+
arr = getattr(cache, attr, None)
|
|
944
|
+
if arr is not None:
|
|
945
|
+
result[attr] = arr
|
|
946
|
+
if edge_src is not None and edge_dst is not None:
|
|
947
|
+
signature = (id(edge_src), id(edge_dst), len(nodes))
|
|
948
|
+
result["edge_signature"] = signature
|
|
949
|
+
if cache is not None:
|
|
950
|
+
cache.edge_signature = signature
|
|
253
951
|
else:
|
|
254
952
|
theta_np = None
|
|
255
953
|
epi_np = None
|
|
256
954
|
vf_np = None
|
|
257
955
|
cos_theta_np = None
|
|
258
956
|
sin_theta_np = None
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
"
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
"
|
|
276
|
-
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
"
|
|
280
|
-
|
|
281
|
-
"
|
|
282
|
-
|
|
283
|
-
"
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
957
|
+
edge_src = None
|
|
958
|
+
edge_dst = None
|
|
959
|
+
if cache is not None:
|
|
960
|
+
cache.edge_src = None
|
|
961
|
+
cache.edge_dst = None
|
|
962
|
+
|
|
963
|
+
result.setdefault("neighbor_edge_values_np", None)
|
|
964
|
+
if cache is not None and "edge_signature" not in result:
|
|
965
|
+
result["edge_signature"] = cache.edge_signature
|
|
966
|
+
|
|
967
|
+
result["theta_np"] = theta_np
|
|
968
|
+
result["epi_np"] = epi_np
|
|
969
|
+
result["vf_np"] = vf_np
|
|
970
|
+
result["cos_theta_np"] = cos_theta_np
|
|
971
|
+
result["sin_theta_np"] = sin_theta_np
|
|
972
|
+
if theta_np is not None and getattr(theta_np, "shape", None) == (len(nodes),):
|
|
973
|
+
result["theta"] = theta_np
|
|
974
|
+
if epi_np is not None and getattr(epi_np, "shape", None) == (len(nodes),):
|
|
975
|
+
result["epi"] = epi_np
|
|
976
|
+
if vf_np is not None and getattr(vf_np, "shape", None) == (len(nodes),):
|
|
977
|
+
result["vf"] = vf_np
|
|
978
|
+
if cos_theta_np is not None and getattr(cos_theta_np, "shape", None) == (len(nodes),):
|
|
979
|
+
result["cos_theta"] = cos_theta_np
|
|
980
|
+
if sin_theta_np is not None and getattr(sin_theta_np, "shape", None) == (len(nodes),):
|
|
981
|
+
result["sin_theta"] = sin_theta_np
|
|
982
|
+
result["deg_array"] = deg_array
|
|
983
|
+
result["edge_src"] = edge_src
|
|
984
|
+
result["edge_dst"] = edge_dst
|
|
985
|
+
result["edge_count"] = edge_count
|
|
986
|
+
result["prefer_sparse"] = prefer_sparse
|
|
987
|
+
result["dense_override"] = dense_override
|
|
988
|
+
result.setdefault("neighbor_accum_np", None)
|
|
989
|
+
result.setdefault("neighbor_accum_signature", None)
|
|
990
|
+
|
|
991
|
+
return result
|
|
287
992
|
|
|
288
993
|
|
|
289
994
|
def _apply_dnfr_gradients(
|
|
290
|
-
G,
|
|
291
|
-
data,
|
|
292
|
-
th_bar,
|
|
293
|
-
epi_bar,
|
|
294
|
-
vf_bar,
|
|
295
|
-
deg_bar=None,
|
|
296
|
-
degs=None,
|
|
297
|
-
|
|
298
|
-
|
|
995
|
+
G: TNFRGraph,
|
|
996
|
+
data: MutableMapping[str, Any],
|
|
997
|
+
th_bar: Sequence[float] | np.ndarray,
|
|
998
|
+
epi_bar: Sequence[float] | np.ndarray,
|
|
999
|
+
vf_bar: Sequence[float] | np.ndarray,
|
|
1000
|
+
deg_bar: Sequence[float] | np.ndarray | None = None,
|
|
1001
|
+
degs: Mapping[Any, float] | Sequence[float] | np.ndarray | None = None,
|
|
1002
|
+
*,
|
|
1003
|
+
n_jobs: int | None = None,
|
|
1004
|
+
profile: MutableMapping[str, float] | None = None,
|
|
1005
|
+
) -> None:
|
|
1006
|
+
"""Combine precomputed gradients and write ΔNFR to each node.
|
|
1007
|
+
|
|
1008
|
+
Parameters
|
|
1009
|
+
----------
|
|
1010
|
+
profile : MutableMapping[str, float] or None, optional
|
|
1011
|
+
Mutable mapping receiving aggregated timings for the gradient assembly
|
|
1012
|
+
(``"dnfr_gradient_assembly"``) and in-place writes
|
|
1013
|
+
(``"dnfr_inplace_write"``).
|
|
1014
|
+
"""
|
|
1015
|
+
start_timer, stop_timer = _profile_start_stop(
|
|
1016
|
+
profile,
|
|
1017
|
+
keys=("dnfr_gradient_assembly", "dnfr_inplace_write"),
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
np = get_numpy()
|
|
299
1021
|
nodes = data["nodes"]
|
|
300
1022
|
theta = data["theta"]
|
|
301
1023
|
epi = data["epi"]
|
|
@@ -307,25 +1029,146 @@ def _apply_dnfr_gradients(
|
|
|
307
1029
|
if degs is None:
|
|
308
1030
|
degs = data.get("degs")
|
|
309
1031
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1032
|
+
cache: DnfrCache | None = data.get("cache")
|
|
1033
|
+
|
|
1034
|
+
theta_np = data.get("theta_np")
|
|
1035
|
+
epi_np = data.get("epi_np")
|
|
1036
|
+
vf_np = data.get("vf_np")
|
|
1037
|
+
deg_array = data.get("deg_array") if w_topo != 0.0 else None
|
|
1038
|
+
|
|
1039
|
+
use_vector = (
|
|
1040
|
+
np is not None
|
|
1041
|
+
and theta_np is not None
|
|
1042
|
+
and epi_np is not None
|
|
1043
|
+
and vf_np is not None
|
|
1044
|
+
and isinstance(th_bar, np.ndarray)
|
|
1045
|
+
and isinstance(epi_bar, np.ndarray)
|
|
1046
|
+
and isinstance(vf_bar, np.ndarray)
|
|
1047
|
+
)
|
|
1048
|
+
if use_vector and w_topo != 0.0:
|
|
1049
|
+
use_vector = (
|
|
1050
|
+
deg_bar is not None
|
|
1051
|
+
and isinstance(deg_bar, np.ndarray)
|
|
1052
|
+
and isinstance(deg_array, np.ndarray)
|
|
324
1053
|
)
|
|
325
|
-
set_dnfr(G, n, float(dnfr))
|
|
326
1054
|
|
|
1055
|
+
grad_timer = start_timer()
|
|
1056
|
+
|
|
1057
|
+
if use_vector:
|
|
1058
|
+
grad_phase = _ensure_cached_array(cache, "grad_phase_np", theta_np.shape, np)
|
|
1059
|
+
grad_epi = _ensure_cached_array(cache, "grad_epi_np", epi_np.shape, np)
|
|
1060
|
+
grad_vf = _ensure_cached_array(cache, "grad_vf_np", vf_np.shape, np)
|
|
1061
|
+
grad_total = _ensure_cached_array(cache, "grad_total_np", theta_np.shape, np)
|
|
1062
|
+
grad_topo = None
|
|
1063
|
+
if w_topo != 0.0:
|
|
1064
|
+
grad_topo = _ensure_cached_array(cache, "grad_topo_np", deg_array.shape, np)
|
|
1065
|
+
|
|
1066
|
+
angle_diff_array(theta_np, th_bar, np=np, out=grad_phase)
|
|
1067
|
+
np.multiply(grad_phase, -1.0 / math.pi, out=grad_phase)
|
|
1068
|
+
|
|
1069
|
+
np.copyto(grad_epi, epi_bar, casting="unsafe")
|
|
1070
|
+
grad_epi -= epi_np
|
|
1071
|
+
|
|
1072
|
+
np.copyto(grad_vf, vf_bar, casting="unsafe")
|
|
1073
|
+
grad_vf -= vf_np
|
|
1074
|
+
|
|
1075
|
+
if grad_topo is not None and deg_bar is not None:
|
|
1076
|
+
np.copyto(grad_topo, deg_bar, casting="unsafe")
|
|
1077
|
+
grad_topo -= deg_array
|
|
327
1078
|
|
|
328
|
-
|
|
1079
|
+
if w_phase != 0.0:
|
|
1080
|
+
np.multiply(grad_phase, w_phase, out=grad_total)
|
|
1081
|
+
else:
|
|
1082
|
+
grad_total.fill(0.0)
|
|
1083
|
+
if w_epi != 0.0:
|
|
1084
|
+
if w_epi != 1.0:
|
|
1085
|
+
np.multiply(grad_epi, w_epi, out=grad_epi)
|
|
1086
|
+
np.add(grad_total, grad_epi, out=grad_total)
|
|
1087
|
+
if w_vf != 0.0:
|
|
1088
|
+
if w_vf != 1.0:
|
|
1089
|
+
np.multiply(grad_vf, w_vf, out=grad_vf)
|
|
1090
|
+
np.add(grad_total, grad_vf, out=grad_total)
|
|
1091
|
+
if w_topo != 0.0 and grad_topo is not None:
|
|
1092
|
+
if w_topo != 1.0:
|
|
1093
|
+
np.multiply(grad_topo, w_topo, out=grad_topo)
|
|
1094
|
+
np.add(grad_total, grad_topo, out=grad_total)
|
|
1095
|
+
|
|
1096
|
+
dnfr_values = grad_total
|
|
1097
|
+
else:
|
|
1098
|
+
effective_jobs = _resolve_parallel_jobs(n_jobs, len(nodes))
|
|
1099
|
+
if effective_jobs:
|
|
1100
|
+
chunk_results = []
|
|
1101
|
+
with ProcessPoolExecutor(max_workers=effective_jobs) as executor:
|
|
1102
|
+
futures = []
|
|
1103
|
+
for start, end in _iter_chunk_offsets(len(nodes), effective_jobs):
|
|
1104
|
+
if start == end:
|
|
1105
|
+
continue
|
|
1106
|
+
futures.append(
|
|
1107
|
+
executor.submit(
|
|
1108
|
+
_dnfr_gradients_worker,
|
|
1109
|
+
start,
|
|
1110
|
+
end,
|
|
1111
|
+
nodes,
|
|
1112
|
+
theta,
|
|
1113
|
+
epi,
|
|
1114
|
+
vf,
|
|
1115
|
+
th_bar,
|
|
1116
|
+
epi_bar,
|
|
1117
|
+
vf_bar,
|
|
1118
|
+
deg_bar,
|
|
1119
|
+
degs,
|
|
1120
|
+
w_phase,
|
|
1121
|
+
w_epi,
|
|
1122
|
+
w_vf,
|
|
1123
|
+
w_topo,
|
|
1124
|
+
)
|
|
1125
|
+
)
|
|
1126
|
+
for future in futures:
|
|
1127
|
+
chunk_results.append(future.result())
|
|
1128
|
+
|
|
1129
|
+
dnfr_values = [0.0] * len(nodes)
|
|
1130
|
+
for start, chunk in sorted(chunk_results, key=lambda item: item[0]):
|
|
1131
|
+
end = start + len(chunk)
|
|
1132
|
+
dnfr_values[start:end] = chunk
|
|
1133
|
+
else:
|
|
1134
|
+
dnfr_values = []
|
|
1135
|
+
for i, n in enumerate(nodes):
|
|
1136
|
+
g_phase = -angle_diff(theta[i], th_bar[i]) / math.pi
|
|
1137
|
+
g_epi = epi_bar[i] - epi[i]
|
|
1138
|
+
g_vf = vf_bar[i] - vf[i]
|
|
1139
|
+
if w_topo != 0.0 and deg_bar is not None and degs is not None:
|
|
1140
|
+
if isinstance(degs, dict):
|
|
1141
|
+
deg_i = float(degs.get(n, 0))
|
|
1142
|
+
else:
|
|
1143
|
+
deg_i = float(degs[i])
|
|
1144
|
+
g_topo = deg_bar[i] - deg_i
|
|
1145
|
+
else:
|
|
1146
|
+
g_topo = 0.0
|
|
1147
|
+
dnfr_values.append(
|
|
1148
|
+
w_phase * g_phase + w_epi * g_epi + w_vf * g_vf + w_topo * g_topo
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
if cache is not None:
|
|
1152
|
+
cache.grad_phase_np = None
|
|
1153
|
+
cache.grad_epi_np = None
|
|
1154
|
+
cache.grad_vf_np = None
|
|
1155
|
+
cache.grad_topo_np = None
|
|
1156
|
+
cache.grad_total_np = None
|
|
1157
|
+
|
|
1158
|
+
stop_timer("dnfr_gradient_assembly", grad_timer)
|
|
1159
|
+
|
|
1160
|
+
write_timer = start_timer()
|
|
1161
|
+
for i, n in enumerate(nodes):
|
|
1162
|
+
set_dnfr(G, n, float(dnfr_values[i]))
|
|
1163
|
+
stop_timer("dnfr_inplace_write", write_timer)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _init_bar_arrays(
|
|
1167
|
+
data: MutableMapping[str, Any],
|
|
1168
|
+
*,
|
|
1169
|
+
degs: Mapping[Any, float] | Sequence[float] | None = None,
|
|
1170
|
+
np: ModuleType | None = None,
|
|
1171
|
+
) -> tuple[Sequence[float], Sequence[float], Sequence[float], Sequence[float] | None]:
|
|
329
1172
|
"""Prepare containers for neighbour means.
|
|
330
1173
|
|
|
331
1174
|
If ``np`` is provided, NumPy arrays are created; otherwise lists are used.
|
|
@@ -333,97 +1176,234 @@ def _init_bar_arrays(data, *, degs=None, np=None):
|
|
|
333
1176
|
active.
|
|
334
1177
|
"""
|
|
335
1178
|
|
|
1179
|
+
nodes = data["nodes"]
|
|
336
1180
|
theta = data["theta"]
|
|
337
1181
|
epi = data["epi"]
|
|
338
1182
|
vf = data["vf"]
|
|
339
1183
|
w_topo = data["w_topo"]
|
|
1184
|
+
cache: DnfrCache | None = data.get("cache")
|
|
340
1185
|
if np is None:
|
|
341
1186
|
np = get_numpy()
|
|
342
1187
|
if np is not None:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
1188
|
+
size = len(theta)
|
|
1189
|
+
if cache is not None:
|
|
1190
|
+
th_bar = cache.th_bar_np
|
|
1191
|
+
if th_bar is None or getattr(th_bar, "shape", None) != (size,):
|
|
1192
|
+
th_bar = np.array(theta, dtype=float)
|
|
1193
|
+
else:
|
|
1194
|
+
np.copyto(th_bar, theta, casting="unsafe")
|
|
1195
|
+
cache.th_bar_np = th_bar
|
|
1196
|
+
|
|
1197
|
+
epi_bar = cache.epi_bar_np
|
|
1198
|
+
if epi_bar is None or getattr(epi_bar, "shape", None) != (size,):
|
|
1199
|
+
epi_bar = np.array(epi, dtype=float)
|
|
1200
|
+
else:
|
|
1201
|
+
np.copyto(epi_bar, epi, casting="unsafe")
|
|
1202
|
+
cache.epi_bar_np = epi_bar
|
|
1203
|
+
|
|
1204
|
+
vf_bar = cache.vf_bar_np
|
|
1205
|
+
if vf_bar is None or getattr(vf_bar, "shape", None) != (size,):
|
|
1206
|
+
vf_bar = np.array(vf, dtype=float)
|
|
1207
|
+
else:
|
|
1208
|
+
np.copyto(vf_bar, vf, casting="unsafe")
|
|
1209
|
+
cache.vf_bar_np = vf_bar
|
|
1210
|
+
|
|
1211
|
+
if w_topo != 0.0 and degs is not None:
|
|
1212
|
+
if isinstance(degs, dict):
|
|
1213
|
+
deg_size = len(nodes)
|
|
1214
|
+
else:
|
|
1215
|
+
deg_size = len(degs)
|
|
1216
|
+
deg_bar = cache.deg_bar_np
|
|
1217
|
+
if deg_bar is None or getattr(deg_bar, "shape", None) != (deg_size,):
|
|
1218
|
+
if isinstance(degs, dict):
|
|
1219
|
+
deg_bar = np.array(
|
|
1220
|
+
[float(degs.get(node, 0.0)) for node in nodes],
|
|
1221
|
+
dtype=float,
|
|
1222
|
+
)
|
|
1223
|
+
else:
|
|
1224
|
+
deg_bar = np.array(degs, dtype=float)
|
|
1225
|
+
else:
|
|
1226
|
+
if isinstance(degs, dict):
|
|
1227
|
+
for i, node in enumerate(nodes):
|
|
1228
|
+
deg_bar[i] = float(degs.get(node, 0.0))
|
|
1229
|
+
else:
|
|
1230
|
+
np.copyto(deg_bar, degs, casting="unsafe")
|
|
1231
|
+
cache.deg_bar_np = deg_bar
|
|
1232
|
+
else:
|
|
1233
|
+
deg_bar = None
|
|
1234
|
+
if cache is not None:
|
|
1235
|
+
cache.deg_bar_np = None
|
|
1236
|
+
else:
|
|
1237
|
+
th_bar = np.array(theta, dtype=float)
|
|
1238
|
+
epi_bar = np.array(epi, dtype=float)
|
|
1239
|
+
vf_bar = np.array(vf, dtype=float)
|
|
1240
|
+
deg_bar = (
|
|
1241
|
+
np.array(degs, dtype=float)
|
|
1242
|
+
if w_topo != 0.0 and degs is not None
|
|
1243
|
+
else None
|
|
1244
|
+
)
|
|
351
1245
|
else:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
1246
|
+
size = len(theta)
|
|
1247
|
+
if cache is not None:
|
|
1248
|
+
th_bar = cache.th_bar
|
|
1249
|
+
if th_bar is None or len(th_bar) != size:
|
|
1250
|
+
th_bar = [0.0] * size
|
|
1251
|
+
th_bar[:] = theta
|
|
1252
|
+
cache.th_bar = th_bar
|
|
1253
|
+
|
|
1254
|
+
epi_bar = cache.epi_bar
|
|
1255
|
+
if epi_bar is None or len(epi_bar) != size:
|
|
1256
|
+
epi_bar = [0.0] * size
|
|
1257
|
+
epi_bar[:] = epi
|
|
1258
|
+
cache.epi_bar = epi_bar
|
|
1259
|
+
|
|
1260
|
+
vf_bar = cache.vf_bar
|
|
1261
|
+
if vf_bar is None or len(vf_bar) != size:
|
|
1262
|
+
vf_bar = [0.0] * size
|
|
1263
|
+
vf_bar[:] = vf
|
|
1264
|
+
cache.vf_bar = vf_bar
|
|
1265
|
+
|
|
1266
|
+
if w_topo != 0.0 and degs is not None:
|
|
1267
|
+
if isinstance(degs, dict):
|
|
1268
|
+
deg_size = len(nodes)
|
|
1269
|
+
else:
|
|
1270
|
+
deg_size = len(degs)
|
|
1271
|
+
deg_bar = cache.deg_bar
|
|
1272
|
+
if deg_bar is None or len(deg_bar) != deg_size:
|
|
1273
|
+
deg_bar = [0.0] * deg_size
|
|
1274
|
+
if isinstance(degs, dict):
|
|
1275
|
+
for i, node in enumerate(nodes):
|
|
1276
|
+
deg_bar[i] = float(degs.get(node, 0.0))
|
|
1277
|
+
else:
|
|
1278
|
+
for i, value in enumerate(degs):
|
|
1279
|
+
deg_bar[i] = float(value)
|
|
1280
|
+
cache.deg_bar = deg_bar
|
|
1281
|
+
else:
|
|
1282
|
+
deg_bar = None
|
|
1283
|
+
cache.deg_bar = None
|
|
1284
|
+
else:
|
|
1285
|
+
th_bar = list(theta)
|
|
1286
|
+
epi_bar = list(epi)
|
|
1287
|
+
vf_bar = list(vf)
|
|
1288
|
+
deg_bar = list(degs) if w_topo != 0.0 and degs is not None else None
|
|
356
1289
|
return th_bar, epi_bar, vf_bar, deg_bar
|
|
357
1290
|
|
|
358
1291
|
|
|
359
1292
|
def _compute_neighbor_means(
|
|
360
|
-
G,
|
|
361
|
-
data,
|
|
1293
|
+
G: TNFRGraph,
|
|
1294
|
+
data: MutableMapping[str, Any],
|
|
362
1295
|
*,
|
|
363
|
-
x,
|
|
364
|
-
y,
|
|
365
|
-
epi_sum,
|
|
366
|
-
vf_sum,
|
|
367
|
-
count,
|
|
368
|
-
deg_sum=None,
|
|
369
|
-
degs=None,
|
|
370
|
-
np=None,
|
|
371
|
-
):
|
|
1296
|
+
x: Sequence[float],
|
|
1297
|
+
y: Sequence[float],
|
|
1298
|
+
epi_sum: Sequence[float],
|
|
1299
|
+
vf_sum: Sequence[float],
|
|
1300
|
+
count: Sequence[float] | np.ndarray,
|
|
1301
|
+
deg_sum: Sequence[float] | None = None,
|
|
1302
|
+
degs: Mapping[Any, float] | Sequence[float] | None = None,
|
|
1303
|
+
np: ModuleType | None = None,
|
|
1304
|
+
) -> tuple[Sequence[float], Sequence[float], Sequence[float], Sequence[float] | None]:
|
|
372
1305
|
"""Return neighbour mean arrays for ΔNFR."""
|
|
373
1306
|
w_topo = data["w_topo"]
|
|
374
1307
|
theta = data["theta"]
|
|
1308
|
+
cache: DnfrCache | None = data.get("cache")
|
|
375
1309
|
is_numpy = np is not None and isinstance(count, np.ndarray)
|
|
376
1310
|
th_bar, epi_bar, vf_bar, deg_bar = _init_bar_arrays(
|
|
377
1311
|
data, degs=degs, np=np if is_numpy else None
|
|
378
1312
|
)
|
|
379
1313
|
|
|
380
1314
|
if is_numpy:
|
|
1315
|
+
n = count.shape[0]
|
|
381
1316
|
mask = count > 0
|
|
382
|
-
if np.any(mask):
|
|
383
|
-
th_bar
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
1317
|
+
if not np.any(mask):
|
|
1318
|
+
return th_bar, epi_bar, vf_bar, deg_bar
|
|
1319
|
+
|
|
1320
|
+
inv = _ensure_cached_array(cache, "neighbor_inv_count_np", (n,), np)
|
|
1321
|
+
inv.fill(0.0)
|
|
1322
|
+
np.divide(1.0, count, out=inv, where=mask)
|
|
1323
|
+
|
|
1324
|
+
cos_avg = _ensure_cached_array(cache, "neighbor_cos_avg_np", (n,), np)
|
|
1325
|
+
cos_avg.fill(0.0)
|
|
1326
|
+
np.multiply(x, inv, out=cos_avg, where=mask)
|
|
1327
|
+
|
|
1328
|
+
sin_avg = _ensure_cached_array(cache, "neighbor_sin_avg_np", (n,), np)
|
|
1329
|
+
sin_avg.fill(0.0)
|
|
1330
|
+
np.multiply(y, inv, out=sin_avg, where=mask)
|
|
1331
|
+
|
|
1332
|
+
lengths = _ensure_cached_array(cache, "neighbor_mean_length_np", (n,), np)
|
|
1333
|
+
np.hypot(cos_avg, sin_avg, out=lengths)
|
|
1334
|
+
|
|
1335
|
+
temp = _ensure_cached_array(cache, "neighbor_mean_tmp_np", (n,), np)
|
|
1336
|
+
np.arctan2(sin_avg, cos_avg, out=temp)
|
|
1337
|
+
|
|
1338
|
+
theta_src = data.get("theta_np")
|
|
1339
|
+
if theta_src is None:
|
|
1340
|
+
theta_src = np.asarray(theta, dtype=float)
|
|
1341
|
+
zero_mask = lengths <= _MEAN_VECTOR_EPS
|
|
1342
|
+
np.copyto(temp, theta_src, where=zero_mask)
|
|
1343
|
+
np.copyto(th_bar, temp, where=mask, casting="unsafe")
|
|
1344
|
+
|
|
1345
|
+
np.divide(epi_sum, count, out=epi_bar, where=mask)
|
|
1346
|
+
np.divide(vf_sum, count, out=vf_bar, where=mask)
|
|
1347
|
+
if w_topo != 0.0 and deg_bar is not None and deg_sum is not None:
|
|
1348
|
+
np.divide(deg_sum, count, out=deg_bar, where=mask)
|
|
390
1349
|
return th_bar, epi_bar, vf_bar, deg_bar
|
|
391
1350
|
|
|
392
1351
|
n = len(theta)
|
|
393
|
-
cos_th = data["cos_theta"]
|
|
394
|
-
sin_th = data["sin_theta"]
|
|
395
|
-
idx = data["idx"]
|
|
396
|
-
nodes = data["nodes"]
|
|
397
|
-
deg_list = data.get("deg_list")
|
|
398
1352
|
for i in range(n):
|
|
399
1353
|
c = count[i]
|
|
400
|
-
if c:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
1354
|
+
if not c:
|
|
1355
|
+
continue
|
|
1356
|
+
inv = 1.0 / float(c)
|
|
1357
|
+
cos_avg = x[i] * inv
|
|
1358
|
+
sin_avg = y[i] * inv
|
|
1359
|
+
if math.hypot(cos_avg, sin_avg) <= _MEAN_VECTOR_EPS:
|
|
1360
|
+
th_bar[i] = theta[i]
|
|
1361
|
+
else:
|
|
1362
|
+
th_bar[i] = math.atan2(sin_avg, cos_avg)
|
|
1363
|
+
epi_bar[i] = epi_sum[i] * inv
|
|
1364
|
+
vf_bar[i] = vf_sum[i] * inv
|
|
1365
|
+
if w_topo != 0.0 and deg_bar is not None and deg_sum is not None:
|
|
1366
|
+
deg_bar[i] = deg_sum[i] * inv
|
|
410
1367
|
return th_bar, epi_bar, vf_bar, deg_bar
|
|
411
1368
|
|
|
412
1369
|
|
|
413
1370
|
def _compute_dnfr_common(
|
|
414
|
-
G,
|
|
415
|
-
data,
|
|
1371
|
+
G: TNFRGraph,
|
|
1372
|
+
data: MutableMapping[str, Any],
|
|
416
1373
|
*,
|
|
417
|
-
x,
|
|
418
|
-
y,
|
|
419
|
-
epi_sum,
|
|
420
|
-
vf_sum,
|
|
421
|
-
count,
|
|
422
|
-
deg_sum=None,
|
|
423
|
-
degs=None,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1374
|
+
x: Sequence[float],
|
|
1375
|
+
y: Sequence[float],
|
|
1376
|
+
epi_sum: Sequence[float],
|
|
1377
|
+
vf_sum: Sequence[float],
|
|
1378
|
+
count: Sequence[float] | None,
|
|
1379
|
+
deg_sum: Sequence[float] | None = None,
|
|
1380
|
+
degs: Sequence[float] | None = None,
|
|
1381
|
+
n_jobs: int | None = None,
|
|
1382
|
+
profile: MutableMapping[str, float] | None = None,
|
|
1383
|
+
) -> None:
|
|
1384
|
+
"""Compute neighbour means and apply ΔNFR gradients.
|
|
1385
|
+
|
|
1386
|
+
Parameters
|
|
1387
|
+
----------
|
|
1388
|
+
profile : MutableMapping[str, float] or None, optional
|
|
1389
|
+
Mutable mapping that records wall-clock durations for the neighbour
|
|
1390
|
+
mean computation (``"dnfr_neighbor_means"``), the gradient assembly
|
|
1391
|
+
(``"dnfr_gradient_assembly"``) and the final in-place writes to the
|
|
1392
|
+
graph (``"dnfr_inplace_write"``).
|
|
1393
|
+
"""
|
|
1394
|
+
start_timer, stop_timer = _profile_start_stop(
|
|
1395
|
+
profile,
|
|
1396
|
+
keys=("dnfr_neighbor_means", "dnfr_gradient_assembly", "dnfr_inplace_write"),
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1399
|
+
np_module = get_numpy()
|
|
1400
|
+
if np_module is not None and isinstance(
|
|
1401
|
+
count, getattr(np_module, "ndarray", tuple)
|
|
1402
|
+
):
|
|
1403
|
+
np_arg = np_module
|
|
1404
|
+
else:
|
|
1405
|
+
np_arg = None
|
|
1406
|
+
neighbor_timer = start_timer()
|
|
427
1407
|
th_bar, epi_bar, vf_bar, deg_bar = _compute_neighbor_means(
|
|
428
1408
|
G,
|
|
429
1409
|
data,
|
|
@@ -434,121 +1414,724 @@ def _compute_dnfr_common(
|
|
|
434
1414
|
count=count,
|
|
435
1415
|
deg_sum=deg_sum,
|
|
436
1416
|
degs=degs,
|
|
437
|
-
np=
|
|
1417
|
+
np=np_arg,
|
|
1418
|
+
)
|
|
1419
|
+
stop_timer("dnfr_neighbor_means", neighbor_timer)
|
|
1420
|
+
_apply_dnfr_gradients(
|
|
1421
|
+
G,
|
|
1422
|
+
data,
|
|
1423
|
+
th_bar,
|
|
1424
|
+
epi_bar,
|
|
1425
|
+
vf_bar,
|
|
1426
|
+
deg_bar,
|
|
1427
|
+
degs,
|
|
1428
|
+
n_jobs=n_jobs,
|
|
1429
|
+
profile=profile,
|
|
438
1430
|
)
|
|
439
|
-
_apply_dnfr_gradients(G, data, th_bar, epi_bar, vf_bar, deg_bar, degs)
|
|
440
1431
|
|
|
441
1432
|
|
|
442
|
-
def
|
|
1433
|
+
def _reset_numpy_buffer(
|
|
1434
|
+
buffer: np.ndarray | None,
|
|
1435
|
+
size: int,
|
|
1436
|
+
np: ModuleType,
|
|
1437
|
+
) -> np.ndarray:
|
|
1438
|
+
if (
|
|
1439
|
+
buffer is None
|
|
1440
|
+
or getattr(buffer, "shape", None) is None
|
|
1441
|
+
or buffer.shape[0] != size
|
|
1442
|
+
):
|
|
1443
|
+
return np.zeros(size, dtype=float)
|
|
1444
|
+
buffer.fill(0.0)
|
|
1445
|
+
return buffer
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
def _init_neighbor_sums(
|
|
1449
|
+
data: MutableMapping[str, Any],
|
|
1450
|
+
*,
|
|
1451
|
+
np: ModuleType | None = None,
|
|
1452
|
+
) -> NeighborStats:
|
|
443
1453
|
"""Initialise containers for neighbour sums."""
|
|
444
1454
|
nodes = data["nodes"]
|
|
445
1455
|
n = len(nodes)
|
|
446
1456
|
w_topo = data["w_topo"]
|
|
1457
|
+
cache: DnfrCache | None = data.get("cache")
|
|
1458
|
+
|
|
1459
|
+
def _reset_list(buffer: list[float] | None, value: float = 0.0) -> list[float]:
|
|
1460
|
+
if buffer is None or len(buffer) != n:
|
|
1461
|
+
return [value] * n
|
|
1462
|
+
for i in range(n):
|
|
1463
|
+
buffer[i] = value
|
|
1464
|
+
return buffer
|
|
1465
|
+
|
|
447
1466
|
if np is not None:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
1467
|
+
if cache is not None:
|
|
1468
|
+
x = cache.neighbor_x_np
|
|
1469
|
+
y = cache.neighbor_y_np
|
|
1470
|
+
epi_sum = cache.neighbor_epi_sum_np
|
|
1471
|
+
vf_sum = cache.neighbor_vf_sum_np
|
|
1472
|
+
count = cache.neighbor_count_np
|
|
1473
|
+
x = _reset_numpy_buffer(x, n, np)
|
|
1474
|
+
y = _reset_numpy_buffer(y, n, np)
|
|
1475
|
+
epi_sum = _reset_numpy_buffer(epi_sum, n, np)
|
|
1476
|
+
vf_sum = _reset_numpy_buffer(vf_sum, n, np)
|
|
1477
|
+
count = _reset_numpy_buffer(count, n, np)
|
|
1478
|
+
cache.neighbor_x_np = x
|
|
1479
|
+
cache.neighbor_y_np = y
|
|
1480
|
+
cache.neighbor_epi_sum_np = epi_sum
|
|
1481
|
+
cache.neighbor_vf_sum_np = vf_sum
|
|
1482
|
+
cache.neighbor_count_np = count
|
|
1483
|
+
cache.neighbor_x = _reset_list(cache.neighbor_x)
|
|
1484
|
+
cache.neighbor_y = _reset_list(cache.neighbor_y)
|
|
1485
|
+
cache.neighbor_epi_sum = _reset_list(cache.neighbor_epi_sum)
|
|
1486
|
+
cache.neighbor_vf_sum = _reset_list(cache.neighbor_vf_sum)
|
|
1487
|
+
cache.neighbor_count = _reset_list(cache.neighbor_count)
|
|
1488
|
+
if w_topo != 0.0:
|
|
1489
|
+
deg_sum = _reset_numpy_buffer(cache.neighbor_deg_sum_np, n, np)
|
|
1490
|
+
cache.neighbor_deg_sum_np = deg_sum
|
|
1491
|
+
cache.neighbor_deg_sum = _reset_list(cache.neighbor_deg_sum)
|
|
1492
|
+
else:
|
|
1493
|
+
cache.neighbor_deg_sum_np = None
|
|
1494
|
+
cache.neighbor_deg_sum = None
|
|
1495
|
+
deg_sum = None
|
|
1496
|
+
else:
|
|
1497
|
+
x = np.zeros(n, dtype=float)
|
|
1498
|
+
y = np.zeros(n, dtype=float)
|
|
1499
|
+
epi_sum = np.zeros(n, dtype=float)
|
|
1500
|
+
vf_sum = np.zeros(n, dtype=float)
|
|
1501
|
+
count = np.zeros(n, dtype=float)
|
|
1502
|
+
deg_sum = np.zeros(n, dtype=float) if w_topo != 0.0 else None
|
|
454
1503
|
degs = None
|
|
455
1504
|
else:
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
1505
|
+
if cache is not None:
|
|
1506
|
+
x = _reset_list(cache.neighbor_x)
|
|
1507
|
+
y = _reset_list(cache.neighbor_y)
|
|
1508
|
+
epi_sum = _reset_list(cache.neighbor_epi_sum)
|
|
1509
|
+
vf_sum = _reset_list(cache.neighbor_vf_sum)
|
|
1510
|
+
count = _reset_list(cache.neighbor_count)
|
|
1511
|
+
cache.neighbor_x = x
|
|
1512
|
+
cache.neighbor_y = y
|
|
1513
|
+
cache.neighbor_epi_sum = epi_sum
|
|
1514
|
+
cache.neighbor_vf_sum = vf_sum
|
|
1515
|
+
cache.neighbor_count = count
|
|
1516
|
+
if w_topo != 0.0:
|
|
1517
|
+
deg_sum = _reset_list(cache.neighbor_deg_sum)
|
|
1518
|
+
cache.neighbor_deg_sum = deg_sum
|
|
1519
|
+
else:
|
|
1520
|
+
cache.neighbor_deg_sum = None
|
|
1521
|
+
deg_sum = None
|
|
1522
|
+
else:
|
|
1523
|
+
x = [0.0] * n
|
|
1524
|
+
y = [0.0] * n
|
|
1525
|
+
epi_sum = [0.0] * n
|
|
1526
|
+
vf_sum = [0.0] * n
|
|
1527
|
+
count = [0.0] * n
|
|
1528
|
+
deg_sum = [0.0] * n if w_topo != 0.0 else None
|
|
461
1529
|
deg_list = data.get("deg_list")
|
|
462
|
-
if w_topo != 0 and deg_list is not None:
|
|
463
|
-
|
|
464
|
-
degs = list(deg_list)
|
|
1530
|
+
if w_topo != 0.0 and deg_list is not None:
|
|
1531
|
+
degs = deg_list
|
|
465
1532
|
else:
|
|
466
|
-
deg_sum = None
|
|
467
1533
|
degs = None
|
|
468
1534
|
return x, y, epi_sum, vf_sum, count, deg_sum, degs
|
|
469
1535
|
|
|
470
1536
|
|
|
471
|
-
def
|
|
472
|
-
|
|
1537
|
+
def _prefer_sparse_accumulation(n: int, edge_count: int | None) -> bool:
|
|
1538
|
+
"""Return ``True`` when neighbour sums should use edge accumulation."""
|
|
1539
|
+
|
|
1540
|
+
if n <= 1 or not edge_count:
|
|
1541
|
+
return False
|
|
1542
|
+
possible_edges = n * (n - 1)
|
|
1543
|
+
if possible_edges <= 0:
|
|
1544
|
+
return False
|
|
1545
|
+
density = edge_count / possible_edges
|
|
1546
|
+
return density <= _SPARSE_DENSITY_THRESHOLD
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def _accumulate_neighbors_dense(
|
|
1550
|
+
G: TNFRGraph,
|
|
1551
|
+
data: MutableMapping[str, Any],
|
|
1552
|
+
*,
|
|
1553
|
+
x: np.ndarray,
|
|
1554
|
+
y: np.ndarray,
|
|
1555
|
+
epi_sum: np.ndarray,
|
|
1556
|
+
vf_sum: np.ndarray,
|
|
1557
|
+
count: np.ndarray,
|
|
1558
|
+
deg_sum: np.ndarray | None,
|
|
1559
|
+
np: ModuleType,
|
|
1560
|
+
) -> NeighborStats:
|
|
1561
|
+
"""Vectorised neighbour accumulation using a dense adjacency matrix."""
|
|
1562
|
+
|
|
473
1563
|
nodes = data["nodes"]
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1564
|
+
if not nodes:
|
|
1565
|
+
return x, y, epi_sum, vf_sum, count, deg_sum, None
|
|
1566
|
+
|
|
1567
|
+
A = data.get("A")
|
|
1568
|
+
if A is None:
|
|
1569
|
+
return _accumulate_neighbors_numpy(
|
|
1570
|
+
G,
|
|
1571
|
+
data,
|
|
1572
|
+
x=x,
|
|
1573
|
+
y=y,
|
|
1574
|
+
epi_sum=epi_sum,
|
|
1575
|
+
vf_sum=vf_sum,
|
|
1576
|
+
count=count,
|
|
1577
|
+
deg_sum=deg_sum,
|
|
1578
|
+
np=np,
|
|
1579
|
+
)
|
|
1580
|
+
|
|
1581
|
+
cache: DnfrCache | None = data.get("cache")
|
|
1582
|
+
n = len(nodes)
|
|
1583
|
+
|
|
1584
|
+
state = _ensure_numpy_state_vectors(data, np)
|
|
1585
|
+
vectors = [state["cos"], state["sin"], state["epi"], state["vf"]]
|
|
1586
|
+
|
|
1587
|
+
components = _ensure_cached_array(cache, "dense_components_np", (n, 4), np)
|
|
1588
|
+
accum = _ensure_cached_array(cache, "dense_accum_np", (n, 4), np)
|
|
1589
|
+
|
|
1590
|
+
# ``components`` retains the last source copies so callers relying on
|
|
1591
|
+
# cached buffers (e.g. diagnostics) still observe meaningful values.
|
|
1592
|
+
np.copyto(components, np.column_stack(vectors), casting="unsafe")
|
|
1593
|
+
|
|
1594
|
+
np.matmul(A, components, out=accum)
|
|
1595
|
+
|
|
1596
|
+
np.copyto(x, accum[:, 0], casting="unsafe")
|
|
1597
|
+
np.copyto(y, accum[:, 1], casting="unsafe")
|
|
1598
|
+
np.copyto(epi_sum, accum[:, 2], casting="unsafe")
|
|
1599
|
+
np.copyto(vf_sum, accum[:, 3], casting="unsafe")
|
|
1600
|
+
|
|
1601
|
+
degree_counts = data.get("dense_degree_np")
|
|
1602
|
+
if degree_counts is None or getattr(degree_counts, "shape", (0,))[0] != n:
|
|
1603
|
+
degree_counts = None
|
|
1604
|
+
if degree_counts is None and cache is not None:
|
|
1605
|
+
cached_counts = cache.dense_degree_np
|
|
1606
|
+
if cached_counts is not None and getattr(cached_counts, "shape", (0,))[0] == n:
|
|
1607
|
+
degree_counts = cached_counts
|
|
1608
|
+
if degree_counts is None:
|
|
1609
|
+
degree_counts = A.sum(axis=1)
|
|
1610
|
+
if cache is not None:
|
|
1611
|
+
cache.dense_degree_np = degree_counts
|
|
1612
|
+
data["dense_degree_np"] = degree_counts
|
|
1613
|
+
np.copyto(count, degree_counts, casting="unsafe")
|
|
1614
|
+
|
|
1615
|
+
degs = None
|
|
1616
|
+
if deg_sum is not None:
|
|
1617
|
+
deg_array = data.get("deg_array")
|
|
1618
|
+
if deg_array is None:
|
|
1619
|
+
deg_array = _resolve_numpy_degree_array(
|
|
1620
|
+
data,
|
|
1621
|
+
count,
|
|
1622
|
+
cache=cache,
|
|
1623
|
+
np=np,
|
|
479
1624
|
)
|
|
1625
|
+
if deg_array is None:
|
|
1626
|
+
deg_sum.fill(0.0)
|
|
1627
|
+
else:
|
|
1628
|
+
np.matmul(A, deg_array, out=deg_sum)
|
|
1629
|
+
degs = deg_array
|
|
1630
|
+
|
|
1631
|
+
return x, y, epi_sum, vf_sum, count, deg_sum, degs
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
def _accumulate_neighbors_broadcasted(
|
|
1635
|
+
*,
|
|
1636
|
+
edge_src: np.ndarray,
|
|
1637
|
+
edge_dst: np.ndarray,
|
|
1638
|
+
cos: np.ndarray,
|
|
1639
|
+
sin: np.ndarray,
|
|
1640
|
+
epi: np.ndarray,
|
|
1641
|
+
vf: np.ndarray,
|
|
1642
|
+
x: np.ndarray,
|
|
1643
|
+
y: np.ndarray,
|
|
1644
|
+
epi_sum: np.ndarray,
|
|
1645
|
+
vf_sum: np.ndarray,
|
|
1646
|
+
count: np.ndarray | None,
|
|
1647
|
+
deg_sum: np.ndarray | None,
|
|
1648
|
+
deg_array: np.ndarray | None,
|
|
1649
|
+
cache: DnfrCache | None,
|
|
1650
|
+
np: ModuleType,
|
|
1651
|
+
chunk_size: int | None = None,
|
|
1652
|
+
) -> dict[str, np.ndarray]:
|
|
1653
|
+
"""Accumulate neighbour contributions using direct indexed reductions."""
|
|
1654
|
+
|
|
1655
|
+
n = x.shape[0]
|
|
1656
|
+
edge_count = int(edge_src.size)
|
|
1657
|
+
|
|
1658
|
+
include_count = count is not None
|
|
1659
|
+
use_topology = deg_sum is not None and deg_array is not None
|
|
1660
|
+
|
|
1661
|
+
component_rows = 4 + (1 if include_count else 0) + (1 if use_topology else 0)
|
|
1662
|
+
|
|
1663
|
+
if edge_count:
|
|
1664
|
+
if chunk_size is None:
|
|
1665
|
+
resolved_chunk = edge_count
|
|
1666
|
+
else:
|
|
1667
|
+
try:
|
|
1668
|
+
resolved_chunk = int(chunk_size)
|
|
1669
|
+
except (TypeError, ValueError):
|
|
1670
|
+
resolved_chunk = edge_count
|
|
1671
|
+
else:
|
|
1672
|
+
if resolved_chunk <= 0:
|
|
1673
|
+
resolved_chunk = edge_count
|
|
1674
|
+
resolved_chunk = max(1, min(edge_count, resolved_chunk))
|
|
1675
|
+
else:
|
|
1676
|
+
resolved_chunk = 0
|
|
1677
|
+
|
|
1678
|
+
use_chunks = bool(edge_count and resolved_chunk < edge_count)
|
|
1679
|
+
|
|
1680
|
+
if cache is not None:
|
|
1681
|
+
base_signature = (id(edge_src), id(edge_dst), n, edge_count)
|
|
1682
|
+
cache.edge_signature = base_signature
|
|
1683
|
+
signature = (base_signature, component_rows)
|
|
1684
|
+
previous_signature = cache.neighbor_accum_signature
|
|
1685
|
+
|
|
1686
|
+
accum = cache.neighbor_accum_np
|
|
1687
|
+
if (
|
|
1688
|
+
accum is None
|
|
1689
|
+
or getattr(accum, "shape", None) != (component_rows, n)
|
|
1690
|
+
or previous_signature != signature
|
|
1691
|
+
):
|
|
1692
|
+
accum = np.zeros((component_rows, n), dtype=float)
|
|
1693
|
+
cache.neighbor_accum_np = accum
|
|
1694
|
+
else:
|
|
1695
|
+
accum.fill(0.0)
|
|
1696
|
+
|
|
1697
|
+
workspace = cache.neighbor_edge_values_np
|
|
1698
|
+
if use_chunks:
|
|
1699
|
+
workspace_length = resolved_chunk
|
|
1700
|
+
else:
|
|
1701
|
+
workspace_length = component_rows
|
|
1702
|
+
if workspace_length:
|
|
1703
|
+
expected_shape = (component_rows, workspace_length)
|
|
1704
|
+
if workspace is None or getattr(workspace, "shape", None) != expected_shape:
|
|
1705
|
+
workspace = np.empty(expected_shape, dtype=float)
|
|
1706
|
+
else:
|
|
1707
|
+
workspace = None
|
|
1708
|
+
cache.neighbor_edge_values_np = workspace
|
|
1709
|
+
|
|
1710
|
+
cache.neighbor_accum_signature = signature
|
|
1711
|
+
else:
|
|
1712
|
+
accum = np.zeros((component_rows, n), dtype=float)
|
|
1713
|
+
workspace_length = resolved_chunk if use_chunks else component_rows
|
|
1714
|
+
workspace = (
|
|
1715
|
+
np.empty((component_rows, workspace_length), dtype=float)
|
|
1716
|
+
if workspace_length
|
|
1717
|
+
else None
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
if edge_count:
|
|
1721
|
+
row = 0
|
|
1722
|
+
cos_row = row
|
|
1723
|
+
row += 1
|
|
1724
|
+
sin_row = row
|
|
1725
|
+
row += 1
|
|
1726
|
+
epi_row = row
|
|
1727
|
+
row += 1
|
|
1728
|
+
vf_row = row
|
|
1729
|
+
row += 1
|
|
1730
|
+
count_row = row if include_count and count is not None else None
|
|
1731
|
+
if count_row is not None:
|
|
1732
|
+
row += 1
|
|
1733
|
+
deg_row = row if use_topology and deg_array is not None else None
|
|
1734
|
+
|
|
1735
|
+
edge_src_int = edge_src.astype(np.intp, copy=False)
|
|
1736
|
+
edge_dst_int = edge_dst.astype(np.intp, copy=False)
|
|
1737
|
+
|
|
1738
|
+
if use_chunks:
|
|
1739
|
+
chunk_step = resolved_chunk if resolved_chunk else edge_count
|
|
1740
|
+
chunk_indices = range(0, edge_count, chunk_step)
|
|
1741
|
+
|
|
1742
|
+
for start in chunk_indices:
|
|
1743
|
+
end = min(start + chunk_step, edge_count)
|
|
1744
|
+
if start >= end:
|
|
1745
|
+
continue
|
|
1746
|
+
src_slice = edge_src_int[start:end]
|
|
1747
|
+
dst_slice = edge_dst_int[start:end]
|
|
1748
|
+
slice_len = end - start
|
|
1749
|
+
if slice_len <= 0:
|
|
1750
|
+
continue
|
|
1751
|
+
|
|
1752
|
+
if workspace is not None:
|
|
1753
|
+
chunk_matrix = workspace[:, :slice_len]
|
|
1754
|
+
else:
|
|
1755
|
+
chunk_matrix = np.empty((component_rows, slice_len), dtype=float)
|
|
1756
|
+
|
|
1757
|
+
np.take(cos, dst_slice, out=chunk_matrix[cos_row, :slice_len])
|
|
1758
|
+
np.take(sin, dst_slice, out=chunk_matrix[sin_row, :slice_len])
|
|
1759
|
+
np.take(epi, dst_slice, out=chunk_matrix[epi_row, :slice_len])
|
|
1760
|
+
np.take(vf, dst_slice, out=chunk_matrix[vf_row, :slice_len])
|
|
1761
|
+
|
|
1762
|
+
if count_row is not None:
|
|
1763
|
+
chunk_matrix[count_row, :slice_len].fill(1.0)
|
|
1764
|
+
if deg_row is not None and deg_array is not None:
|
|
1765
|
+
np.take(deg_array, dst_slice, out=chunk_matrix[deg_row, :slice_len])
|
|
1766
|
+
|
|
1767
|
+
def _accumulate_into(
|
|
1768
|
+
target_row: int | None,
|
|
1769
|
+
values: np.ndarray | None = None,
|
|
1770
|
+
*,
|
|
1771
|
+
unit_weight: bool = False,
|
|
1772
|
+
) -> None:
|
|
1773
|
+
if target_row is None:
|
|
1774
|
+
return
|
|
1775
|
+
row_view = accum[target_row]
|
|
1776
|
+
if unit_weight:
|
|
1777
|
+
np.add.at(row_view, src_slice, 1.0)
|
|
1778
|
+
else:
|
|
1779
|
+
if values is None:
|
|
1780
|
+
return
|
|
1781
|
+
np.add.at(row_view, src_slice, values)
|
|
1782
|
+
|
|
1783
|
+
_accumulate_into(cos_row, chunk_matrix[cos_row, :slice_len])
|
|
1784
|
+
_accumulate_into(sin_row, chunk_matrix[sin_row, :slice_len])
|
|
1785
|
+
_accumulate_into(epi_row, chunk_matrix[epi_row, :slice_len])
|
|
1786
|
+
_accumulate_into(vf_row, chunk_matrix[vf_row, :slice_len])
|
|
1787
|
+
|
|
1788
|
+
if count_row is not None:
|
|
1789
|
+
_accumulate_into(count_row, unit_weight=True)
|
|
1790
|
+
|
|
1791
|
+
if deg_row is not None and deg_array is not None:
|
|
1792
|
+
_accumulate_into(deg_row, chunk_matrix[deg_row, :slice_len])
|
|
1793
|
+
else:
|
|
1794
|
+
def _apply_full_bincount(
|
|
1795
|
+
target_row: int | None,
|
|
1796
|
+
values: np.ndarray | None = None,
|
|
1797
|
+
*,
|
|
1798
|
+
unit_weight: bool = False,
|
|
1799
|
+
) -> None:
|
|
1800
|
+
if target_row is None:
|
|
1801
|
+
return
|
|
1802
|
+
if values is None and not unit_weight:
|
|
1803
|
+
return
|
|
1804
|
+
if unit_weight:
|
|
1805
|
+
component_accum = np.bincount(
|
|
1806
|
+
edge_src_int,
|
|
1807
|
+
minlength=n,
|
|
1808
|
+
)
|
|
1809
|
+
else:
|
|
1810
|
+
component_accum = np.bincount(
|
|
1811
|
+
edge_src_int,
|
|
1812
|
+
weights=values,
|
|
1813
|
+
minlength=n,
|
|
1814
|
+
)
|
|
1815
|
+
np.copyto(
|
|
1816
|
+
accum[target_row, : n],
|
|
1817
|
+
component_accum[:n],
|
|
1818
|
+
casting="unsafe",
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
_apply_full_bincount(cos_row, np.take(cos, edge_dst_int))
|
|
1822
|
+
_apply_full_bincount(sin_row, np.take(sin, edge_dst_int))
|
|
1823
|
+
_apply_full_bincount(epi_row, np.take(epi, edge_dst_int))
|
|
1824
|
+
_apply_full_bincount(vf_row, np.take(vf, edge_dst_int))
|
|
1825
|
+
|
|
1826
|
+
if count_row is not None:
|
|
1827
|
+
_apply_full_bincount(count_row, unit_weight=True)
|
|
1828
|
+
|
|
1829
|
+
if deg_row is not None and deg_array is not None:
|
|
1830
|
+
_apply_full_bincount(deg_row, np.take(deg_array, edge_dst_int))
|
|
1831
|
+
else:
|
|
1832
|
+
accum.fill(0.0)
|
|
1833
|
+
if workspace is not None:
|
|
1834
|
+
workspace.fill(0.0)
|
|
1835
|
+
|
|
1836
|
+
row = 0
|
|
1837
|
+
np.copyto(x, accum[row], casting="unsafe")
|
|
1838
|
+
row += 1
|
|
1839
|
+
np.copyto(y, accum[row], casting="unsafe")
|
|
1840
|
+
row += 1
|
|
1841
|
+
np.copyto(epi_sum, accum[row], casting="unsafe")
|
|
1842
|
+
row += 1
|
|
1843
|
+
np.copyto(vf_sum, accum[row], casting="unsafe")
|
|
1844
|
+
row += 1
|
|
1845
|
+
|
|
1846
|
+
if include_count and count is not None:
|
|
1847
|
+
np.copyto(count, accum[row], casting="unsafe")
|
|
1848
|
+
row += 1
|
|
1849
|
+
|
|
1850
|
+
if use_topology and deg_sum is not None:
|
|
1851
|
+
np.copyto(deg_sum, accum[row], casting="unsafe")
|
|
1852
|
+
|
|
1853
|
+
return {
|
|
1854
|
+
"accumulator": accum,
|
|
1855
|
+
"edge_values": workspace,
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
|
|
1859
|
+
def _build_neighbor_sums_common(
|
|
1860
|
+
G: TNFRGraph,
|
|
1861
|
+
data: MutableMapping[str, Any],
|
|
1862
|
+
*,
|
|
1863
|
+
use_numpy: bool,
|
|
1864
|
+
n_jobs: int | None = None,
|
|
1865
|
+
) -> NeighborStats:
|
|
1866
|
+
"""Build neighbour accumulators honouring cached NumPy buffers when possible."""
|
|
1867
|
+
|
|
1868
|
+
nodes = data["nodes"]
|
|
1869
|
+
cache: DnfrCache | None = data.get("cache")
|
|
1870
|
+
np_module = get_numpy()
|
|
1871
|
+
has_numpy_buffers = _has_cached_numpy_buffers(data, cache)
|
|
1872
|
+
if use_numpy and np_module is None and has_numpy_buffers:
|
|
1873
|
+
np_module = sys.modules.get("numpy")
|
|
1874
|
+
|
|
1875
|
+
if np_module is not None:
|
|
480
1876
|
if not nodes:
|
|
481
|
-
return
|
|
1877
|
+
return _init_neighbor_sums(data, np=np_module)
|
|
1878
|
+
|
|
482
1879
|
x, y, epi_sum, vf_sum, count, deg_sum, degs = _init_neighbor_sums(
|
|
483
|
-
data, np=
|
|
1880
|
+
data, np=np_module
|
|
484
1881
|
)
|
|
1882
|
+
prefer_sparse = data.get("prefer_sparse")
|
|
1883
|
+
if prefer_sparse is None:
|
|
1884
|
+
prefer_sparse = _prefer_sparse_accumulation(
|
|
1885
|
+
len(nodes), data.get("edge_count")
|
|
1886
|
+
)
|
|
1887
|
+
data["prefer_sparse"] = prefer_sparse
|
|
1888
|
+
|
|
1889
|
+
use_dense = False
|
|
485
1890
|
A = data.get("A")
|
|
486
|
-
if A is None:
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
data
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
y[:] = A @ sin_th
|
|
510
|
-
epi_sum[:] = A @ epi
|
|
511
|
-
vf_sum[:] = A @ vf
|
|
512
|
-
count[:] = A.sum(axis=1)
|
|
513
|
-
if w_topo != 0.0:
|
|
514
|
-
deg_array = data.get("deg_array")
|
|
515
|
-
if deg_array is None:
|
|
516
|
-
deg_list = data.get("deg_list")
|
|
517
|
-
if deg_list is not None:
|
|
518
|
-
deg_array = np.array(deg_list, dtype=float)
|
|
519
|
-
data["deg_array"] = deg_array
|
|
520
|
-
if cache is not None:
|
|
521
|
-
cache.deg_array = deg_array
|
|
522
|
-
else:
|
|
523
|
-
deg_array = count
|
|
524
|
-
deg_sum[:] = A @ deg_array
|
|
525
|
-
degs = deg_array
|
|
526
|
-
return x, y, epi_sum, vf_sum, count, deg_sum, degs
|
|
527
|
-
else:
|
|
528
|
-
x, y, epi_sum, vf_sum, count, deg_sum, degs_list = _init_neighbor_sums(
|
|
529
|
-
data
|
|
1891
|
+
if use_numpy and not prefer_sparse and A is not None:
|
|
1892
|
+
shape = getattr(A, "shape", (0, 0))
|
|
1893
|
+
use_dense = shape[0] == len(nodes) and shape[1] == len(nodes)
|
|
1894
|
+
if use_numpy and data.get("dense_override") and A is not None:
|
|
1895
|
+
shape = getattr(A, "shape", (0, 0))
|
|
1896
|
+
if shape[0] == len(nodes) and shape[1] == len(nodes):
|
|
1897
|
+
use_dense = True
|
|
1898
|
+
|
|
1899
|
+
if use_dense:
|
|
1900
|
+
accumulator = _accumulate_neighbors_dense
|
|
1901
|
+
else:
|
|
1902
|
+
_ensure_numpy_state_vectors(data, np_module)
|
|
1903
|
+
accumulator = _accumulate_neighbors_numpy
|
|
1904
|
+
return accumulator(
|
|
1905
|
+
G,
|
|
1906
|
+
data,
|
|
1907
|
+
x=x,
|
|
1908
|
+
y=y,
|
|
1909
|
+
epi_sum=epi_sum,
|
|
1910
|
+
vf_sum=vf_sum,
|
|
1911
|
+
count=count,
|
|
1912
|
+
deg_sum=deg_sum,
|
|
1913
|
+
np=np_module,
|
|
530
1914
|
)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1915
|
+
|
|
1916
|
+
if not nodes:
|
|
1917
|
+
return _init_neighbor_sums(data)
|
|
1918
|
+
|
|
1919
|
+
x, y, epi_sum, vf_sum, count, deg_sum, degs_list = _init_neighbor_sums(data)
|
|
1920
|
+
idx = data["idx"]
|
|
1921
|
+
epi = data["epi"]
|
|
1922
|
+
vf = data["vf"]
|
|
1923
|
+
cos_th = data["cos_theta"]
|
|
1924
|
+
sin_th = data["sin_theta"]
|
|
1925
|
+
deg_list = data.get("deg_list")
|
|
1926
|
+
|
|
1927
|
+
effective_jobs = _resolve_parallel_jobs(n_jobs, len(nodes))
|
|
1928
|
+
if effective_jobs:
|
|
1929
|
+
neighbor_indices: list[list[int]] = []
|
|
1930
|
+
for node in nodes:
|
|
1931
|
+
indices: list[int] = []
|
|
539
1932
|
for v in G.neighbors(node):
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1933
|
+
indices.append(idx[v])
|
|
1934
|
+
neighbor_indices.append(indices)
|
|
1935
|
+
|
|
1936
|
+
chunk_results = []
|
|
1937
|
+
with ProcessPoolExecutor(max_workers=effective_jobs) as executor:
|
|
1938
|
+
futures = []
|
|
1939
|
+
for start, end in _iter_chunk_offsets(len(nodes), effective_jobs):
|
|
1940
|
+
if start == end:
|
|
1941
|
+
continue
|
|
1942
|
+
futures.append(
|
|
1943
|
+
executor.submit(
|
|
1944
|
+
_neighbor_sums_worker,
|
|
1945
|
+
start,
|
|
1946
|
+
end,
|
|
1947
|
+
neighbor_indices,
|
|
1948
|
+
cos_th,
|
|
1949
|
+
sin_th,
|
|
1950
|
+
epi,
|
|
1951
|
+
vf,
|
|
1952
|
+
x[start:end],
|
|
1953
|
+
y[start:end],
|
|
1954
|
+
epi_sum[start:end],
|
|
1955
|
+
vf_sum[start:end],
|
|
1956
|
+
count[start:end],
|
|
1957
|
+
deg_sum[start:end] if deg_sum is not None else None,
|
|
1958
|
+
deg_list,
|
|
1959
|
+
degs_list,
|
|
1960
|
+
)
|
|
1961
|
+
)
|
|
1962
|
+
for future in futures:
|
|
1963
|
+
chunk_results.append(future.result())
|
|
1964
|
+
|
|
1965
|
+
for (
|
|
1966
|
+
start,
|
|
1967
|
+
chunk_x,
|
|
1968
|
+
chunk_y,
|
|
1969
|
+
chunk_epi,
|
|
1970
|
+
chunk_vf,
|
|
1971
|
+
chunk_count,
|
|
1972
|
+
chunk_deg,
|
|
1973
|
+
) in sorted(chunk_results, key=lambda item: item[0]):
|
|
1974
|
+
end = start + len(chunk_x)
|
|
1975
|
+
x[start:end] = chunk_x
|
|
1976
|
+
y[start:end] = chunk_y
|
|
1977
|
+
epi_sum[start:end] = chunk_epi
|
|
1978
|
+
vf_sum[start:end] = chunk_vf
|
|
1979
|
+
count[start:end] = chunk_count
|
|
1980
|
+
if deg_sum is not None and chunk_deg is not None:
|
|
1981
|
+
deg_sum[start:end] = chunk_deg
|
|
548
1982
|
return x, y, epi_sum, vf_sum, count, deg_sum, degs_list
|
|
549
1983
|
|
|
1984
|
+
for i, node in enumerate(nodes):
|
|
1985
|
+
deg_i = degs_list[i] if degs_list is not None else 0.0
|
|
1986
|
+
x_i = x[i]
|
|
1987
|
+
y_i = y[i]
|
|
1988
|
+
epi_i = epi_sum[i]
|
|
1989
|
+
vf_i = vf_sum[i]
|
|
1990
|
+
count_i = count[i]
|
|
1991
|
+
deg_acc = deg_sum[i] if deg_sum is not None else 0.0
|
|
1992
|
+
for v in G.neighbors(node):
|
|
1993
|
+
j = idx[v]
|
|
1994
|
+
cos_j = cos_th[j]
|
|
1995
|
+
sin_j = sin_th[j]
|
|
1996
|
+
epi_j = epi[j]
|
|
1997
|
+
vf_j = vf[j]
|
|
1998
|
+
x_i += cos_j
|
|
1999
|
+
y_i += sin_j
|
|
2000
|
+
epi_i += epi_j
|
|
2001
|
+
vf_i += vf_j
|
|
2002
|
+
count_i += 1
|
|
2003
|
+
if deg_sum is not None:
|
|
2004
|
+
deg_acc += deg_list[j] if deg_list is not None else deg_i
|
|
2005
|
+
x[i] = x_i
|
|
2006
|
+
y[i] = y_i
|
|
2007
|
+
epi_sum[i] = epi_i
|
|
2008
|
+
vf_sum[i] = vf_i
|
|
2009
|
+
count[i] = count_i
|
|
2010
|
+
if deg_sum is not None:
|
|
2011
|
+
deg_sum[i] = deg_acc
|
|
2012
|
+
return x, y, epi_sum, vf_sum, count, deg_sum, degs_list
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
def _accumulate_neighbors_numpy(
|
|
2016
|
+
G: TNFRGraph,
|
|
2017
|
+
data: MutableMapping[str, Any],
|
|
2018
|
+
*,
|
|
2019
|
+
x: np.ndarray,
|
|
2020
|
+
y: np.ndarray,
|
|
2021
|
+
epi_sum: np.ndarray,
|
|
2022
|
+
vf_sum: np.ndarray,
|
|
2023
|
+
count: np.ndarray | None,
|
|
2024
|
+
deg_sum: np.ndarray | None,
|
|
2025
|
+
np: ModuleType,
|
|
2026
|
+
) -> NeighborStats:
|
|
2027
|
+
"""Vectorised neighbour accumulation reusing cached NumPy buffers."""
|
|
550
2028
|
|
|
551
|
-
|
|
2029
|
+
nodes = data["nodes"]
|
|
2030
|
+
if not nodes:
|
|
2031
|
+
return x, y, epi_sum, vf_sum, count, deg_sum, None
|
|
2032
|
+
|
|
2033
|
+
cache: DnfrCache | None = data.get("cache")
|
|
2034
|
+
|
|
2035
|
+
state = _ensure_numpy_state_vectors(data, np)
|
|
2036
|
+
cos_th = state["cos"]
|
|
2037
|
+
sin_th = state["sin"]
|
|
2038
|
+
epi = state["epi"]
|
|
2039
|
+
vf = state["vf"]
|
|
2040
|
+
|
|
2041
|
+
edge_src = data.get("edge_src")
|
|
2042
|
+
edge_dst = data.get("edge_dst")
|
|
2043
|
+
if edge_src is None or edge_dst is None:
|
|
2044
|
+
edge_src, edge_dst = _build_edge_index_arrays(G, nodes, data["idx"], np)
|
|
2045
|
+
data["edge_src"] = edge_src
|
|
2046
|
+
data["edge_dst"] = edge_dst
|
|
2047
|
+
if cache is not None:
|
|
2048
|
+
cache.edge_src = edge_src
|
|
2049
|
+
cache.edge_dst = edge_dst
|
|
2050
|
+
if edge_src is not None:
|
|
2051
|
+
data["edge_count"] = int(edge_src.size)
|
|
2052
|
+
|
|
2053
|
+
cached_deg_array = data.get("deg_array")
|
|
2054
|
+
reuse_count_from_deg = bool(count is not None and cached_deg_array is not None)
|
|
2055
|
+
count_for_accum = count
|
|
2056
|
+
if count is not None:
|
|
2057
|
+
if reuse_count_from_deg:
|
|
2058
|
+
# Reuse the cached degree vector as neighbour counts to avoid
|
|
2059
|
+
# allocating an extra accumulator row in the broadcast routine.
|
|
2060
|
+
np.copyto(count, cached_deg_array, casting="unsafe")
|
|
2061
|
+
count_for_accum = None
|
|
2062
|
+
else:
|
|
2063
|
+
count.fill(0.0)
|
|
2064
|
+
|
|
2065
|
+
deg_array = None
|
|
2066
|
+
if deg_sum is not None:
|
|
2067
|
+
deg_sum.fill(0.0)
|
|
2068
|
+
deg_array = _resolve_numpy_degree_array(
|
|
2069
|
+
data, count if count is not None else None, cache=cache, np=np
|
|
2070
|
+
)
|
|
2071
|
+
elif cached_deg_array is not None:
|
|
2072
|
+
deg_array = cached_deg_array
|
|
2073
|
+
|
|
2074
|
+
edge_count = int(edge_src.size) if edge_src is not None else 0
|
|
2075
|
+
chunk_hint = data.get("neighbor_chunk_hint")
|
|
2076
|
+
if chunk_hint is None:
|
|
2077
|
+
chunk_hint = G.graph.get("DNFR_CHUNK_SIZE")
|
|
2078
|
+
resolved_neighbor_chunk = (
|
|
2079
|
+
resolve_chunk_size(
|
|
2080
|
+
chunk_hint,
|
|
2081
|
+
edge_count,
|
|
2082
|
+
minimum=1,
|
|
2083
|
+
approx_bytes_per_item=_DNFR_APPROX_BYTES_PER_EDGE,
|
|
2084
|
+
clamp_to=None,
|
|
2085
|
+
)
|
|
2086
|
+
if edge_count
|
|
2087
|
+
else 0
|
|
2088
|
+
)
|
|
2089
|
+
data["neighbor_chunk_hint"] = chunk_hint
|
|
2090
|
+
data["neighbor_chunk_size"] = resolved_neighbor_chunk
|
|
2091
|
+
|
|
2092
|
+
accum = _accumulate_neighbors_broadcasted(
|
|
2093
|
+
edge_src=edge_src,
|
|
2094
|
+
edge_dst=edge_dst,
|
|
2095
|
+
cos=cos_th,
|
|
2096
|
+
sin=sin_th,
|
|
2097
|
+
epi=epi,
|
|
2098
|
+
vf=vf,
|
|
2099
|
+
x=x,
|
|
2100
|
+
y=y,
|
|
2101
|
+
epi_sum=epi_sum,
|
|
2102
|
+
vf_sum=vf_sum,
|
|
2103
|
+
count=count_for_accum,
|
|
2104
|
+
deg_sum=deg_sum,
|
|
2105
|
+
deg_array=deg_array,
|
|
2106
|
+
cache=cache,
|
|
2107
|
+
np=np,
|
|
2108
|
+
chunk_size=resolved_neighbor_chunk,
|
|
2109
|
+
)
|
|
2110
|
+
|
|
2111
|
+
data["neighbor_accum_np"] = accum.get("accumulator")
|
|
2112
|
+
edge_values = accum.get("edge_values")
|
|
2113
|
+
data["neighbor_edge_values_np"] = edge_values
|
|
2114
|
+
if edge_values is not None:
|
|
2115
|
+
width = getattr(edge_values, "shape", (0, 0))[1]
|
|
2116
|
+
data["neighbor_chunk_size"] = int(width)
|
|
2117
|
+
else:
|
|
2118
|
+
data["neighbor_chunk_size"] = resolved_neighbor_chunk
|
|
2119
|
+
if cache is not None:
|
|
2120
|
+
data["neighbor_accum_signature"] = cache.neighbor_accum_signature
|
|
2121
|
+
if reuse_count_from_deg and cached_deg_array is not None:
|
|
2122
|
+
count = cached_deg_array
|
|
2123
|
+
degs = deg_array if deg_sum is not None and deg_array is not None else None
|
|
2124
|
+
return x, y, epi_sum, vf_sum, count, deg_sum, degs
|
|
2125
|
+
|
|
2126
|
+
|
|
2127
|
+
def _compute_dnfr(
|
|
2128
|
+
G: TNFRGraph,
|
|
2129
|
+
data: MutableMapping[str, Any],
|
|
2130
|
+
*,
|
|
2131
|
+
use_numpy: bool | None = None,
|
|
2132
|
+
n_jobs: int | None = None,
|
|
2133
|
+
profile: MutableMapping[str, float] | None = None,
|
|
2134
|
+
) -> None:
|
|
552
2135
|
"""Compute ΔNFR using neighbour sums.
|
|
553
2136
|
|
|
554
2137
|
Parameters
|
|
@@ -557,11 +2140,56 @@ def _compute_dnfr(G, data, *, use_numpy: bool = False) -> None:
|
|
|
557
2140
|
Graph on which the computation is performed.
|
|
558
2141
|
data : dict
|
|
559
2142
|
Precomputed ΔNFR data as returned by :func:`_prepare_dnfr_data`.
|
|
560
|
-
use_numpy : bool, optional
|
|
561
|
-
When ``True`` the
|
|
562
|
-
|
|
2143
|
+
use_numpy : bool | None, optional
|
|
2144
|
+
Backwards compatibility flag. When ``True`` the function eagerly
|
|
2145
|
+
prepares NumPy buffers (if available). When ``False`` the engine still
|
|
2146
|
+
prefers the vectorised path whenever :func:`get_numpy` returns a module
|
|
2147
|
+
and the graph does not set ``vectorized_dnfr`` to ``False``.
|
|
2148
|
+
profile : MutableMapping[str, float] or None, optional
|
|
2149
|
+
Mutable mapping that aggregates wall-clock durations for neighbour
|
|
2150
|
+
accumulation and records which execution branch was used. The
|
|
2151
|
+
``"dnfr_neighbor_accumulation"`` bucket gathers the time spent inside
|
|
2152
|
+
:func:`_build_neighbor_sums_common`, while ``"dnfr_path"`` stores the
|
|
2153
|
+
string ``"vectorized"`` or ``"fallback"`` describing the active
|
|
2154
|
+
implementation.
|
|
563
2155
|
"""
|
|
564
|
-
|
|
2156
|
+
start_timer, stop_timer = _profile_start_stop(
|
|
2157
|
+
profile,
|
|
2158
|
+
keys=("dnfr_neighbor_accumulation",),
|
|
2159
|
+
)
|
|
2160
|
+
|
|
2161
|
+
np_module = get_numpy()
|
|
2162
|
+
data["dnfr_numpy_available"] = bool(np_module)
|
|
2163
|
+
vector_disabled = G.graph.get("vectorized_dnfr") is False
|
|
2164
|
+
prefer_dense = np_module is not None and not vector_disabled
|
|
2165
|
+
if use_numpy is True and np_module is not None:
|
|
2166
|
+
prefer_dense = True
|
|
2167
|
+
if use_numpy is False or vector_disabled:
|
|
2168
|
+
prefer_dense = False
|
|
2169
|
+
data["dnfr_used_numpy"] = bool(prefer_dense and np_module is not None)
|
|
2170
|
+
if profile is not None:
|
|
2171
|
+
profile["dnfr_path"] = "vectorized" if data["dnfr_used_numpy"] else "fallback"
|
|
2172
|
+
|
|
2173
|
+
data["n_jobs"] = n_jobs
|
|
2174
|
+
try:
|
|
2175
|
+
neighbor_timer = start_timer()
|
|
2176
|
+
res = _build_neighbor_sums_common(
|
|
2177
|
+
G,
|
|
2178
|
+
data,
|
|
2179
|
+
use_numpy=prefer_dense,
|
|
2180
|
+
n_jobs=n_jobs,
|
|
2181
|
+
)
|
|
2182
|
+
stop_timer("dnfr_neighbor_accumulation", neighbor_timer)
|
|
2183
|
+
except TypeError as exc:
|
|
2184
|
+
if "n_jobs" not in str(exc):
|
|
2185
|
+
raise
|
|
2186
|
+
neighbor_timer = start_timer()
|
|
2187
|
+
res = _build_neighbor_sums_common(
|
|
2188
|
+
G,
|
|
2189
|
+
data,
|
|
2190
|
+
use_numpy=prefer_dense,
|
|
2191
|
+
)
|
|
2192
|
+
stop_timer("dnfr_neighbor_accumulation", neighbor_timer)
|
|
565
2193
|
if res is None:
|
|
566
2194
|
return
|
|
567
2195
|
x, y, epi_sum, vf_sum, count, deg_sum, degs = res
|
|
@@ -575,10 +2203,18 @@ def _compute_dnfr(G, data, *, use_numpy: bool = False) -> None:
|
|
|
575
2203
|
count=count,
|
|
576
2204
|
deg_sum=deg_sum,
|
|
577
2205
|
degs=degs,
|
|
2206
|
+
n_jobs=n_jobs,
|
|
2207
|
+
profile=profile,
|
|
578
2208
|
)
|
|
579
2209
|
|
|
580
2210
|
|
|
581
|
-
def default_compute_delta_nfr(
|
|
2211
|
+
def default_compute_delta_nfr(
|
|
2212
|
+
G: TNFRGraph,
|
|
2213
|
+
*,
|
|
2214
|
+
cache_size: int | None = 1,
|
|
2215
|
+
n_jobs: int | None = None,
|
|
2216
|
+
profile: MutableMapping[str, float] | None = None,
|
|
2217
|
+
) -> None:
|
|
582
2218
|
"""Compute ΔNFR by mixing phase, EPI, νf and a topological term.
|
|
583
2219
|
|
|
584
2220
|
Parameters
|
|
@@ -589,29 +2225,97 @@ def default_compute_delta_nfr(G, *, cache_size: int | None = 1) -> None:
|
|
|
589
2225
|
Maximum number of edge configurations cached in ``G.graph``. Values
|
|
590
2226
|
``None`` or <= 0 imply unlimited cache. Defaults to ``1`` to keep the
|
|
591
2227
|
previous behaviour.
|
|
2228
|
+
n_jobs : int | None, optional
|
|
2229
|
+
Parallel worker count for the pure-Python accumulation path. ``None``
|
|
2230
|
+
or values <= 1 preserve the serial behaviour. The vectorised NumPy
|
|
2231
|
+
branch ignores this parameter as it already operates in bulk.
|
|
2232
|
+
profile : MutableMapping[str, float] or None, optional
|
|
2233
|
+
Mutable mapping that aggregates the wall-clock timings captured during
|
|
2234
|
+
the ΔNFR computation. The mapping receives the buckets documented in
|
|
2235
|
+
:func:`_prepare_dnfr_data` and :func:`_compute_dnfr`, plus
|
|
2236
|
+
``"dnfr_neighbor_means"``, ``"dnfr_gradient_assembly"`` and
|
|
2237
|
+
``"dnfr_inplace_write"`` describing the internal stages of
|
|
2238
|
+
:func:`_compute_dnfr_common`. ``"dnfr_path"`` reflects whether the
|
|
2239
|
+
vectorised or fallback implementation executed the call.
|
|
592
2240
|
"""
|
|
593
|
-
|
|
2241
|
+
if profile is not None:
|
|
2242
|
+
for key in (
|
|
2243
|
+
"dnfr_cache_rebuild",
|
|
2244
|
+
"dnfr_neighbor_accumulation",
|
|
2245
|
+
"dnfr_neighbor_means",
|
|
2246
|
+
"dnfr_gradient_assembly",
|
|
2247
|
+
"dnfr_inplace_write",
|
|
2248
|
+
):
|
|
2249
|
+
profile.setdefault(key, 0.0)
|
|
2250
|
+
|
|
2251
|
+
data = _prepare_dnfr_data(G, cache_size=cache_size, profile=profile)
|
|
594
2252
|
_write_dnfr_metadata(
|
|
595
2253
|
G,
|
|
596
2254
|
weights=data["weights"],
|
|
597
2255
|
hook_name="default_compute_delta_nfr",
|
|
598
2256
|
)
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
2257
|
+
_compute_dnfr(G, data, n_jobs=n_jobs, profile=profile)
|
|
2258
|
+
if not data.get("dnfr_numpy_available"):
|
|
2259
|
+
cache = data.get("cache")
|
|
2260
|
+
cache_size = data.get("cache_size")
|
|
2261
|
+
caching_enabled = (
|
|
2262
|
+
isinstance(cache, DnfrCache)
|
|
2263
|
+
and (cache_size is None or int(cache_size) > 0)
|
|
2264
|
+
)
|
|
2265
|
+
if isinstance(cache, DnfrCache) and not caching_enabled:
|
|
2266
|
+
for attr in (
|
|
2267
|
+
"neighbor_x_np",
|
|
2268
|
+
"neighbor_y_np",
|
|
2269
|
+
"neighbor_epi_sum_np",
|
|
2270
|
+
"neighbor_vf_sum_np",
|
|
2271
|
+
"neighbor_count_np",
|
|
2272
|
+
"neighbor_deg_sum_np",
|
|
2273
|
+
"neighbor_inv_count_np",
|
|
2274
|
+
"neighbor_cos_avg_np",
|
|
2275
|
+
"neighbor_sin_avg_np",
|
|
2276
|
+
"neighbor_mean_tmp_np",
|
|
2277
|
+
"neighbor_mean_length_np",
|
|
2278
|
+
"neighbor_accum_np",
|
|
2279
|
+
"neighbor_edge_values_np",
|
|
2280
|
+
):
|
|
2281
|
+
setattr(cache, attr, None)
|
|
2282
|
+
cache.neighbor_accum_signature = None
|
|
602
2283
|
|
|
603
2284
|
|
|
604
2285
|
def set_delta_nfr_hook(
|
|
605
|
-
G
|
|
2286
|
+
G: TNFRGraph,
|
|
2287
|
+
func: DeltaNFRHook,
|
|
2288
|
+
*,
|
|
2289
|
+
name: str | None = None,
|
|
2290
|
+
note: str | None = None,
|
|
606
2291
|
) -> None:
|
|
607
2292
|
"""Set a stable hook to compute ΔNFR.
|
|
608
|
-
|
|
609
|
-
|
|
2293
|
+
|
|
2294
|
+
The callable should accept ``(G, *[, n_jobs])`` and is responsible for
|
|
2295
|
+
writing ``ALIAS_DNFR`` in each node. ``n_jobs`` is optional and ignored by
|
|
2296
|
+
hooks that do not support parallel execution. Basic metadata in
|
|
2297
|
+
``G.graph`` is updated accordingly.
|
|
610
2298
|
"""
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
2299
|
+
|
|
2300
|
+
def _wrapped(graph: TNFRGraph, *args: Any, **kwargs: Any) -> None:
|
|
2301
|
+
if "n_jobs" in kwargs:
|
|
2302
|
+
try:
|
|
2303
|
+
func(graph, *args, **kwargs)
|
|
2304
|
+
return
|
|
2305
|
+
except TypeError as exc:
|
|
2306
|
+
if "n_jobs" not in str(exc):
|
|
2307
|
+
raise
|
|
2308
|
+
kwargs = dict(kwargs)
|
|
2309
|
+
kwargs.pop("n_jobs", None)
|
|
2310
|
+
func(graph, *args, **kwargs)
|
|
2311
|
+
return
|
|
2312
|
+
func(graph, *args, **kwargs)
|
|
2313
|
+
|
|
2314
|
+
_wrapped.__name__ = getattr(func, "__name__", "custom_dnfr")
|
|
2315
|
+
_wrapped.__doc__ = getattr(func, "__doc__", _wrapped.__doc__)
|
|
2316
|
+
|
|
2317
|
+
G.graph["compute_delta_nfr"] = _wrapped
|
|
2318
|
+
G.graph["_dnfr_hook_name"] = str(name or getattr(func, "__name__", "custom_dnfr"))
|
|
615
2319
|
if "_dnfr_weights" not in G.graph:
|
|
616
2320
|
_configure_dnfr_weights(G)
|
|
617
2321
|
if note:
|
|
@@ -620,114 +2324,293 @@ def set_delta_nfr_hook(
|
|
|
620
2324
|
G.graph["_DNFR_META"] = meta
|
|
621
2325
|
|
|
622
2326
|
|
|
2327
|
+
def _dnfr_hook_chunk_worker(
|
|
2328
|
+
G: TNFRGraph,
|
|
2329
|
+
node_ids: Sequence[NodeId],
|
|
2330
|
+
grad_items: tuple[
|
|
2331
|
+
tuple[str, Callable[[TNFRGraph, NodeId, Mapping[str, Any]], float]],
|
|
2332
|
+
...,
|
|
2333
|
+
],
|
|
2334
|
+
weights: Mapping[str, float],
|
|
2335
|
+
) -> list[tuple[NodeId, float]]:
|
|
2336
|
+
"""Compute weighted gradients for ``node_ids``.
|
|
2337
|
+
|
|
2338
|
+
The helper is defined at module level so it can be pickled by
|
|
2339
|
+
:class:`concurrent.futures.ProcessPoolExecutor`.
|
|
2340
|
+
"""
|
|
2341
|
+
|
|
2342
|
+
results: list[tuple[NodeId, float]] = []
|
|
2343
|
+
for node in node_ids:
|
|
2344
|
+
nd = G.nodes[node]
|
|
2345
|
+
total = 0.0
|
|
2346
|
+
for name, func in grad_items:
|
|
2347
|
+
w = weights.get(name, 0.0)
|
|
2348
|
+
if w:
|
|
2349
|
+
total += w * float(func(G, node, nd))
|
|
2350
|
+
results.append((node, total))
|
|
2351
|
+
return results
|
|
2352
|
+
|
|
2353
|
+
|
|
623
2354
|
def _apply_dnfr_hook(
|
|
624
|
-
G,
|
|
625
|
-
grads:
|
|
2355
|
+
G: TNFRGraph,
|
|
2356
|
+
grads: Mapping[str, Callable[[TNFRGraph, NodeId, Mapping[str, Any]], float]],
|
|
626
2357
|
*,
|
|
627
|
-
weights:
|
|
2358
|
+
weights: Mapping[str, float],
|
|
628
2359
|
hook_name: str,
|
|
629
2360
|
note: str | None = None,
|
|
2361
|
+
n_jobs: int | None = None,
|
|
630
2362
|
) -> None:
|
|
631
|
-
"""
|
|
2363
|
+
"""Compute and store ΔNFR using ``grads``.
|
|
632
2364
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
2365
|
+
Parameters
|
|
2366
|
+
----------
|
|
2367
|
+
G : nx.Graph
|
|
2368
|
+
Graph whose nodes will receive the ΔNFR update.
|
|
2369
|
+
grads : dict
|
|
2370
|
+
Mapping from component names to callables with signature
|
|
2371
|
+
``(G, node, data) -> float`` returning the gradient contribution.
|
|
2372
|
+
weights : dict
|
|
2373
|
+
Weight per component; missing entries default to ``0``.
|
|
2374
|
+
hook_name : str
|
|
2375
|
+
Friendly identifier stored in ``G.graph`` metadata.
|
|
2376
|
+
note : str | None, optional
|
|
2377
|
+
Additional documentation recorded next to the hook metadata.
|
|
2378
|
+
n_jobs : int | None, optional
|
|
2379
|
+
Optional worker count for the pure-Python execution path. When NumPy
|
|
2380
|
+
is available the helper always prefers the vectorised implementation
|
|
2381
|
+
and ignores ``n_jobs`` because the computation already happens in
|
|
2382
|
+
bulk.
|
|
636
2383
|
"""
|
|
637
2384
|
|
|
638
|
-
|
|
639
|
-
|
|
2385
|
+
nodes_data: list[tuple[NodeId, Mapping[str, Any]]] = list(G.nodes(data=True))
|
|
2386
|
+
if not nodes_data:
|
|
2387
|
+
_write_dnfr_metadata(G, weights=weights, hook_name=hook_name, note=note)
|
|
2388
|
+
return
|
|
2389
|
+
|
|
2390
|
+
np_module = cast(ModuleType | None, get_numpy())
|
|
2391
|
+
if np_module is not None:
|
|
2392
|
+
totals = np_module.zeros(len(nodes_data), dtype=float)
|
|
640
2393
|
for name, func in grads.items():
|
|
641
|
-
w = weights.get(name, 0.0)
|
|
642
|
-
if w:
|
|
643
|
-
|
|
644
|
-
|
|
2394
|
+
w = float(weights.get(name, 0.0))
|
|
2395
|
+
if w == 0.0:
|
|
2396
|
+
continue
|
|
2397
|
+
values = np_module.fromiter(
|
|
2398
|
+
(float(func(G, n, nd)) for n, nd in nodes_data),
|
|
2399
|
+
dtype=float,
|
|
2400
|
+
count=len(nodes_data),
|
|
2401
|
+
)
|
|
2402
|
+
if w == 1.0:
|
|
2403
|
+
np_module.add(totals, values, out=totals)
|
|
2404
|
+
else:
|
|
2405
|
+
np_module.add(totals, values * w, out=totals)
|
|
2406
|
+
for idx, (n, _) in enumerate(nodes_data):
|
|
2407
|
+
set_dnfr(G, n, float(totals[idx]))
|
|
2408
|
+
_write_dnfr_metadata(G, weights=weights, hook_name=hook_name, note=note)
|
|
2409
|
+
return
|
|
2410
|
+
|
|
2411
|
+
effective_jobs = _resolve_parallel_jobs(n_jobs, len(nodes_data))
|
|
2412
|
+
results: list[tuple[NodeId, float]] | None = None
|
|
2413
|
+
if effective_jobs:
|
|
2414
|
+
grad_items = tuple(grads.items())
|
|
2415
|
+
try:
|
|
2416
|
+
import pickle
|
|
2417
|
+
|
|
2418
|
+
pickle.dumps((grad_items, weights, G), protocol=pickle.HIGHEST_PROTOCOL)
|
|
2419
|
+
except Exception:
|
|
2420
|
+
effective_jobs = None
|
|
2421
|
+
else:
|
|
2422
|
+
chunk_results: list[tuple[NodeId, float]] = []
|
|
2423
|
+
with ProcessPoolExecutor(max_workers=effective_jobs) as executor:
|
|
2424
|
+
futures = []
|
|
2425
|
+
node_ids: list[NodeId] = [n for n, _ in nodes_data]
|
|
2426
|
+
for start, end in _iter_chunk_offsets(len(node_ids), effective_jobs):
|
|
2427
|
+
if start == end:
|
|
2428
|
+
continue
|
|
2429
|
+
futures.append(
|
|
2430
|
+
executor.submit(
|
|
2431
|
+
_dnfr_hook_chunk_worker,
|
|
2432
|
+
G,
|
|
2433
|
+
node_ids[start:end],
|
|
2434
|
+
grad_items,
|
|
2435
|
+
weights,
|
|
2436
|
+
)
|
|
2437
|
+
)
|
|
2438
|
+
for future in futures:
|
|
2439
|
+
chunk_results.extend(future.result())
|
|
2440
|
+
results = chunk_results
|
|
2441
|
+
|
|
2442
|
+
if results is None:
|
|
2443
|
+
results = []
|
|
2444
|
+
for n, nd in nodes_data:
|
|
2445
|
+
total = 0.0
|
|
2446
|
+
for name, func in grads.items():
|
|
2447
|
+
w = weights.get(name, 0.0)
|
|
2448
|
+
if w:
|
|
2449
|
+
total += w * float(func(G, n, nd))
|
|
2450
|
+
results.append((n, total))
|
|
2451
|
+
|
|
2452
|
+
for node, value in results:
|
|
2453
|
+
set_dnfr(G, node, float(value))
|
|
645
2454
|
|
|
646
2455
|
_write_dnfr_metadata(G, weights=weights, hook_name=hook_name, note=note)
|
|
647
2456
|
|
|
648
2457
|
|
|
649
|
-
# ---
|
|
650
|
-
|
|
651
|
-
"""Example: ΔNFR from phase only (Kuramoto-like)."""
|
|
2458
|
+
# --- Example hooks (optional) ---
|
|
2459
|
+
|
|
652
2460
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
2461
|
+
class _PhaseGradient:
|
|
2462
|
+
"""Callable computing the phase contribution using cached trig values."""
|
|
2463
|
+
|
|
2464
|
+
__slots__ = ("cos", "sin")
|
|
2465
|
+
|
|
2466
|
+
def __init__(
|
|
2467
|
+
self,
|
|
2468
|
+
cos_map: Mapping[NodeId, float],
|
|
2469
|
+
sin_map: Mapping[NodeId, float],
|
|
2470
|
+
) -> None:
|
|
2471
|
+
self.cos: Mapping[NodeId, float] = cos_map
|
|
2472
|
+
self.sin: Mapping[NodeId, float] = sin_map
|
|
2473
|
+
|
|
2474
|
+
def __call__(
|
|
2475
|
+
self,
|
|
2476
|
+
G: TNFRGraph,
|
|
2477
|
+
n: NodeId,
|
|
2478
|
+
nd: Mapping[str, Any],
|
|
2479
|
+
) -> float:
|
|
2480
|
+
theta_val = get_theta_attr(nd, 0.0)
|
|
2481
|
+
th_i = float(theta_val if theta_val is not None else 0.0)
|
|
2482
|
+
neighbors = list(G.neighbors(n))
|
|
2483
|
+
if neighbors:
|
|
2484
|
+
th_bar = neighbor_phase_mean_list(
|
|
2485
|
+
neighbors,
|
|
2486
|
+
cos_th=self.cos,
|
|
2487
|
+
sin_th=self.sin,
|
|
2488
|
+
fallback=th_i,
|
|
2489
|
+
)
|
|
2490
|
+
else:
|
|
2491
|
+
th_bar = th_i
|
|
656
2492
|
return -angle_diff(th_i, th_bar) / math.pi
|
|
657
2493
|
|
|
2494
|
+
|
|
2495
|
+
class _NeighborAverageGradient:
|
|
2496
|
+
"""Callable computing neighbour averages for scalar attributes."""
|
|
2497
|
+
|
|
2498
|
+
__slots__ = ("alias", "values")
|
|
2499
|
+
|
|
2500
|
+
def __init__(
|
|
2501
|
+
self,
|
|
2502
|
+
alias: tuple[str, ...],
|
|
2503
|
+
values: MutableMapping[NodeId, float],
|
|
2504
|
+
) -> None:
|
|
2505
|
+
self.alias: tuple[str, ...] = alias
|
|
2506
|
+
self.values: MutableMapping[NodeId, float] = values
|
|
2507
|
+
|
|
2508
|
+
def __call__(
|
|
2509
|
+
self,
|
|
2510
|
+
G: TNFRGraph,
|
|
2511
|
+
n: NodeId,
|
|
2512
|
+
nd: Mapping[str, Any],
|
|
2513
|
+
) -> float:
|
|
2514
|
+
val = self.values.get(n)
|
|
2515
|
+
if val is None:
|
|
2516
|
+
val = float(get_attr(nd, self.alias, 0.0))
|
|
2517
|
+
self.values[n] = val
|
|
2518
|
+
neighbors = list(G.neighbors(n))
|
|
2519
|
+
if not neighbors:
|
|
2520
|
+
return 0.0
|
|
2521
|
+
total = 0.0
|
|
2522
|
+
for neigh in neighbors:
|
|
2523
|
+
neigh_val = self.values.get(neigh)
|
|
2524
|
+
if neigh_val is None:
|
|
2525
|
+
neigh_val = float(get_attr(G.nodes[neigh], self.alias, val))
|
|
2526
|
+
self.values[neigh] = neigh_val
|
|
2527
|
+
total += neigh_val
|
|
2528
|
+
return total / len(neighbors) - val
|
|
2529
|
+
|
|
2530
|
+
|
|
2531
|
+
def dnfr_phase_only(G: TNFRGraph, *, n_jobs: int | None = None) -> None:
|
|
2532
|
+
"""Compute ΔNFR from phase only (Kuramoto-like).
|
|
2533
|
+
|
|
2534
|
+
Parameters
|
|
2535
|
+
----------
|
|
2536
|
+
G : nx.Graph
|
|
2537
|
+
Graph whose nodes receive the ΔNFR assignment.
|
|
2538
|
+
n_jobs : int | None, optional
|
|
2539
|
+
Parallel worker hint used when NumPy is unavailable. Defaults to
|
|
2540
|
+
serial execution.
|
|
2541
|
+
"""
|
|
2542
|
+
|
|
2543
|
+
trig = compute_theta_trig(G.nodes(data=True))
|
|
2544
|
+
g_phase = _PhaseGradient(trig.cos, trig.sin)
|
|
658
2545
|
_apply_dnfr_hook(
|
|
659
2546
|
G,
|
|
660
2547
|
{"phase": g_phase},
|
|
661
2548
|
weights={"phase": 1.0},
|
|
662
2549
|
hook_name="dnfr_phase_only",
|
|
663
|
-
note="
|
|
2550
|
+
note="Example hook.",
|
|
2551
|
+
n_jobs=n_jobs,
|
|
664
2552
|
)
|
|
665
2553
|
|
|
666
2554
|
|
|
667
|
-
def dnfr_epi_vf_mixed(G) -> None:
|
|
668
|
-
"""
|
|
2555
|
+
def dnfr_epi_vf_mixed(G: TNFRGraph, *, n_jobs: int | None = None) -> None:
|
|
2556
|
+
"""Compute ΔNFR without phase, mixing EPI and νf.
|
|
669
2557
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
else:
|
|
679
|
-
epi_bar = float(epi_i)
|
|
680
|
-
return epi_bar - epi_i
|
|
681
|
-
|
|
682
|
-
def g_vf(G, n, nd):
|
|
683
|
-
vf_i = get_attr(nd, ALIAS_VF, 0.0)
|
|
684
|
-
neighbors = list(G.neighbors(n))
|
|
685
|
-
if neighbors:
|
|
686
|
-
total = 0.0
|
|
687
|
-
for v in neighbors:
|
|
688
|
-
total += float(get_attr(G.nodes[v], ALIAS_VF, vf_i))
|
|
689
|
-
vf_bar = total / len(neighbors)
|
|
690
|
-
else:
|
|
691
|
-
vf_bar = float(vf_i)
|
|
692
|
-
return vf_bar - vf_i
|
|
2558
|
+
Parameters
|
|
2559
|
+
----------
|
|
2560
|
+
G : nx.Graph
|
|
2561
|
+
Graph whose nodes receive the ΔNFR assignment.
|
|
2562
|
+
n_jobs : int | None, optional
|
|
2563
|
+
Parallel worker hint used when NumPy is unavailable. Defaults to
|
|
2564
|
+
serial execution.
|
|
2565
|
+
"""
|
|
693
2566
|
|
|
2567
|
+
epi_values = {
|
|
2568
|
+
n: float(get_attr(nd, ALIAS_EPI, 0.0)) for n, nd in G.nodes(data=True)
|
|
2569
|
+
}
|
|
2570
|
+
vf_values = {n: float(get_attr(nd, ALIAS_VF, 0.0)) for n, nd in G.nodes(data=True)}
|
|
2571
|
+
grads = {
|
|
2572
|
+
"epi": _NeighborAverageGradient(ALIAS_EPI, epi_values),
|
|
2573
|
+
"vf": _NeighborAverageGradient(ALIAS_VF, vf_values),
|
|
2574
|
+
}
|
|
694
2575
|
_apply_dnfr_hook(
|
|
695
2576
|
G,
|
|
696
|
-
|
|
2577
|
+
grads,
|
|
697
2578
|
weights={"phase": 0.0, "epi": 0.5, "vf": 0.5},
|
|
698
2579
|
hook_name="dnfr_epi_vf_mixed",
|
|
699
|
-
note="
|
|
2580
|
+
note="Example hook.",
|
|
2581
|
+
n_jobs=n_jobs,
|
|
700
2582
|
)
|
|
701
2583
|
|
|
702
2584
|
|
|
703
|
-
def dnfr_laplacian(G) -> None:
|
|
704
|
-
"""Explicit topological gradient using Laplacian over EPI and νf.
|
|
2585
|
+
def dnfr_laplacian(G: TNFRGraph, *, n_jobs: int | None = None) -> None:
|
|
2586
|
+
"""Explicit topological gradient using Laplacian over EPI and νf.
|
|
2587
|
+
|
|
2588
|
+
Parameters
|
|
2589
|
+
----------
|
|
2590
|
+
G : nx.Graph
|
|
2591
|
+
Graph whose nodes receive the ΔNFR assignment.
|
|
2592
|
+
n_jobs : int | None, optional
|
|
2593
|
+
Parallel worker hint used when NumPy is unavailable. Defaults to
|
|
2594
|
+
serial execution.
|
|
2595
|
+
"""
|
|
2596
|
+
|
|
705
2597
|
weights_cfg = get_param(G, "DNFR_WEIGHTS")
|
|
706
2598
|
wE = float(weights_cfg.get("epi", DEFAULTS["DNFR_WEIGHTS"]["epi"]))
|
|
707
2599
|
wV = float(weights_cfg.get("vf", DEFAULTS["DNFR_WEIGHTS"]["vf"]))
|
|
708
2600
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
def g_vf(G, n, nd):
|
|
719
|
-
vf = get_attr(nd, ALIAS_VF, 0.0)
|
|
720
|
-
neigh = list(G.neighbors(n))
|
|
721
|
-
deg = len(neigh) or 1
|
|
722
|
-
vf_bar = sum(get_attr(G.nodes[v], ALIAS_VF, vf) for v in neigh) / deg
|
|
723
|
-
return vf_bar - vf
|
|
724
|
-
|
|
2601
|
+
epi_values = {
|
|
2602
|
+
n: float(get_attr(nd, ALIAS_EPI, 0.0)) for n, nd in G.nodes(data=True)
|
|
2603
|
+
}
|
|
2604
|
+
vf_values = {n: float(get_attr(nd, ALIAS_VF, 0.0)) for n, nd in G.nodes(data=True)}
|
|
2605
|
+
grads = {
|
|
2606
|
+
"epi": _NeighborAverageGradient(ALIAS_EPI, epi_values),
|
|
2607
|
+
"vf": _NeighborAverageGradient(ALIAS_VF, vf_values),
|
|
2608
|
+
}
|
|
725
2609
|
_apply_dnfr_hook(
|
|
726
2610
|
G,
|
|
727
|
-
|
|
2611
|
+
grads,
|
|
728
2612
|
weights={"epi": wE, "vf": wV},
|
|
729
2613
|
hook_name="dnfr_laplacian",
|
|
730
|
-
note="
|
|
2614
|
+
note="Topological gradient",
|
|
2615
|
+
n_jobs=n_jobs,
|
|
731
2616
|
)
|
|
732
|
-
|
|
733
|
-
|