tnfr 4.5.2__py3-none-any.whl → 6.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tnfr/__init__.py +228 -49
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +106 -21
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +666 -512
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +2 -9
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +21 -7
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +42 -20
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +54 -20
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +0 -2
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +12 -0
- tnfr/config/__init__.pyi +8 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/{config.py → config/init.py} +11 -7
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +106 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +104 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +78 -24
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +1 -2
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +4 -12
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +9 -91
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +112 -634
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +201 -0
- tnfr/dynamics/aliases.py +22 -0
- tnfr/dynamics/coordination.py +343 -0
- tnfr/dynamics/dnfr.py +1936 -354
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +369 -75
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +8 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +56 -41
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +7 -7
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +54 -37
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +85 -38
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +19 -338
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +110 -30
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +1 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +55 -19
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +12 -10
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +99 -34
- tnfr/io.pyi +11 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +934 -294
- tnfr/metrics/common.py +1 -3
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +192 -34
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +707 -101
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +27 -13
- tnfr/metrics/glyph_timing.py +218 -38
- tnfr/metrics/reporting.py +22 -18
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +199 -25
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +53 -18
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +3 -7
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +148 -125
- tnfr/node.pyi +161 -0
- tnfr/observers.py +44 -30
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +14 -13
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +84 -52
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +86 -23
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +141 -57
- tnfr/presets.py +9 -54
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +259 -73
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +24 -17
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +55 -43
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +44 -267
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +3 -2
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +272 -82
- tnfr/trace.pyi +68 -0
- tnfr/types.py +345 -6
- tnfr/types.pyi +145 -0
- tnfr/utils/__init__.py +158 -0
- tnfr/utils/__init__.pyi +133 -0
- tnfr/utils/cache.py +755 -0
- tnfr/utils/cache.pyi +156 -0
- tnfr/{collections_utils.py → utils/data.py} +57 -90
- tnfr/utils/data.pyi +73 -0
- tnfr/utils/graph.py +87 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +746 -0
- tnfr/utils/init.pyi +85 -0
- tnfr/{json_utils.py → utils/io.py} +13 -18
- tnfr/utils/io.pyi +10 -0
- tnfr/utils/validators.py +130 -0
- tnfr/utils/validators.pyi +19 -0
- tnfr/validation/__init__.py +25 -0
- tnfr/validation/__init__.pyi +17 -0
- tnfr/validation/compatibility.py +59 -0
- tnfr/validation/compatibility.pyi +8 -0
- tnfr/validation/grammar.py +149 -0
- tnfr/validation/grammar.pyi +11 -0
- tnfr/validation/rules.py +194 -0
- tnfr/validation/rules.pyi +18 -0
- tnfr/validation/syntax.py +151 -0
- tnfr/validation/syntax.pyi +7 -0
- tnfr-6.0.0.dist-info/METADATA +135 -0
- tnfr-6.0.0.dist-info/RECORD +157 -0
- tnfr/graph_utils.py +0 -84
- tnfr/import_utils.py +0 -228
- tnfr/logging_utils.py +0 -116
- 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-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/node.py
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
"""Node utilities and structures for TNFR graphs."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Callable,
|
|
7
|
+
Iterable,
|
|
8
|
+
MutableMapping,
|
|
9
|
+
Optional,
|
|
10
|
+
Protocol,
|
|
11
|
+
SupportsFloat,
|
|
12
|
+
TypeVar,
|
|
13
|
+
)
|
|
5
14
|
from collections.abc import Hashable
|
|
6
15
|
import math
|
|
16
|
+
from dataclasses import dataclass
|
|
7
17
|
|
|
8
18
|
from .constants import get_aliases
|
|
9
19
|
from .alias import (
|
|
10
20
|
get_attr,
|
|
21
|
+
get_theta_attr,
|
|
11
22
|
get_attr_str,
|
|
12
23
|
set_attr,
|
|
13
24
|
set_attr_str,
|
|
@@ -15,12 +26,23 @@ from .alias import (
|
|
|
15
26
|
set_dnfr,
|
|
16
27
|
set_theta,
|
|
17
28
|
)
|
|
18
|
-
from .
|
|
29
|
+
from .types import (
|
|
30
|
+
CouplingWeight,
|
|
31
|
+
DeltaNFR,
|
|
32
|
+
EPIValue,
|
|
33
|
+
NodeId,
|
|
34
|
+
Phase,
|
|
35
|
+
SecondDerivativeEPI,
|
|
36
|
+
SenseIndex,
|
|
37
|
+
StructuralFrequency,
|
|
38
|
+
TNFRGraph,
|
|
39
|
+
)
|
|
40
|
+
from .utils import (
|
|
19
41
|
cached_node_list,
|
|
20
42
|
ensure_node_offset_map,
|
|
21
43
|
increment_edge_version,
|
|
44
|
+
supports_add_edge,
|
|
22
45
|
)
|
|
23
|
-
from .graph_utils import supports_add_edge
|
|
24
46
|
from .locking import get_lock
|
|
25
47
|
|
|
26
48
|
ALIAS_EPI = get_aliases("EPI")
|
|
@@ -31,88 +53,77 @@ ALIAS_EPI_KIND = get_aliases("EPI_KIND")
|
|
|
31
53
|
ALIAS_DNFR = get_aliases("DNFR")
|
|
32
54
|
ALIAS_D2EPI = get_aliases("D2EPI")
|
|
33
55
|
|
|
34
|
-
# Mapping of NodoNX attribute specifications used to generate property
|
|
35
|
-
# descriptors. Each entry defines the keyword arguments passed to
|
|
36
|
-
# ``_nx_attr_property`` for a given attribute name.
|
|
37
|
-
ATTR_SPECS: dict[str, dict] = {
|
|
38
|
-
"EPI": {"aliases": ALIAS_EPI},
|
|
39
|
-
"vf": {
|
|
40
|
-
"aliases": ALIAS_VF,
|
|
41
|
-
"setter": set_vf,
|
|
42
|
-
"use_graph_setter": True,
|
|
43
|
-
},
|
|
44
|
-
"theta": {
|
|
45
|
-
"aliases": ALIAS_THETA,
|
|
46
|
-
"setter": set_theta,
|
|
47
|
-
"use_graph_setter": True,
|
|
48
|
-
},
|
|
49
|
-
"Si": {"aliases": ALIAS_SI},
|
|
50
|
-
"epi_kind": {
|
|
51
|
-
"aliases": ALIAS_EPI_KIND,
|
|
52
|
-
"default": "",
|
|
53
|
-
"getter": get_attr_str,
|
|
54
|
-
"setter": set_attr_str,
|
|
55
|
-
"to_python": str,
|
|
56
|
-
"to_storage": str,
|
|
57
|
-
},
|
|
58
|
-
"dnfr": {
|
|
59
|
-
"aliases": ALIAS_DNFR,
|
|
60
|
-
"setter": set_dnfr,
|
|
61
|
-
"use_graph_setter": True,
|
|
62
|
-
},
|
|
63
|
-
"d2EPI": {"aliases": ALIAS_D2EPI},
|
|
64
|
-
}
|
|
65
|
-
|
|
66
56
|
T = TypeVar("T")
|
|
67
57
|
|
|
68
|
-
__all__ = ("
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
to_storage=float,
|
|
79
|
-
use_graph_setter=False,
|
|
80
|
-
):
|
|
81
|
-
"""Generate ``NodoNX`` property descriptors.
|
|
82
|
-
|
|
83
|
-
Parameters
|
|
84
|
-
----------
|
|
85
|
-
aliases:
|
|
86
|
-
Immutable tuple of aliases used to access the attribute in the
|
|
87
|
-
underlying ``networkx`` node.
|
|
88
|
-
default:
|
|
89
|
-
Value returned when the attribute is missing.
|
|
90
|
-
getter, setter:
|
|
91
|
-
Helper functions used to retrieve or store the value. ``setter`` can
|
|
92
|
-
either accept ``(mapping, aliases, value)`` or, when
|
|
93
|
-
``use_graph_setter`` is ``True``, ``(G, n, value)``.
|
|
94
|
-
to_python, to_storage:
|
|
95
|
-
Conversion helpers applied when getting or setting the value,
|
|
96
|
-
respectively.
|
|
97
|
-
use_graph_setter:
|
|
98
|
-
Whether ``setter`` expects ``(G, n, value)`` instead of
|
|
99
|
-
``(mapping, aliases, value)``.
|
|
58
|
+
__all__ = ("NodeNX", "NodeProtocol", "add_edge")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class AttrSpec:
|
|
63
|
+
"""Configuration required to expose a ``networkx`` node attribute.
|
|
64
|
+
|
|
65
|
+
``AttrSpec`` mirrors the defaults previously used by
|
|
66
|
+
:func:`_nx_attr_property` and centralises the descriptor generation
|
|
67
|
+
logic to keep a single source of truth for NodeNX attribute access.
|
|
100
68
|
"""
|
|
101
69
|
|
|
102
|
-
|
|
103
|
-
|
|
70
|
+
aliases: tuple[str, ...]
|
|
71
|
+
default: Any = 0.0
|
|
72
|
+
getter: Callable[[MutableMapping[str, Any], tuple[str, ...], Any], Any] = get_attr
|
|
73
|
+
setter: Callable[..., None] = set_attr
|
|
74
|
+
to_python: Callable[[Any], Any] = float
|
|
75
|
+
to_storage: Callable[[Any], Any] = float
|
|
76
|
+
use_graph_setter: bool = False
|
|
104
77
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
78
|
+
def build_property(self) -> property:
|
|
79
|
+
"""Create the property descriptor for ``NodeNX`` attributes."""
|
|
80
|
+
|
|
81
|
+
def fget(instance: "NodeNX") -> T:
|
|
82
|
+
return self.to_python(
|
|
83
|
+
self.getter(instance.G.nodes[instance.n], self.aliases, self.default)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def fset(instance: "NodeNX", value: T) -> None:
|
|
87
|
+
value = self.to_storage(value)
|
|
88
|
+
if self.use_graph_setter:
|
|
89
|
+
self.setter(instance.G, instance.n, value)
|
|
90
|
+
else:
|
|
91
|
+
self.setter(instance.G.nodes[instance.n], self.aliases, value)
|
|
92
|
+
|
|
93
|
+
return property(fget, fset)
|
|
111
94
|
|
|
112
|
-
|
|
95
|
+
|
|
96
|
+
# Mapping of NodeNX attribute specifications used to generate property
|
|
97
|
+
# descriptors. Each entry defines the keyword arguments passed to
|
|
98
|
+
# ``AttrSpec.build_property`` for a given attribute name.
|
|
99
|
+
ATTR_SPECS: dict[str, AttrSpec] = {
|
|
100
|
+
"EPI": AttrSpec(aliases=ALIAS_EPI),
|
|
101
|
+
"vf": AttrSpec(aliases=ALIAS_VF, setter=set_vf, use_graph_setter=True),
|
|
102
|
+
"theta": AttrSpec(
|
|
103
|
+
aliases=ALIAS_THETA,
|
|
104
|
+
getter=lambda mapping, _aliases, default: get_theta_attr(mapping, default),
|
|
105
|
+
setter=set_theta,
|
|
106
|
+
use_graph_setter=True,
|
|
107
|
+
),
|
|
108
|
+
"Si": AttrSpec(aliases=ALIAS_SI),
|
|
109
|
+
"epi_kind": AttrSpec(
|
|
110
|
+
aliases=ALIAS_EPI_KIND,
|
|
111
|
+
default="",
|
|
112
|
+
getter=get_attr_str,
|
|
113
|
+
setter=set_attr_str,
|
|
114
|
+
to_python=str,
|
|
115
|
+
to_storage=str,
|
|
116
|
+
),
|
|
117
|
+
"dnfr": AttrSpec(aliases=ALIAS_DNFR, setter=set_dnfr, use_graph_setter=True),
|
|
118
|
+
"d2EPI": AttrSpec(aliases=ALIAS_D2EPI),
|
|
119
|
+
}
|
|
113
120
|
|
|
114
121
|
|
|
115
|
-
def _add_edge_common(
|
|
122
|
+
def _add_edge_common(
|
|
123
|
+
n1: NodeId,
|
|
124
|
+
n2: NodeId,
|
|
125
|
+
weight: CouplingWeight | SupportsFloat | str,
|
|
126
|
+
) -> Optional[CouplingWeight]:
|
|
116
127
|
"""Validate basic edge constraints.
|
|
117
128
|
|
|
118
129
|
Returns the parsed weight if the edge can be added. ``None`` is returned
|
|
@@ -132,12 +143,12 @@ def _add_edge_common(n1, n2, weight) -> Optional[float]:
|
|
|
132
143
|
|
|
133
144
|
|
|
134
145
|
def add_edge(
|
|
135
|
-
graph,
|
|
136
|
-
n1,
|
|
137
|
-
n2,
|
|
138
|
-
weight,
|
|
146
|
+
graph: TNFRGraph,
|
|
147
|
+
n1: NodeId,
|
|
148
|
+
n2: NodeId,
|
|
149
|
+
weight: CouplingWeight | SupportsFloat | str,
|
|
139
150
|
overwrite: bool = False,
|
|
140
|
-
):
|
|
151
|
+
) -> None:
|
|
141
152
|
"""Add an edge between ``n1`` and ``n2`` in a ``networkx`` graph."""
|
|
142
153
|
|
|
143
154
|
weight = _add_edge_common(n1, n2, weight)
|
|
@@ -154,60 +165,70 @@ def add_edge(
|
|
|
154
165
|
increment_edge_version(graph)
|
|
155
166
|
|
|
156
167
|
|
|
157
|
-
class
|
|
168
|
+
class NodeProtocol(Protocol):
|
|
158
169
|
"""Minimal protocol for TNFR nodes."""
|
|
159
170
|
|
|
160
|
-
EPI:
|
|
161
|
-
vf:
|
|
162
|
-
theta:
|
|
163
|
-
Si:
|
|
171
|
+
EPI: EPIValue
|
|
172
|
+
vf: StructuralFrequency
|
|
173
|
+
theta: Phase
|
|
174
|
+
Si: SenseIndex
|
|
164
175
|
epi_kind: str
|
|
165
|
-
dnfr:
|
|
166
|
-
d2EPI:
|
|
167
|
-
graph:
|
|
176
|
+
dnfr: DeltaNFR
|
|
177
|
+
d2EPI: SecondDerivativeEPI
|
|
178
|
+
graph: MutableMapping[str, Any]
|
|
168
179
|
|
|
169
|
-
def neighbors(self) -> Iterable[
|
|
180
|
+
def neighbors(self) -> Iterable[NodeProtocol | Hashable]:
|
|
181
|
+
...
|
|
170
182
|
|
|
171
|
-
def _glyph_storage(self) -> MutableMapping[str, object]:
|
|
183
|
+
def _glyph_storage(self) -> MutableMapping[str, object]:
|
|
184
|
+
...
|
|
172
185
|
|
|
173
|
-
def has_edge(self, other: "
|
|
186
|
+
def has_edge(self, other: "NodeProtocol") -> bool:
|
|
187
|
+
...
|
|
174
188
|
|
|
175
189
|
def add_edge(
|
|
176
|
-
self,
|
|
177
|
-
|
|
190
|
+
self,
|
|
191
|
+
other: NodeProtocol,
|
|
192
|
+
weight: CouplingWeight,
|
|
193
|
+
*,
|
|
194
|
+
overwrite: bool = False,
|
|
195
|
+
) -> None:
|
|
196
|
+
...
|
|
178
197
|
|
|
179
|
-
def offset(self) -> int:
|
|
198
|
+
def offset(self) -> int:
|
|
199
|
+
...
|
|
180
200
|
|
|
181
|
-
def all_nodes(self) -> Iterable[
|
|
201
|
+
def all_nodes(self) -> Iterable[NodeProtocol]:
|
|
202
|
+
...
|
|
182
203
|
|
|
183
204
|
|
|
184
|
-
class
|
|
205
|
+
class NodeNX(NodeProtocol):
|
|
185
206
|
"""Adapter for ``networkx`` nodes."""
|
|
186
207
|
|
|
187
|
-
# Statically defined property descriptors for ``
|
|
208
|
+
# Statically defined property descriptors for ``NodeNX`` attributes.
|
|
188
209
|
# Declaring them here makes the attributes discoverable by type checkers
|
|
189
210
|
# and IDEs, avoiding the previous runtime ``setattr`` loop.
|
|
190
|
-
EPI:
|
|
191
|
-
vf:
|
|
192
|
-
theta:
|
|
193
|
-
Si:
|
|
194
|
-
epi_kind: str =
|
|
195
|
-
dnfr:
|
|
196
|
-
d2EPI:
|
|
197
|
-
|
|
198
|
-
def __init__(self, G, n):
|
|
199
|
-
self.G = G
|
|
200
|
-
self.n = n
|
|
201
|
-
self.graph = G.graph
|
|
211
|
+
EPI: EPIValue = ATTR_SPECS["EPI"].build_property()
|
|
212
|
+
vf: StructuralFrequency = ATTR_SPECS["vf"].build_property()
|
|
213
|
+
theta: Phase = ATTR_SPECS["theta"].build_property()
|
|
214
|
+
Si: SenseIndex = ATTR_SPECS["Si"].build_property()
|
|
215
|
+
epi_kind: str = ATTR_SPECS["epi_kind"].build_property()
|
|
216
|
+
dnfr: DeltaNFR = ATTR_SPECS["dnfr"].build_property()
|
|
217
|
+
d2EPI: SecondDerivativeEPI = ATTR_SPECS["d2EPI"].build_property()
|
|
218
|
+
|
|
219
|
+
def __init__(self, G: TNFRGraph, n: NodeId) -> None:
|
|
220
|
+
self.G: TNFRGraph = G
|
|
221
|
+
self.n: NodeId = n
|
|
222
|
+
self.graph: MutableMapping[str, Any] = G.graph
|
|
202
223
|
G.graph.setdefault("_node_cache", {})[n] = self
|
|
203
224
|
|
|
204
|
-
def _glyph_storage(self):
|
|
225
|
+
def _glyph_storage(self) -> MutableMapping[str, Any]:
|
|
205
226
|
return self.G.nodes[self.n]
|
|
206
227
|
|
|
207
228
|
@classmethod
|
|
208
|
-
def from_graph(cls, G, n):
|
|
209
|
-
"""Return cached ``
|
|
210
|
-
lock = get_lock(f"
|
|
229
|
+
def from_graph(cls, G: TNFRGraph, n: NodeId) -> "NodeNX":
|
|
230
|
+
"""Return cached ``NodeNX`` for ``(G, n)`` with thread safety."""
|
|
231
|
+
lock = get_lock(f"node_nx_cache_{id(G)}")
|
|
211
232
|
with lock:
|
|
212
233
|
cache = G.graph.setdefault("_node_cache", {})
|
|
213
234
|
node = cache.get(n)
|
|
@@ -215,23 +236,27 @@ class NodoNX(NodoProtocol):
|
|
|
215
236
|
node = cls(G, n)
|
|
216
237
|
return node
|
|
217
238
|
|
|
218
|
-
def neighbors(self) -> Iterable[
|
|
239
|
+
def neighbors(self) -> Iterable[NodeId]:
|
|
219
240
|
"""Iterate neighbour identifiers (IDs).
|
|
220
241
|
|
|
221
242
|
Wrap each resulting ID with :meth:`from_graph` to obtain the cached
|
|
222
|
-
``
|
|
243
|
+
``NodeNX`` instance when actual node objects are required.
|
|
223
244
|
"""
|
|
224
245
|
return self.G.neighbors(self.n)
|
|
225
246
|
|
|
226
|
-
def has_edge(self, other:
|
|
227
|
-
if isinstance(other,
|
|
247
|
+
def has_edge(self, other: NodeProtocol) -> bool:
|
|
248
|
+
if isinstance(other, NodeNX):
|
|
228
249
|
return self.G.has_edge(self.n, other.n)
|
|
229
250
|
raise NotImplementedError
|
|
230
251
|
|
|
231
252
|
def add_edge(
|
|
232
|
-
self,
|
|
253
|
+
self,
|
|
254
|
+
other: NodeProtocol,
|
|
255
|
+
weight: CouplingWeight,
|
|
256
|
+
*,
|
|
257
|
+
overwrite: bool = False,
|
|
233
258
|
) -> None:
|
|
234
|
-
if isinstance(other,
|
|
259
|
+
if isinstance(other, NodeNX):
|
|
235
260
|
add_edge(
|
|
236
261
|
self.G,
|
|
237
262
|
self.n,
|
|
@@ -246,12 +271,10 @@ class NodoNX(NodoProtocol):
|
|
|
246
271
|
mapping = ensure_node_offset_map(self.G)
|
|
247
272
|
return mapping.get(self.n, 0)
|
|
248
273
|
|
|
249
|
-
def all_nodes(self) -> Iterable[
|
|
274
|
+
def all_nodes(self) -> Iterable[NodeProtocol]:
|
|
250
275
|
override = self.graph.get("_all_nodes")
|
|
251
276
|
if override is not None:
|
|
252
277
|
return override
|
|
253
278
|
|
|
254
279
|
nodes = cached_node_list(self.G)
|
|
255
|
-
return tuple(
|
|
256
|
-
|
|
257
|
-
|
|
280
|
+
return tuple(NodeNX.from_graph(self.G, v) for v in nodes)
|
tnfr/node.pyi
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Hashable, Iterable, MutableMapping
|
|
4
|
+
from typing import Any, Callable, Optional, Protocol, SupportsFloat, TypeVar
|
|
5
|
+
|
|
6
|
+
from .types import (
|
|
7
|
+
CouplingWeight,
|
|
8
|
+
DeltaNFR,
|
|
9
|
+
EPIValue,
|
|
10
|
+
NodeId,
|
|
11
|
+
Phase,
|
|
12
|
+
SecondDerivativeEPI,
|
|
13
|
+
SenseIndex,
|
|
14
|
+
StructuralFrequency,
|
|
15
|
+
TNFRGraph,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
__all__ = ("NodeNX", "NodeProtocol", "add_edge")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AttrSpec:
|
|
24
|
+
aliases: tuple[str, ...]
|
|
25
|
+
default: Any
|
|
26
|
+
getter: Callable[[MutableMapping[str, Any], tuple[str, ...], Any], Any]
|
|
27
|
+
setter: Callable[..., None]
|
|
28
|
+
to_python: Callable[[Any], Any]
|
|
29
|
+
to_storage: Callable[[Any], Any]
|
|
30
|
+
use_graph_setter: bool
|
|
31
|
+
|
|
32
|
+
def build_property(self) -> property: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
ALIAS_EPI: tuple[str, ...]
|
|
36
|
+
ALIAS_VF: tuple[str, ...]
|
|
37
|
+
ALIAS_THETA: tuple[str, ...]
|
|
38
|
+
ALIAS_SI: tuple[str, ...]
|
|
39
|
+
ALIAS_EPI_KIND: tuple[str, ...]
|
|
40
|
+
ALIAS_DNFR: tuple[str, ...]
|
|
41
|
+
ALIAS_D2EPI: tuple[str, ...]
|
|
42
|
+
|
|
43
|
+
ATTR_SPECS: dict[str, AttrSpec]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _add_edge_common(
|
|
47
|
+
n1: NodeId,
|
|
48
|
+
n2: NodeId,
|
|
49
|
+
weight: CouplingWeight | SupportsFloat | str,
|
|
50
|
+
) -> Optional[CouplingWeight]: ...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def add_edge(
|
|
54
|
+
graph: TNFRGraph,
|
|
55
|
+
n1: NodeId,
|
|
56
|
+
n2: NodeId,
|
|
57
|
+
weight: CouplingWeight | SupportsFloat | str,
|
|
58
|
+
overwrite: bool = ...,
|
|
59
|
+
) -> None: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class NodeProtocol(Protocol):
|
|
63
|
+
EPI: EPIValue
|
|
64
|
+
vf: StructuralFrequency
|
|
65
|
+
theta: Phase
|
|
66
|
+
Si: SenseIndex
|
|
67
|
+
epi_kind: str
|
|
68
|
+
dnfr: DeltaNFR
|
|
69
|
+
d2EPI: SecondDerivativeEPI
|
|
70
|
+
graph: MutableMapping[str, Any]
|
|
71
|
+
|
|
72
|
+
def neighbors(self) -> Iterable[NodeProtocol | Hashable]: ...
|
|
73
|
+
|
|
74
|
+
def _glyph_storage(self) -> MutableMapping[str, object]: ...
|
|
75
|
+
|
|
76
|
+
def has_edge(self, other: NodeProtocol) -> bool: ...
|
|
77
|
+
|
|
78
|
+
def add_edge(
|
|
79
|
+
self,
|
|
80
|
+
other: NodeProtocol,
|
|
81
|
+
weight: CouplingWeight,
|
|
82
|
+
*,
|
|
83
|
+
overwrite: bool = ...,
|
|
84
|
+
) -> None: ...
|
|
85
|
+
|
|
86
|
+
def offset(self) -> int: ...
|
|
87
|
+
|
|
88
|
+
def all_nodes(self) -> Iterable[NodeProtocol]: ...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class NodeNX(NodeProtocol):
|
|
92
|
+
G: TNFRGraph
|
|
93
|
+
n: NodeId
|
|
94
|
+
graph: MutableMapping[str, Any]
|
|
95
|
+
|
|
96
|
+
def __init__(self, G: TNFRGraph, n: NodeId) -> None: ...
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_graph(cls, G: TNFRGraph, n: NodeId) -> "NodeNX": ...
|
|
100
|
+
|
|
101
|
+
def _glyph_storage(self) -> MutableMapping[str, Any]: ...
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def EPI(self) -> EPIValue: ...
|
|
105
|
+
|
|
106
|
+
@EPI.setter
|
|
107
|
+
def EPI(self, value: EPIValue) -> None: ...
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def vf(self) -> StructuralFrequency: ...
|
|
111
|
+
|
|
112
|
+
@vf.setter
|
|
113
|
+
def vf(self, value: StructuralFrequency) -> None: ...
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def theta(self) -> Phase: ...
|
|
117
|
+
|
|
118
|
+
@theta.setter
|
|
119
|
+
def theta(self, value: Phase) -> None: ...
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def Si(self) -> SenseIndex: ...
|
|
123
|
+
|
|
124
|
+
@Si.setter
|
|
125
|
+
def Si(self, value: SenseIndex) -> None: ...
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def epi_kind(self) -> str: ...
|
|
129
|
+
|
|
130
|
+
@epi_kind.setter
|
|
131
|
+
def epi_kind(self, value: str) -> None: ...
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def dnfr(self) -> DeltaNFR: ...
|
|
135
|
+
|
|
136
|
+
@dnfr.setter
|
|
137
|
+
def dnfr(self, value: DeltaNFR) -> None: ...
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def d2EPI(self) -> SecondDerivativeEPI: ...
|
|
141
|
+
|
|
142
|
+
@d2EPI.setter
|
|
143
|
+
def d2EPI(self, value: SecondDerivativeEPI) -> None: ...
|
|
144
|
+
|
|
145
|
+
def neighbors(self) -> Iterable[NodeId]: ...
|
|
146
|
+
|
|
147
|
+
def has_edge(self, other: NodeProtocol) -> bool: ...
|
|
148
|
+
|
|
149
|
+
def add_edge(
|
|
150
|
+
self,
|
|
151
|
+
other: NodeProtocol,
|
|
152
|
+
weight: CouplingWeight,
|
|
153
|
+
*,
|
|
154
|
+
overwrite: bool = ...,
|
|
155
|
+
) -> None: ...
|
|
156
|
+
|
|
157
|
+
def offset(self) -> int: ...
|
|
158
|
+
|
|
159
|
+
def all_nodes(self) -> Iterable[NodeProtocol]: ...
|
|
160
|
+
|
|
161
|
+
|
tnfr/observers.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Observer management."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
4
6
|
from functools import partial
|
|
5
7
|
import statistics
|
|
6
8
|
from statistics import StatisticsError, pvariance
|
|
7
9
|
|
|
8
|
-
from .
|
|
9
|
-
from .alias import get_attr
|
|
10
|
+
from .alias import get_theta_attr
|
|
10
11
|
from .helpers.numeric import angle_diff
|
|
11
12
|
from .callback_utils import CallbackEvent, callback_manager
|
|
12
13
|
from .glyph_history import (
|
|
@@ -14,15 +15,17 @@ from .glyph_history import (
|
|
|
14
15
|
count_glyphs,
|
|
15
16
|
append_metric,
|
|
16
17
|
)
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
18
|
+
from .types import Glyph, GlyphLoadDistribution, TNFRGraph
|
|
19
|
+
from .utils import (
|
|
20
|
+
get_logger,
|
|
21
|
+
get_numpy,
|
|
22
|
+
mix_groups,
|
|
23
|
+
normalize_counter,
|
|
24
|
+
validate_window,
|
|
25
|
+
)
|
|
26
|
+
from .config.constants import GLYPH_GROUPS
|
|
19
27
|
from .gamma import kuramoto_R_psi
|
|
20
|
-
from .logging_utils import get_logger
|
|
21
|
-
from .import_utils import get_numpy
|
|
22
28
|
from .metrics.common import compute_coherence
|
|
23
|
-
from .validators import validate_window
|
|
24
|
-
|
|
25
|
-
ALIAS_THETA = get_aliases("THETA")
|
|
26
29
|
|
|
27
30
|
__all__ = (
|
|
28
31
|
"attach_standard_observer",
|
|
@@ -42,11 +45,10 @@ DEFAULT_GLYPH_LOAD_SPAN = 50
|
|
|
42
45
|
DEFAULT_WBAR_SPAN = 25
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
|
|
46
48
|
# -------------------------
|
|
47
|
-
#
|
|
49
|
+
# Standard Γ(R) observer
|
|
48
50
|
# -------------------------
|
|
49
|
-
def _std_log(kind: str, G, ctx:
|
|
51
|
+
def _std_log(kind: str, G: TNFRGraph, ctx: Mapping[str, object]) -> None:
|
|
50
52
|
"""Store compact events in ``history['events']``."""
|
|
51
53
|
h = ensure_history(G)
|
|
52
54
|
append_metric(h, "events", (kind, dict(ctx)))
|
|
@@ -59,7 +61,7 @@ _STD_CALLBACKS = {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
|
|
62
|
-
def attach_standard_observer(G):
|
|
64
|
+
def attach_standard_observer(G: TNFRGraph) -> TNFRGraph:
|
|
63
65
|
"""Register standard callbacks: before_step, after_step, on_remesh."""
|
|
64
66
|
if G.graph.get("_STD_OBSERVER"):
|
|
65
67
|
return G
|
|
@@ -69,12 +71,12 @@ def attach_standard_observer(G):
|
|
|
69
71
|
return G
|
|
70
72
|
|
|
71
73
|
|
|
72
|
-
def _ensure_nodes(G) -> bool:
|
|
74
|
+
def _ensure_nodes(G: TNFRGraph) -> bool:
|
|
73
75
|
"""Return ``True`` when the graph has nodes."""
|
|
74
76
|
return bool(G.number_of_nodes())
|
|
75
77
|
|
|
76
78
|
|
|
77
|
-
def kuramoto_metrics(G) -> tuple[float, float]:
|
|
79
|
+
def kuramoto_metrics(G: TNFRGraph) -> tuple[float, float]:
|
|
78
80
|
"""Return Kuramoto order ``R`` and mean phase ``ψ``.
|
|
79
81
|
|
|
80
82
|
Delegates to :func:`kuramoto_R_psi` and performs the computation exactly
|
|
@@ -83,7 +85,11 @@ def kuramoto_metrics(G) -> tuple[float, float]:
|
|
|
83
85
|
return kuramoto_R_psi(G)
|
|
84
86
|
|
|
85
87
|
|
|
86
|
-
def phase_sync(
|
|
88
|
+
def phase_sync(
|
|
89
|
+
G: TNFRGraph,
|
|
90
|
+
R: float | None = None,
|
|
91
|
+
psi: float | None = None,
|
|
92
|
+
) -> float:
|
|
87
93
|
if not _ensure_nodes(G):
|
|
88
94
|
return 1.0
|
|
89
95
|
if R is None or psi is None:
|
|
@@ -92,10 +98,11 @@ def phase_sync(G, R: float | None = None, psi: float | None = None) -> float:
|
|
|
92
98
|
R = R_calc
|
|
93
99
|
if psi is None:
|
|
94
100
|
psi = psi_calc
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
def _theta(nd: Mapping[str, object]) -> float:
|
|
102
|
+
value = get_theta_attr(nd, 0.0)
|
|
103
|
+
return float(value) if value is not None else 0.0
|
|
104
|
+
|
|
105
|
+
diffs = (angle_diff(_theta(data), psi) for _, data in G.nodes(data=True))
|
|
99
106
|
# Try NumPy for a vectorised population variance
|
|
100
107
|
np = get_numpy()
|
|
101
108
|
if np is not None:
|
|
@@ -110,7 +117,7 @@ def phase_sync(G, R: float | None = None, psi: float | None = None) -> float:
|
|
|
110
117
|
|
|
111
118
|
|
|
112
119
|
def kuramoto_order(
|
|
113
|
-
G, R: float | None = None, psi: float | None = None
|
|
120
|
+
G: TNFRGraph, R: float | None = None, psi: float | None = None
|
|
114
121
|
) -> float:
|
|
115
122
|
"""R in [0,1], 1 means perfectly aligned phases."""
|
|
116
123
|
if not _ensure_nodes(G):
|
|
@@ -120,7 +127,7 @@ def kuramoto_order(
|
|
|
120
127
|
return float(R)
|
|
121
128
|
|
|
122
129
|
|
|
123
|
-
def glyph_load(G, window: int | None = None) ->
|
|
130
|
+
def glyph_load(G: TNFRGraph, window: int | None = None) -> GlyphLoadDistribution:
|
|
124
131
|
"""Return distribution of glyphs applied in the network.
|
|
125
132
|
|
|
126
133
|
- ``window``: if provided, count only the last ``window`` events per node;
|
|
@@ -128,21 +135,28 @@ def glyph_load(G, window: int | None = None) -> dict:
|
|
|
128
135
|
Returns a dict with proportions per glyph and useful aggregates.
|
|
129
136
|
"""
|
|
130
137
|
if window == 0:
|
|
131
|
-
return {"_count": 0}
|
|
138
|
+
return {"_count": 0.0}
|
|
132
139
|
if window is None:
|
|
133
140
|
window_int = DEFAULT_GLYPH_LOAD_SPAN
|
|
134
141
|
else:
|
|
135
142
|
window_int = validate_window(window, positive=True)
|
|
136
143
|
total = count_glyphs(G, window=window_int, last_only=(window_int == 1))
|
|
137
|
-
|
|
144
|
+
dist_raw, count = normalize_counter(total)
|
|
138
145
|
if count == 0:
|
|
139
|
-
return {"_count": 0}
|
|
140
|
-
dist = mix_groups(
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
return {"_count": 0.0}
|
|
147
|
+
dist = mix_groups(dist_raw, GLYPH_GROUPS)
|
|
148
|
+
glyph_dist: GlyphLoadDistribution = {}
|
|
149
|
+
for key, value in dist.items():
|
|
150
|
+
try:
|
|
151
|
+
glyph_key: Glyph | str = Glyph(key)
|
|
152
|
+
except ValueError:
|
|
153
|
+
glyph_key = key
|
|
154
|
+
glyph_dist[glyph_key] = value
|
|
155
|
+
glyph_dist["_count"] = float(count)
|
|
156
|
+
return glyph_dist
|
|
143
157
|
|
|
144
158
|
|
|
145
|
-
def wbar(G, window: int | None = None) -> float:
|
|
159
|
+
def wbar(G: TNFRGraph, window: int | None = None) -> float:
|
|
146
160
|
"""Return W̄ = mean of ``C(t)`` over a recent window.
|
|
147
161
|
|
|
148
162
|
Uses :func:`ensure_history` to obtain ``G.graph['history']`` and falls back
|
|
@@ -151,7 +165,7 @@ def wbar(G, window: int | None = None) -> float:
|
|
|
151
165
|
hist = ensure_history(G)
|
|
152
166
|
cs = list(hist.get("C_steps", []))
|
|
153
167
|
if not cs:
|
|
154
|
-
# fallback:
|
|
168
|
+
# fallback: instantaneous coherence
|
|
155
169
|
return compute_coherence(G)
|
|
156
170
|
w_param = DEFAULT_WBAR_SPAN if window is None else window
|
|
157
171
|
w = validate_window(w_param, positive=True)
|