tnfr 4.5.1__py3-none-any.whl → 6.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tnfr/__init__.py +270 -90
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +631 -0
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +732 -0
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +381 -0
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +89 -0
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +199 -0
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +322 -0
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +34 -0
- 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/init.py +36 -0
- 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 +228 -0
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +158 -0
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +102 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +16 -0
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +136 -0
- 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 +2315 -0
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +561 -0
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +34 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +216 -0
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +283 -0
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +320 -89
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +337 -0
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +23 -153
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +151 -0
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +88 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +214 -0
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +199 -0
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +311 -0
- tnfr/io.pyi +11 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +1469 -0
- tnfr/metrics/common.py +149 -0
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +259 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +840 -0
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +151 -0
- tnfr/metrics/glyph_timing.py +369 -0
- tnfr/metrics/reporting.py +152 -0
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +294 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +216 -0
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +105 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +255 -177
- tnfr/node.pyi +161 -0
- tnfr/observers.py +154 -150
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +135 -134
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +452 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +266 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +569 -0
- tnfr/presets.py +10 -23
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +440 -0
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +217 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +307 -142
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +69 -164
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +61 -0
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +520 -95
- tnfr/trace.pyi +68 -0
- tnfr/types.py +382 -17
- 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/utils/data.py +267 -0
- 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/utils/io.py +157 -0
- 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/cli.py +0 -322
- tnfr/config.py +0 -41
- tnfr/constants.py +0 -277
- tnfr/dynamics.py +0 -814
- tnfr/helpers.py +0 -264
- tnfr/main.py +0 -47
- tnfr/metrics.py +0 -597
- tnfr/operators.py +0 -525
- tnfr/program.py +0 -176
- tnfr/scenarios.py +0 -34
- tnfr/validators.py +0 -38
- tnfr-4.5.1.dist-info/METADATA +0 -221
- tnfr-4.5.1.dist-info/RECORD +0 -28
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/initialization.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Node initialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import random
|
|
5
|
+
from typing import TYPE_CHECKING, cast
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from .constants import VF_KEY, THETA_KEY, get_graph_param
|
|
10
|
+
from .helpers.numeric import clamp
|
|
11
|
+
from .rng import make_rng
|
|
12
|
+
from .types import NodeInitAttrMap
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
15
|
+
import networkx as nx
|
|
16
|
+
|
|
17
|
+
__all__ = ("InitParams", "init_node_attrs")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class InitParams:
|
|
22
|
+
"""Parameters governing node initialisation."""
|
|
23
|
+
|
|
24
|
+
seed: int | None
|
|
25
|
+
init_rand_phase: bool
|
|
26
|
+
th_min: float
|
|
27
|
+
th_max: float
|
|
28
|
+
vf_mode: str
|
|
29
|
+
vf_min_lim: float
|
|
30
|
+
vf_max_lim: float
|
|
31
|
+
vf_uniform_min: float | None
|
|
32
|
+
vf_uniform_max: float | None
|
|
33
|
+
vf_mean: float
|
|
34
|
+
vf_std: float
|
|
35
|
+
clamp_to_limits: bool
|
|
36
|
+
si_min: float
|
|
37
|
+
si_max: float
|
|
38
|
+
epi_val: float
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_graph(cls, G: "nx.Graph") -> "InitParams":
|
|
42
|
+
"""Construct ``InitParams`` from ``G.graph`` configuration."""
|
|
43
|
+
|
|
44
|
+
return cls(
|
|
45
|
+
seed=get_graph_param(G, "RANDOM_SEED", int),
|
|
46
|
+
init_rand_phase=get_graph_param(G, "INIT_RANDOM_PHASE", bool),
|
|
47
|
+
th_min=get_graph_param(G, "INIT_THETA_MIN"),
|
|
48
|
+
th_max=get_graph_param(G, "INIT_THETA_MAX"),
|
|
49
|
+
vf_mode=str(get_graph_param(G, "INIT_VF_MODE", str)).lower(),
|
|
50
|
+
vf_min_lim=get_graph_param(G, "VF_MIN"),
|
|
51
|
+
vf_max_lim=get_graph_param(G, "VF_MAX"),
|
|
52
|
+
vf_uniform_min=get_graph_param(G, "INIT_VF_MIN"),
|
|
53
|
+
vf_uniform_max=get_graph_param(G, "INIT_VF_MAX"),
|
|
54
|
+
vf_mean=get_graph_param(G, "INIT_VF_MEAN"),
|
|
55
|
+
vf_std=get_graph_param(G, "INIT_VF_STD"),
|
|
56
|
+
clamp_to_limits=get_graph_param(
|
|
57
|
+
G, "INIT_VF_CLAMP_TO_LIMITS", bool
|
|
58
|
+
),
|
|
59
|
+
si_min=get_graph_param(G, "INIT_SI_MIN"),
|
|
60
|
+
si_max=get_graph_param(G, "INIT_SI_MAX"),
|
|
61
|
+
epi_val=get_graph_param(G, "INIT_EPI_VALUE"),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _init_phase(
|
|
66
|
+
nd: NodeInitAttrMap,
|
|
67
|
+
rng: random.Random,
|
|
68
|
+
*,
|
|
69
|
+
override: bool,
|
|
70
|
+
random_phase: bool,
|
|
71
|
+
th_min: float,
|
|
72
|
+
th_max: float,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Initialise ``θ`` in ``nd``."""
|
|
75
|
+
if random_phase:
|
|
76
|
+
if override or THETA_KEY not in nd:
|
|
77
|
+
nd[THETA_KEY] = rng.uniform(th_min, th_max)
|
|
78
|
+
else:
|
|
79
|
+
if override:
|
|
80
|
+
nd[THETA_KEY] = 0.0
|
|
81
|
+
else:
|
|
82
|
+
nd.setdefault(THETA_KEY, 0.0)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _init_vf(
|
|
86
|
+
nd: NodeInitAttrMap,
|
|
87
|
+
rng: random.Random,
|
|
88
|
+
*,
|
|
89
|
+
override: bool,
|
|
90
|
+
mode: str,
|
|
91
|
+
vf_uniform_min: float,
|
|
92
|
+
vf_uniform_max: float,
|
|
93
|
+
vf_mean: float,
|
|
94
|
+
vf_std: float,
|
|
95
|
+
vf_min_lim: float,
|
|
96
|
+
vf_max_lim: float,
|
|
97
|
+
clamp_to_limits: bool,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Initialise ``νf`` in ``nd``."""
|
|
100
|
+
if mode == "uniform":
|
|
101
|
+
vf = rng.uniform(vf_uniform_min, vf_uniform_max)
|
|
102
|
+
elif mode == "normal":
|
|
103
|
+
for _ in range(16):
|
|
104
|
+
cand = rng.normalvariate(vf_mean, vf_std)
|
|
105
|
+
if vf_min_lim <= cand <= vf_max_lim:
|
|
106
|
+
vf = cand
|
|
107
|
+
break
|
|
108
|
+
else:
|
|
109
|
+
vf = min(
|
|
110
|
+
max(rng.normalvariate(vf_mean, vf_std), vf_min_lim),
|
|
111
|
+
vf_max_lim,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
vf = float(nd.get(VF_KEY, 0.5))
|
|
115
|
+
if clamp_to_limits:
|
|
116
|
+
vf = clamp(vf, vf_min_lim, vf_max_lim)
|
|
117
|
+
if override or VF_KEY not in nd:
|
|
118
|
+
nd[VF_KEY] = vf
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _init_si_epi(
|
|
122
|
+
nd: NodeInitAttrMap,
|
|
123
|
+
rng: random.Random,
|
|
124
|
+
*,
|
|
125
|
+
override: bool,
|
|
126
|
+
si_min: float,
|
|
127
|
+
si_max: float,
|
|
128
|
+
epi_val: float,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Initialise ``Si`` and ``EPI`` in ``nd``."""
|
|
131
|
+
if override or "EPI" not in nd:
|
|
132
|
+
nd["EPI"] = epi_val
|
|
133
|
+
|
|
134
|
+
si = rng.uniform(si_min, si_max)
|
|
135
|
+
if override or "Si" not in nd:
|
|
136
|
+
nd["Si"] = si
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def init_node_attrs(G: "nx.Graph", *, override: bool = True) -> "nx.Graph":
|
|
140
|
+
"""Initialise EPI, θ, νf and Si on the nodes of ``G``.
|
|
141
|
+
|
|
142
|
+
Parameters can be customised via ``G.graph`` entries:
|
|
143
|
+
``RANDOM_SEED``, ``INIT_RANDOM_PHASE``, ``INIT_THETA_MIN/MAX``,
|
|
144
|
+
``INIT_VF_MODE``, ``VF_MIN``, ``VF_MAX``, ``INIT_VF_MIN/MAX``,
|
|
145
|
+
``INIT_VF_MEAN``, ``INIT_VF_STD`` and ``INIT_VF_CLAMP_TO_LIMITS``.
|
|
146
|
+
Ranges for ``Si`` are added via ``INIT_SI_MIN`` and ``INIT_SI_MAX``, and
|
|
147
|
+
for ``EPI`` via ``INIT_EPI_VALUE``. If ``INIT_VF_MIN`` is greater than
|
|
148
|
+
``INIT_VF_MAX``, values are swapped and clamped to ``VF_MIN``/``VF_MAX``.
|
|
149
|
+
"""
|
|
150
|
+
params = InitParams.from_graph(G)
|
|
151
|
+
|
|
152
|
+
vf_uniform_min = params.vf_uniform_min
|
|
153
|
+
vf_uniform_max = params.vf_uniform_max
|
|
154
|
+
vf_min_lim = params.vf_min_lim
|
|
155
|
+
vf_max_lim = params.vf_max_lim
|
|
156
|
+
if vf_uniform_min is None:
|
|
157
|
+
vf_uniform_min = vf_min_lim
|
|
158
|
+
if vf_uniform_max is None:
|
|
159
|
+
vf_uniform_max = vf_max_lim
|
|
160
|
+
if vf_uniform_min > vf_uniform_max:
|
|
161
|
+
vf_uniform_min, vf_uniform_max = vf_uniform_max, vf_uniform_min
|
|
162
|
+
params.vf_uniform_min = max(vf_uniform_min, vf_min_lim)
|
|
163
|
+
params.vf_uniform_max = min(vf_uniform_max, vf_max_lim)
|
|
164
|
+
|
|
165
|
+
rng = make_rng(params.seed, -1, G)
|
|
166
|
+
for _, nd in G.nodes(data=True):
|
|
167
|
+
node_attrs = cast(NodeInitAttrMap, nd)
|
|
168
|
+
|
|
169
|
+
_init_phase(
|
|
170
|
+
node_attrs,
|
|
171
|
+
rng,
|
|
172
|
+
override=override,
|
|
173
|
+
random_phase=params.init_rand_phase,
|
|
174
|
+
th_min=params.th_min,
|
|
175
|
+
th_max=params.th_max,
|
|
176
|
+
)
|
|
177
|
+
_init_vf(
|
|
178
|
+
node_attrs,
|
|
179
|
+
rng,
|
|
180
|
+
override=override,
|
|
181
|
+
mode=params.vf_mode,
|
|
182
|
+
vf_uniform_min=params.vf_uniform_min,
|
|
183
|
+
vf_uniform_max=params.vf_uniform_max,
|
|
184
|
+
vf_mean=params.vf_mean,
|
|
185
|
+
vf_std=params.vf_std,
|
|
186
|
+
vf_min_lim=params.vf_min_lim,
|
|
187
|
+
vf_max_lim=params.vf_max_lim,
|
|
188
|
+
clamp_to_limits=params.clamp_to_limits,
|
|
189
|
+
)
|
|
190
|
+
_init_si_epi(
|
|
191
|
+
node_attrs,
|
|
192
|
+
rng,
|
|
193
|
+
override=override,
|
|
194
|
+
si_min=params.si_min,
|
|
195
|
+
si_max=params.si_max,
|
|
196
|
+
epi_val=params.epi_val,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return G
|
tnfr/initialization.pyi
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
import networkx as nx
|
|
7
|
+
|
|
8
|
+
from .types import NodeInitAttrMap
|
|
9
|
+
|
|
10
|
+
__all__: tuple[str, str] = ("InitParams", "init_node_attrs")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class InitParams:
|
|
15
|
+
seed: int | None
|
|
16
|
+
init_rand_phase: bool
|
|
17
|
+
th_min: float
|
|
18
|
+
th_max: float
|
|
19
|
+
vf_mode: str
|
|
20
|
+
vf_min_lim: float
|
|
21
|
+
vf_max_lim: float
|
|
22
|
+
vf_uniform_min: float | None
|
|
23
|
+
vf_uniform_max: float | None
|
|
24
|
+
vf_mean: float
|
|
25
|
+
vf_std: float
|
|
26
|
+
clamp_to_limits: bool
|
|
27
|
+
si_min: float
|
|
28
|
+
si_max: float
|
|
29
|
+
epi_val: float
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_graph(cls, G: nx.Graph) -> InitParams: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _init_phase(
|
|
36
|
+
nd: NodeInitAttrMap,
|
|
37
|
+
rng: random.Random,
|
|
38
|
+
*,
|
|
39
|
+
override: bool,
|
|
40
|
+
random_phase: bool,
|
|
41
|
+
th_min: float,
|
|
42
|
+
th_max: float,
|
|
43
|
+
) -> None: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _init_vf(
|
|
47
|
+
nd: NodeInitAttrMap,
|
|
48
|
+
rng: random.Random,
|
|
49
|
+
*,
|
|
50
|
+
override: bool,
|
|
51
|
+
mode: str,
|
|
52
|
+
vf_uniform_min: float,
|
|
53
|
+
vf_uniform_max: float,
|
|
54
|
+
vf_mean: float,
|
|
55
|
+
vf_std: float,
|
|
56
|
+
vf_min_lim: float,
|
|
57
|
+
vf_max_lim: float,
|
|
58
|
+
clamp_to_limits: bool,
|
|
59
|
+
) -> None: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _init_si_epi(
|
|
63
|
+
nd: NodeInitAttrMap,
|
|
64
|
+
rng: random.Random,
|
|
65
|
+
*,
|
|
66
|
+
override: bool,
|
|
67
|
+
si_min: float,
|
|
68
|
+
si_max: float,
|
|
69
|
+
epi_val: float,
|
|
70
|
+
) -> None: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def init_node_attrs(G: nx.Graph, *, override: bool = True) -> nx.Graph: ...
|
tnfr/io.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Structured file I/O utilities.
|
|
2
|
+
|
|
3
|
+
Optional parsers such as ``tomllib``/``tomli`` and ``pyyaml`` are loaded via
|
|
4
|
+
the :func:`tnfr.utils.cached_import` helper. Their import results and
|
|
5
|
+
failure states are cached and can be cleared with
|
|
6
|
+
``cached_import.cache_clear()`` and :func:`tnfr.utils.prune_failed_imports`
|
|
7
|
+
when needed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Callable
|
|
17
|
+
from functools import partial
|
|
18
|
+
|
|
19
|
+
from .utils import LazyImportProxy, cached_import, get_logger
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _raise_import_error(name: str, *_: Any, **__: Any) -> Any:
|
|
23
|
+
raise ImportError(f"{name} is not installed")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_MISSING_TOML_ERROR = type(
|
|
27
|
+
"MissingTOMLDependencyError",
|
|
28
|
+
(Exception,),
|
|
29
|
+
{"__doc__": "Fallback error used when tomllib/tomli is missing."},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_MISSING_YAML_ERROR = type(
|
|
33
|
+
"MissingPyYAMLDependencyError",
|
|
34
|
+
(Exception,),
|
|
35
|
+
{"__doc__": "Fallback error used when pyyaml is missing."},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_lazy(value: Any) -> Any:
|
|
40
|
+
if isinstance(value, LazyImportProxy):
|
|
41
|
+
return value.resolve()
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _LazyBool:
|
|
46
|
+
__slots__ = ("_value",)
|
|
47
|
+
|
|
48
|
+
def __init__(self, value: Any) -> None:
|
|
49
|
+
self._value = value
|
|
50
|
+
|
|
51
|
+
def __bool__(self) -> bool:
|
|
52
|
+
return _resolve_lazy(self._value) is not None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_TOMLI_MODULE = cached_import("tomli", emit="log", lazy=True)
|
|
56
|
+
tomllib = cached_import(
|
|
57
|
+
"tomllib",
|
|
58
|
+
emit="log",
|
|
59
|
+
lazy=True,
|
|
60
|
+
fallback=_TOMLI_MODULE,
|
|
61
|
+
)
|
|
62
|
+
has_toml = _LazyBool(tomllib)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_TOMLI_TOML_ERROR = cached_import(
|
|
66
|
+
"tomli",
|
|
67
|
+
"TOMLDecodeError",
|
|
68
|
+
emit="log",
|
|
69
|
+
lazy=True,
|
|
70
|
+
fallback=_MISSING_TOML_ERROR,
|
|
71
|
+
)
|
|
72
|
+
TOMLDecodeError = cached_import(
|
|
73
|
+
"tomllib",
|
|
74
|
+
"TOMLDecodeError",
|
|
75
|
+
emit="log",
|
|
76
|
+
lazy=True,
|
|
77
|
+
fallback=_TOMLI_TOML_ERROR,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_TOMLI_LOADS = cached_import(
|
|
82
|
+
"tomli",
|
|
83
|
+
"loads",
|
|
84
|
+
emit="log",
|
|
85
|
+
lazy=True,
|
|
86
|
+
fallback=partial(_raise_import_error, "tomllib/tomli"),
|
|
87
|
+
)
|
|
88
|
+
_TOML_LOADS: Callable[[str], Any] = cached_import(
|
|
89
|
+
"tomllib",
|
|
90
|
+
"loads",
|
|
91
|
+
emit="log",
|
|
92
|
+
lazy=True,
|
|
93
|
+
fallback=_TOMLI_LOADS,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
yaml = cached_import("yaml", emit="log", lazy=True)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
YAMLError = cached_import(
|
|
101
|
+
"yaml",
|
|
102
|
+
"YAMLError",
|
|
103
|
+
emit="log",
|
|
104
|
+
lazy=True,
|
|
105
|
+
fallback=_MISSING_YAML_ERROR,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_YAML_SAFE_LOAD: Callable[[str], Any] = cached_import(
|
|
110
|
+
"yaml",
|
|
111
|
+
"safe_load",
|
|
112
|
+
emit="log",
|
|
113
|
+
lazy=True,
|
|
114
|
+
fallback=partial(_raise_import_error, "pyyaml"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _parse_yaml(text: str) -> Any:
|
|
119
|
+
"""Parse YAML ``text`` using ``safe_load`` if available."""
|
|
120
|
+
return _YAML_SAFE_LOAD(text)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _parse_toml(text: str) -> Any:
|
|
124
|
+
"""Parse TOML ``text`` using ``tomllib`` or ``tomli``."""
|
|
125
|
+
return _TOML_LOADS(text)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
PARSERS = {
|
|
129
|
+
".json": json.loads,
|
|
130
|
+
".yaml": _parse_yaml,
|
|
131
|
+
".yml": _parse_yaml,
|
|
132
|
+
".toml": _parse_toml,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_parser(suffix: str) -> Callable[[str], Any]:
|
|
137
|
+
try:
|
|
138
|
+
return PARSERS[suffix]
|
|
139
|
+
except KeyError as exc:
|
|
140
|
+
raise ValueError(f"Unsupported suffix: {suffix}") from exc
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_BASE_ERROR_MESSAGES: dict[type[BaseException], str] = {
|
|
144
|
+
OSError: "Could not read {path}: {e}",
|
|
145
|
+
UnicodeDecodeError: "Encoding error while reading {path}: {e}",
|
|
146
|
+
json.JSONDecodeError: "Error parsing JSON file at {path}: {e}",
|
|
147
|
+
ImportError: "Missing dependency parsing {path}: {e}",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _resolve_exception_type(candidate: Any) -> type[BaseException] | None:
|
|
152
|
+
resolved = _resolve_lazy(candidate)
|
|
153
|
+
if isinstance(resolved, type) and issubclass(resolved, BaseException):
|
|
154
|
+
return resolved
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
_OPTIONAL_ERROR_MESSAGE_FACTORIES: tuple[
|
|
159
|
+
tuple[Callable[[], type[BaseException] | None], str],
|
|
160
|
+
...,
|
|
161
|
+
] = (
|
|
162
|
+
(lambda: _resolve_exception_type(YAMLError), "Error parsing YAML file at {path}: {e}"),
|
|
163
|
+
(lambda: _resolve_exception_type(TOMLDecodeError), "Error parsing TOML file at {path}: {e}"),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
_BASE_STRUCTURED_EXCEPTIONS = (
|
|
168
|
+
OSError,
|
|
169
|
+
UnicodeDecodeError,
|
|
170
|
+
json.JSONDecodeError,
|
|
171
|
+
ImportError,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _iter_optional_exceptions() -> list[type[BaseException]]:
|
|
176
|
+
errors: list[type[BaseException]] = []
|
|
177
|
+
for resolver, _ in _OPTIONAL_ERROR_MESSAGE_FACTORIES:
|
|
178
|
+
exc_type = resolver()
|
|
179
|
+
if exc_type is not None:
|
|
180
|
+
errors.append(exc_type)
|
|
181
|
+
return errors
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _is_structured_error(exc: Exception) -> bool:
|
|
185
|
+
if isinstance(exc, _BASE_STRUCTURED_EXCEPTIONS):
|
|
186
|
+
return True
|
|
187
|
+
for optional_exc in _iter_optional_exceptions():
|
|
188
|
+
if isinstance(exc, optional_exc):
|
|
189
|
+
return True
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _format_structured_file_error(path: Path, e: Exception) -> str:
|
|
194
|
+
for exc, msg in _BASE_ERROR_MESSAGES.items():
|
|
195
|
+
if isinstance(e, exc):
|
|
196
|
+
return msg.format(path=path, e=e)
|
|
197
|
+
|
|
198
|
+
for resolver, msg in _OPTIONAL_ERROR_MESSAGE_FACTORIES:
|
|
199
|
+
exc_type = resolver()
|
|
200
|
+
if exc_type is not None and isinstance(e, exc_type):
|
|
201
|
+
return msg.format(path=path, e=e)
|
|
202
|
+
|
|
203
|
+
return f"Error parsing {path}: {e}"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class StructuredFileError(Exception):
|
|
207
|
+
"""Error while reading or parsing a structured file."""
|
|
208
|
+
|
|
209
|
+
def __init__(self, path: Path, original: Exception) -> None:
|
|
210
|
+
super().__init__(_format_structured_file_error(path, original))
|
|
211
|
+
self.path = path
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def read_structured_file(path: Path) -> Any:
|
|
215
|
+
"""Read a JSON, YAML or TOML file and return parsed data."""
|
|
216
|
+
suffix = path.suffix.lower()
|
|
217
|
+
try:
|
|
218
|
+
parser = _get_parser(suffix)
|
|
219
|
+
except ValueError as e:
|
|
220
|
+
raise StructuredFileError(path, e) from e
|
|
221
|
+
try:
|
|
222
|
+
text = path.read_text(encoding="utf-8")
|
|
223
|
+
return parser(text)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
if _is_structured_error(e):
|
|
226
|
+
raise StructuredFileError(path, e) from e
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
logger = get_logger(__name__)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def safe_write(
|
|
234
|
+
path: str | Path,
|
|
235
|
+
write: Callable[[Any], Any],
|
|
236
|
+
*,
|
|
237
|
+
mode: str = "w",
|
|
238
|
+
encoding: str | None = "utf-8",
|
|
239
|
+
atomic: bool = True,
|
|
240
|
+
sync: bool | None = None,
|
|
241
|
+
**open_kwargs: Any,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Write to ``path`` ensuring parent directory exists and handle errors.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
path:
|
|
248
|
+
Destination file path.
|
|
249
|
+
write:
|
|
250
|
+
Callback receiving the opened file object and performing the actual
|
|
251
|
+
write.
|
|
252
|
+
mode:
|
|
253
|
+
File mode passed to :func:`open`. Text modes (default) use UTF-8
|
|
254
|
+
encoding unless ``encoding`` is ``None``. When a binary mode is used
|
|
255
|
+
(``'b'`` in ``mode``) no encoding parameter is supplied so
|
|
256
|
+
``write`` may write bytes.
|
|
257
|
+
encoding:
|
|
258
|
+
Encoding for text modes. Ignored for binary modes.
|
|
259
|
+
atomic:
|
|
260
|
+
When ``True`` (default) writes to a temporary file and atomically
|
|
261
|
+
replaces the destination after flushing to disk. When ``False``
|
|
262
|
+
writes directly to ``path`` without any atomicity guarantee.
|
|
263
|
+
sync:
|
|
264
|
+
When ``True`` flushes and fsyncs the file descriptor after writing.
|
|
265
|
+
``None`` uses ``atomic`` to determine syncing behaviour.
|
|
266
|
+
"""
|
|
267
|
+
path = Path(path)
|
|
268
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
open_params = dict(mode=mode, **open_kwargs)
|
|
270
|
+
if "b" not in mode and encoding is not None:
|
|
271
|
+
open_params["encoding"] = encoding
|
|
272
|
+
if sync is None:
|
|
273
|
+
sync = atomic
|
|
274
|
+
tmp_path: Path | None = None
|
|
275
|
+
try:
|
|
276
|
+
if atomic:
|
|
277
|
+
tmp_fd = tempfile.NamedTemporaryFile(dir=path.parent, delete=False)
|
|
278
|
+
tmp_path = Path(tmp_fd.name)
|
|
279
|
+
tmp_fd.close()
|
|
280
|
+
with open(tmp_path, **open_params) as fd:
|
|
281
|
+
write(fd)
|
|
282
|
+
if sync:
|
|
283
|
+
fd.flush()
|
|
284
|
+
os.fsync(fd.fileno())
|
|
285
|
+
try:
|
|
286
|
+
os.replace(tmp_path, path)
|
|
287
|
+
except OSError as e:
|
|
288
|
+
logger.error(
|
|
289
|
+
"Atomic replace failed for %s -> %s: %s", tmp_path, path, e
|
|
290
|
+
)
|
|
291
|
+
raise
|
|
292
|
+
else:
|
|
293
|
+
with open(path, **open_params) as fd:
|
|
294
|
+
write(fd)
|
|
295
|
+
if sync:
|
|
296
|
+
fd.flush()
|
|
297
|
+
os.fsync(fd.fileno())
|
|
298
|
+
except (OSError, ValueError, TypeError) as e:
|
|
299
|
+
raise type(e)(f"Failed to write file {path}: {e}") from e
|
|
300
|
+
finally:
|
|
301
|
+
if tmp_path is not None:
|
|
302
|
+
tmp_path.unlink(missing_ok=True)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
__all__ = (
|
|
306
|
+
"read_structured_file",
|
|
307
|
+
"safe_write",
|
|
308
|
+
"StructuredFileError",
|
|
309
|
+
"TOMLDecodeError",
|
|
310
|
+
"YAMLError",
|
|
311
|
+
)
|
tnfr/io.pyi
ADDED
tnfr/locking.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Utilities for named locks.
|
|
2
|
+
|
|
3
|
+
This module provides helpers to obtain process-wide ``threading.Lock``
|
|
4
|
+
instances identified by name. Locks are created lazily and reused,
|
|
5
|
+
allowing different modules to synchronise on shared resources without
|
|
6
|
+
redefining locks repeatedly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
from weakref import WeakValueDictionary
|
|
13
|
+
|
|
14
|
+
# Registry of locks by name guarded by ``_REGISTRY_LOCK``.
|
|
15
|
+
# Using ``WeakValueDictionary`` ensures that once a lock is no longer
|
|
16
|
+
# referenced elsewhere, it is removed from the registry automatically,
|
|
17
|
+
# keeping the catalogue aligned with active coherence nodes.
|
|
18
|
+
_locks: WeakValueDictionary[str, threading.Lock] = WeakValueDictionary()
|
|
19
|
+
_REGISTRY_LOCK = threading.Lock()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_lock(name: str) -> threading.Lock:
|
|
23
|
+
"""Return a re-usable lock identified by ``name``.
|
|
24
|
+
|
|
25
|
+
The same lock object is returned for identical names. Locks are
|
|
26
|
+
created on first use and stored in a process-wide registry.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
with _REGISTRY_LOCK:
|
|
30
|
+
lock = _locks.get(name)
|
|
31
|
+
if lock is None:
|
|
32
|
+
lock = threading.Lock()
|
|
33
|
+
_locks[name] = lock
|
|
34
|
+
return lock
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["get_lock"]
|
tnfr/locking.pyi
ADDED
tnfr/metrics/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Registerable metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .core import register_metrics_callbacks
|
|
6
|
+
from .reporting import (
|
|
7
|
+
Tg_global,
|
|
8
|
+
Tg_by_node,
|
|
9
|
+
latency_series,
|
|
10
|
+
glyphogram_series,
|
|
11
|
+
glyph_top,
|
|
12
|
+
build_metrics_summary,
|
|
13
|
+
)
|
|
14
|
+
from .coherence import (
|
|
15
|
+
coherence_matrix,
|
|
16
|
+
local_phase_sync,
|
|
17
|
+
local_phase_sync_weighted,
|
|
18
|
+
register_coherence_callbacks,
|
|
19
|
+
)
|
|
20
|
+
from .diagnosis import (
|
|
21
|
+
register_diagnosis_callbacks,
|
|
22
|
+
dissonance_events,
|
|
23
|
+
)
|
|
24
|
+
from .export import export_metrics
|
|
25
|
+
|
|
26
|
+
__all__ = (
|
|
27
|
+
"register_metrics_callbacks",
|
|
28
|
+
"Tg_global",
|
|
29
|
+
"Tg_by_node",
|
|
30
|
+
"latency_series",
|
|
31
|
+
"glyphogram_series",
|
|
32
|
+
"glyph_top",
|
|
33
|
+
"build_metrics_summary",
|
|
34
|
+
"coherence_matrix",
|
|
35
|
+
"local_phase_sync",
|
|
36
|
+
"local_phase_sync_weighted",
|
|
37
|
+
"register_coherence_callbacks",
|
|
38
|
+
"register_diagnosis_callbacks",
|
|
39
|
+
"dissonance_events",
|
|
40
|
+
"export_metrics",
|
|
41
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
__all__: Any
|
|
4
|
+
|
|
5
|
+
def __getattr__(name: str) -> Any: ...
|
|
6
|
+
|
|
7
|
+
Tg_by_node: Any
|
|
8
|
+
Tg_global: Any
|
|
9
|
+
build_metrics_summary: Any
|
|
10
|
+
coherence_matrix: Any
|
|
11
|
+
dissonance_events: Any
|
|
12
|
+
export_metrics: Any
|
|
13
|
+
glyph_top: Any
|
|
14
|
+
glyphogram_series: Any
|
|
15
|
+
latency_series: Any
|
|
16
|
+
local_phase_sync: Any
|
|
17
|
+
local_phase_sync_weighted: Any
|
|
18
|
+
register_coherence_callbacks: Any
|
|
19
|
+
register_diagnosis_callbacks: Any
|
|
20
|
+
register_metrics_callbacks: Any
|