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
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""Glyph selection helpers for TNFR dynamics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
9
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from operator import itemgetter
|
|
12
|
+
from typing import Any, cast
|
|
13
|
+
|
|
14
|
+
from ..alias import collect_attr, get_attr
|
|
15
|
+
from ..constants import get_graph_param, get_param
|
|
16
|
+
from ..glyph_history import ensure_history, recent_glyph
|
|
17
|
+
from ..helpers.numeric import clamp01
|
|
18
|
+
from ..metrics.common import compute_dnfr_accel_max, merge_and_normalize_weights
|
|
19
|
+
from ..operators import apply_glyph
|
|
20
|
+
from ..selector import (
|
|
21
|
+
_apply_selector_hysteresis,
|
|
22
|
+
_calc_selector_score,
|
|
23
|
+
_selector_norms,
|
|
24
|
+
_selector_thresholds,
|
|
25
|
+
)
|
|
26
|
+
from ..types import Glyph, GlyphSelector, HistoryState, NodeId, TNFRGraph
|
|
27
|
+
from ..utils import get_numpy
|
|
28
|
+
from ..validation.grammar import enforce_canonical_grammar, on_applied_glyph
|
|
29
|
+
from .aliases import ALIAS_D2EPI, ALIAS_DNFR, ALIAS_DSI, ALIAS_SI
|
|
30
|
+
from .._compat import TypeAlias
|
|
31
|
+
|
|
32
|
+
GlyphCode: TypeAlias = Glyph | str
|
|
33
|
+
|
|
34
|
+
__all__ = (
|
|
35
|
+
"GlyphCode",
|
|
36
|
+
"AbstractSelector",
|
|
37
|
+
"DefaultGlyphSelector",
|
|
38
|
+
"ParametricGlyphSelector",
|
|
39
|
+
"default_glyph_selector",
|
|
40
|
+
"parametric_glyph_selector",
|
|
41
|
+
"_SelectorPreselection",
|
|
42
|
+
"_configure_selector_weights",
|
|
43
|
+
"_apply_selector",
|
|
44
|
+
"_apply_glyphs",
|
|
45
|
+
"_selector_parallel_jobs",
|
|
46
|
+
"_prepare_selector_preselection",
|
|
47
|
+
"_resolve_preselected_glyph",
|
|
48
|
+
"_choose_glyph",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AbstractSelector(ABC):
|
|
53
|
+
"""Interface describing glyph selector lifecycle hooks."""
|
|
54
|
+
|
|
55
|
+
def prepare(
|
|
56
|
+
self, graph: TNFRGraph, nodes: Sequence[NodeId]
|
|
57
|
+
) -> None: # pragma: no cover - default no-op
|
|
58
|
+
"""Prepare selector state before evaluating a glyph batch."""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def select(self, graph: TNFRGraph, node: NodeId) -> GlyphCode:
|
|
62
|
+
"""Return the glyph to apply for ``node`` within ``graph``."""
|
|
63
|
+
|
|
64
|
+
def __call__(self, graph: TNFRGraph, node: NodeId) -> GlyphCode:
|
|
65
|
+
"""Allow selectors to be used as legacy callables."""
|
|
66
|
+
|
|
67
|
+
return self.select(graph, node)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _default_selector_logic(G: TNFRGraph, n: NodeId) -> GlyphCode:
|
|
71
|
+
nd = G.nodes[n]
|
|
72
|
+
thr = _selector_thresholds(G)
|
|
73
|
+
hi, lo, dnfr_hi = itemgetter("si_hi", "si_lo", "dnfr_hi")(thr)
|
|
74
|
+
|
|
75
|
+
norms = G.graph.get("_sel_norms")
|
|
76
|
+
if norms is None:
|
|
77
|
+
norms = compute_dnfr_accel_max(G)
|
|
78
|
+
G.graph["_sel_norms"] = norms
|
|
79
|
+
dnfr_max = float(norms.get("dnfr_max", 1.0)) or 1.0
|
|
80
|
+
|
|
81
|
+
Si = clamp01(get_attr(nd, ALIAS_SI, 0.5))
|
|
82
|
+
dnfr = abs(get_attr(nd, ALIAS_DNFR, 0.0)) / dnfr_max
|
|
83
|
+
|
|
84
|
+
if Si >= hi:
|
|
85
|
+
return "IL"
|
|
86
|
+
if Si <= lo:
|
|
87
|
+
return "OZ" if dnfr > dnfr_hi else "ZHIR"
|
|
88
|
+
return "NAV" if dnfr > dnfr_hi else "RA"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _soft_grammar_prefilter(
|
|
92
|
+
G: TNFRGraph,
|
|
93
|
+
n: NodeId,
|
|
94
|
+
cand: GlyphCode,
|
|
95
|
+
dnfr: float,
|
|
96
|
+
accel: float,
|
|
97
|
+
) -> GlyphCode:
|
|
98
|
+
"""Soft grammar: avoid repetitions before the canonical one."""
|
|
99
|
+
|
|
100
|
+
gram = get_graph_param(G, "GRAMMAR", dict)
|
|
101
|
+
gwin = int(gram.get("window", 3))
|
|
102
|
+
avoid = {str(item) for item in gram.get("avoid_repeats", [])}
|
|
103
|
+
force_dn = float(gram.get("force_dnfr", 0.60))
|
|
104
|
+
force_ac = float(gram.get("force_accel", 0.60))
|
|
105
|
+
fallbacks = cast(Mapping[str, GlyphCode], gram.get("fallbacks", {}))
|
|
106
|
+
nd = G.nodes[n]
|
|
107
|
+
cand_key = str(cand)
|
|
108
|
+
if cand_key in avoid and recent_glyph(nd, cand_key, gwin):
|
|
109
|
+
if not (dnfr >= force_dn or accel >= force_ac):
|
|
110
|
+
cand = fallbacks.get(cand_key, cand)
|
|
111
|
+
return cand
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _selector_normalized_metrics(
|
|
115
|
+
nd: Mapping[str, Any], norms: Mapping[str, float]
|
|
116
|
+
) -> tuple[float, float, float]:
|
|
117
|
+
dnfr_max = float(norms.get("dnfr_max", 1.0)) or 1.0
|
|
118
|
+
acc_max = float(norms.get("accel_max", 1.0)) or 1.0
|
|
119
|
+
Si = clamp01(get_attr(nd, ALIAS_SI, 0.5))
|
|
120
|
+
dnfr = abs(get_attr(nd, ALIAS_DNFR, 0.0)) / dnfr_max
|
|
121
|
+
accel = abs(get_attr(nd, ALIAS_D2EPI, 0.0)) / acc_max
|
|
122
|
+
return Si, dnfr, accel
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _selector_base_choice(
|
|
126
|
+
Si: float, dnfr: float, accel: float, thr: Mapping[str, float]
|
|
127
|
+
) -> GlyphCode:
|
|
128
|
+
si_hi, si_lo, dnfr_hi, acc_hi = itemgetter(
|
|
129
|
+
"si_hi", "si_lo", "dnfr_hi", "accel_hi"
|
|
130
|
+
)(thr)
|
|
131
|
+
if Si >= si_hi:
|
|
132
|
+
return "IL"
|
|
133
|
+
if Si <= si_lo:
|
|
134
|
+
if accel >= acc_hi:
|
|
135
|
+
return "THOL"
|
|
136
|
+
return "OZ" if dnfr >= dnfr_hi else "ZHIR"
|
|
137
|
+
if dnfr >= dnfr_hi or accel >= acc_hi:
|
|
138
|
+
return "NAV"
|
|
139
|
+
return "RA"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _configure_selector_weights(G: TNFRGraph) -> Mapping[str, float]:
|
|
143
|
+
weights = merge_and_normalize_weights(
|
|
144
|
+
G, "SELECTOR_WEIGHTS", ("w_si", "w_dnfr", "w_accel")
|
|
145
|
+
)
|
|
146
|
+
cast_weights = cast(Mapping[str, float], weights)
|
|
147
|
+
G.graph["_selector_weights"] = cast_weights
|
|
148
|
+
return cast_weights
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _compute_selector_score(
|
|
152
|
+
G: TNFRGraph,
|
|
153
|
+
nd: Mapping[str, Any],
|
|
154
|
+
Si: float,
|
|
155
|
+
dnfr: float,
|
|
156
|
+
accel: float,
|
|
157
|
+
cand: GlyphCode,
|
|
158
|
+
) -> float:
|
|
159
|
+
W = G.graph.get("_selector_weights")
|
|
160
|
+
if W is None:
|
|
161
|
+
W = _configure_selector_weights(G)
|
|
162
|
+
score = _calc_selector_score(Si, dnfr, accel, cast(Mapping[str, float], W))
|
|
163
|
+
hist_prev = nd.get("glyph_history")
|
|
164
|
+
if hist_prev and hist_prev[-1] == cand:
|
|
165
|
+
delta_si = get_attr(nd, ALIAS_DSI, 0.0)
|
|
166
|
+
h = ensure_history(G)
|
|
167
|
+
sig = h.get("sense_sigma_mag", [])
|
|
168
|
+
delta_sigma = sig[-1] - sig[-2] if len(sig) >= 2 else 0.0
|
|
169
|
+
if delta_si <= 0.0 and delta_sigma <= 0.0:
|
|
170
|
+
score -= 0.05
|
|
171
|
+
return float(score)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _apply_score_override(
|
|
175
|
+
cand: GlyphCode, score: float, dnfr: float, dnfr_lo: float
|
|
176
|
+
) -> GlyphCode:
|
|
177
|
+
cand_key = str(cand)
|
|
178
|
+
if score >= 0.66 and cand_key in ("NAV", "RA", "ZHIR", "OZ"):
|
|
179
|
+
return "IL"
|
|
180
|
+
if score <= 0.33 and cand_key in ("NAV", "RA", "IL"):
|
|
181
|
+
return "OZ" if dnfr >= dnfr_lo else "ZHIR"
|
|
182
|
+
return cand
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _parametric_selector_logic(G: TNFRGraph, n: NodeId) -> GlyphCode:
|
|
186
|
+
nd = G.nodes[n]
|
|
187
|
+
thr = _selector_thresholds(G)
|
|
188
|
+
margin: float | None = get_graph_param(G, "GLYPH_SELECTOR_MARGIN")
|
|
189
|
+
|
|
190
|
+
norms = cast(Mapping[str, float] | None, G.graph.get("_sel_norms"))
|
|
191
|
+
if norms is None:
|
|
192
|
+
norms = _selector_norms(G)
|
|
193
|
+
Si, dnfr, accel = _selector_normalized_metrics(nd, norms)
|
|
194
|
+
|
|
195
|
+
cand = _selector_base_choice(Si, dnfr, accel, thr)
|
|
196
|
+
|
|
197
|
+
hist_cand = _apply_selector_hysteresis(nd, Si, dnfr, accel, thr, margin)
|
|
198
|
+
if hist_cand is not None:
|
|
199
|
+
return hist_cand
|
|
200
|
+
|
|
201
|
+
score = _compute_selector_score(G, nd, Si, dnfr, accel, cand)
|
|
202
|
+
|
|
203
|
+
cand = _apply_score_override(cand, score, dnfr, thr["dnfr_lo"])
|
|
204
|
+
|
|
205
|
+
return _soft_grammar_prefilter(G, n, cand, dnfr, accel)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(slots=True)
|
|
209
|
+
class _SelectorPreselection:
|
|
210
|
+
kind: str
|
|
211
|
+
metrics: Mapping[Any, tuple[float, float, float]]
|
|
212
|
+
base_choices: Mapping[Any, GlyphCode]
|
|
213
|
+
thresholds: Mapping[str, float] | None = None
|
|
214
|
+
margin: float | None = None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _build_default_preselection(
|
|
218
|
+
G: TNFRGraph, nodes: Sequence[NodeId]
|
|
219
|
+
) -> _SelectorPreselection:
|
|
220
|
+
node_list = list(nodes)
|
|
221
|
+
thresholds = _selector_thresholds(G)
|
|
222
|
+
if not node_list:
|
|
223
|
+
return _SelectorPreselection("default", {}, {}, thresholds=thresholds)
|
|
224
|
+
|
|
225
|
+
norms = G.graph.get("_sel_norms") or _selector_norms(G)
|
|
226
|
+
n_jobs = _selector_parallel_jobs(G)
|
|
227
|
+
metrics = _collect_selector_metrics(G, node_list, norms, n_jobs=n_jobs)
|
|
228
|
+
base_choices = _compute_default_base_choices(metrics, thresholds)
|
|
229
|
+
return _SelectorPreselection(
|
|
230
|
+
"default", metrics, base_choices, thresholds=thresholds
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _build_param_preselection(
|
|
235
|
+
G: TNFRGraph, nodes: Sequence[NodeId]
|
|
236
|
+
) -> _SelectorPreselection:
|
|
237
|
+
node_list = list(nodes)
|
|
238
|
+
thresholds = _selector_thresholds(G)
|
|
239
|
+
margin: float | None = get_graph_param(G, "GLYPH_SELECTOR_MARGIN")
|
|
240
|
+
if not node_list:
|
|
241
|
+
return _SelectorPreselection(
|
|
242
|
+
"param", {}, {}, thresholds=thresholds, margin=margin
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
norms = G.graph.get("_sel_norms") or _selector_norms(G)
|
|
246
|
+
n_jobs = _selector_parallel_jobs(G)
|
|
247
|
+
metrics = _collect_selector_metrics(G, node_list, norms, n_jobs=n_jobs)
|
|
248
|
+
base_choices = _compute_param_base_choices(metrics, thresholds, n_jobs)
|
|
249
|
+
return _SelectorPreselection(
|
|
250
|
+
"param",
|
|
251
|
+
metrics,
|
|
252
|
+
base_choices,
|
|
253
|
+
thresholds=thresholds,
|
|
254
|
+
margin=margin,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class DefaultGlyphSelector(AbstractSelector):
|
|
259
|
+
"""Selector implementing the legacy default glyph heuristic."""
|
|
260
|
+
|
|
261
|
+
__slots__ = ("_preselection", "_prepared_graph_id")
|
|
262
|
+
|
|
263
|
+
def __init__(self) -> None:
|
|
264
|
+
self._preselection: _SelectorPreselection | None = None
|
|
265
|
+
self._prepared_graph_id: int | None = None
|
|
266
|
+
|
|
267
|
+
def prepare(self, graph: TNFRGraph, nodes: Sequence[NodeId]) -> None:
|
|
268
|
+
self._preselection = _build_default_preselection(graph, nodes)
|
|
269
|
+
self._prepared_graph_id = id(graph)
|
|
270
|
+
|
|
271
|
+
def select(self, graph: TNFRGraph, node: NodeId) -> GlyphCode:
|
|
272
|
+
if self._prepared_graph_id == id(graph):
|
|
273
|
+
preselection = self._preselection
|
|
274
|
+
else:
|
|
275
|
+
preselection = None
|
|
276
|
+
return _resolve_preselected_glyph(
|
|
277
|
+
graph, node, _default_selector_logic, preselection
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ParametricGlyphSelector(AbstractSelector):
|
|
282
|
+
"""Selector exposing the parametric scoring pipeline."""
|
|
283
|
+
|
|
284
|
+
__slots__ = ("_preselection", "_prepared_graph_id")
|
|
285
|
+
|
|
286
|
+
def __init__(self) -> None:
|
|
287
|
+
self._preselection: _SelectorPreselection | None = None
|
|
288
|
+
self._prepared_graph_id: int | None = None
|
|
289
|
+
|
|
290
|
+
def prepare(self, graph: TNFRGraph, nodes: Sequence[NodeId]) -> None:
|
|
291
|
+
_selector_norms(graph)
|
|
292
|
+
_configure_selector_weights(graph)
|
|
293
|
+
self._preselection = _build_param_preselection(graph, nodes)
|
|
294
|
+
self._prepared_graph_id = id(graph)
|
|
295
|
+
|
|
296
|
+
def select(self, graph: TNFRGraph, node: NodeId) -> GlyphCode:
|
|
297
|
+
if self._prepared_graph_id == id(graph):
|
|
298
|
+
preselection = self._preselection
|
|
299
|
+
else:
|
|
300
|
+
preselection = None
|
|
301
|
+
return _resolve_preselected_glyph(
|
|
302
|
+
graph, node, _parametric_selector_logic, preselection
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
default_glyph_selector = DefaultGlyphSelector()
|
|
307
|
+
parametric_glyph_selector = ParametricGlyphSelector()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _choose_glyph(
|
|
311
|
+
G: TNFRGraph,
|
|
312
|
+
n: NodeId,
|
|
313
|
+
selector: GlyphSelector,
|
|
314
|
+
use_canon: bool,
|
|
315
|
+
h_al: MutableMapping[Any, int],
|
|
316
|
+
h_en: MutableMapping[Any, int],
|
|
317
|
+
al_max: int,
|
|
318
|
+
en_max: int,
|
|
319
|
+
) -> GlyphCode:
|
|
320
|
+
if h_al[n] > al_max:
|
|
321
|
+
return Glyph.AL
|
|
322
|
+
if h_en[n] > en_max:
|
|
323
|
+
return Glyph.EN
|
|
324
|
+
g = selector(G, n)
|
|
325
|
+
if use_canon:
|
|
326
|
+
g = enforce_canonical_grammar(G, n, g)
|
|
327
|
+
return g
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _selector_parallel_jobs(G: TNFRGraph) -> int | None:
|
|
331
|
+
raw_jobs = G.graph.get("GLYPH_SELECTOR_N_JOBS")
|
|
332
|
+
try:
|
|
333
|
+
n_jobs = None if raw_jobs is None else int(raw_jobs)
|
|
334
|
+
except (TypeError, ValueError):
|
|
335
|
+
return None
|
|
336
|
+
if n_jobs is None or n_jobs <= 1:
|
|
337
|
+
return None
|
|
338
|
+
return n_jobs
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _selector_metrics_chunk(
|
|
342
|
+
args: tuple[list[float], list[float], list[float], float, float]
|
|
343
|
+
) -> tuple[list[float], list[float], list[float]]:
|
|
344
|
+
si_values, dnfr_values, accel_values, dnfr_max, accel_max = args
|
|
345
|
+
si_seq = [clamp01(float(v)) for v in si_values]
|
|
346
|
+
dnfr_seq = [abs(float(v)) / dnfr_max for v in dnfr_values]
|
|
347
|
+
accel_seq = [abs(float(v)) / accel_max for v in accel_values]
|
|
348
|
+
return si_seq, dnfr_seq, accel_seq
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _collect_selector_metrics(
|
|
352
|
+
G: TNFRGraph,
|
|
353
|
+
nodes: list[Any],
|
|
354
|
+
norms: Mapping[str, float],
|
|
355
|
+
n_jobs: int | None = None,
|
|
356
|
+
) -> dict[Any, tuple[float, float, float]]:
|
|
357
|
+
if not nodes:
|
|
358
|
+
return {}
|
|
359
|
+
|
|
360
|
+
dynamics_module = sys.modules.get("tnfr.dynamics")
|
|
361
|
+
get_numpy_fn = get_numpy
|
|
362
|
+
if dynamics_module is not None:
|
|
363
|
+
get_numpy_fn = getattr(dynamics_module, "get_numpy", get_numpy)
|
|
364
|
+
|
|
365
|
+
np_mod = get_numpy_fn()
|
|
366
|
+
dnfr_max = float(norms.get("dnfr_max", 1.0)) or 1.0
|
|
367
|
+
accel_max = float(norms.get("accel_max", 1.0)) or 1.0
|
|
368
|
+
|
|
369
|
+
if np_mod is not None:
|
|
370
|
+
si_seq_np = collect_attr(G, nodes, ALIAS_SI, 0.5, np=np_mod).astype(float)
|
|
371
|
+
si_seq_np = np_mod.clip(si_seq_np, 0.0, 1.0)
|
|
372
|
+
dnfr_seq_np = np_mod.abs(
|
|
373
|
+
collect_attr(G, nodes, ALIAS_DNFR, 0.0, np=np_mod).astype(float)
|
|
374
|
+
) / dnfr_max
|
|
375
|
+
accel_seq_np = np_mod.abs(
|
|
376
|
+
collect_attr(G, nodes, ALIAS_D2EPI, 0.0, np=np_mod).astype(float)
|
|
377
|
+
) / accel_max
|
|
378
|
+
|
|
379
|
+
si_seq = si_seq_np.tolist()
|
|
380
|
+
dnfr_seq = dnfr_seq_np.tolist()
|
|
381
|
+
accel_seq = accel_seq_np.tolist()
|
|
382
|
+
else:
|
|
383
|
+
si_values = collect_attr(G, nodes, ALIAS_SI, 0.5)
|
|
384
|
+
dnfr_values = collect_attr(G, nodes, ALIAS_DNFR, 0.0)
|
|
385
|
+
accel_values = collect_attr(G, nodes, ALIAS_D2EPI, 0.0)
|
|
386
|
+
|
|
387
|
+
worker_count = n_jobs if n_jobs is not None and n_jobs > 1 else None
|
|
388
|
+
if worker_count is None:
|
|
389
|
+
si_seq = [clamp01(float(v)) for v in si_values]
|
|
390
|
+
dnfr_seq = [abs(float(v)) / dnfr_max for v in dnfr_values]
|
|
391
|
+
accel_seq = [abs(float(v)) / accel_max for v in accel_values]
|
|
392
|
+
else:
|
|
393
|
+
chunk_size = max(1, math.ceil(len(nodes) / worker_count))
|
|
394
|
+
chunk_bounds = [
|
|
395
|
+
(start, min(start + chunk_size, len(nodes)))
|
|
396
|
+
for start in range(0, len(nodes), chunk_size)
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
si_seq = []
|
|
400
|
+
dnfr_seq = []
|
|
401
|
+
accel_seq = []
|
|
402
|
+
|
|
403
|
+
def _args_iter() -> Sequence[tuple[list[float], list[float], list[float], float, float]]:
|
|
404
|
+
for start, end in chunk_bounds:
|
|
405
|
+
yield (
|
|
406
|
+
si_values[start:end],
|
|
407
|
+
dnfr_values[start:end],
|
|
408
|
+
accel_values[start:end],
|
|
409
|
+
dnfr_max,
|
|
410
|
+
accel_max,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
executor_cls = ProcessPoolExecutor
|
|
414
|
+
if dynamics_module is not None:
|
|
415
|
+
executor_cls = getattr(
|
|
416
|
+
dynamics_module, "ProcessPoolExecutor", ProcessPoolExecutor
|
|
417
|
+
)
|
|
418
|
+
with executor_cls(max_workers=worker_count) as executor:
|
|
419
|
+
for si_chunk, dnfr_chunk, accel_chunk in executor.map(
|
|
420
|
+
_selector_metrics_chunk, _args_iter()
|
|
421
|
+
):
|
|
422
|
+
si_seq.extend(si_chunk)
|
|
423
|
+
dnfr_seq.extend(dnfr_chunk)
|
|
424
|
+
accel_seq.extend(accel_chunk)
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
node: (si_seq[idx], dnfr_seq[idx], accel_seq[idx])
|
|
428
|
+
for idx, node in enumerate(nodes)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _compute_default_base_choices(
|
|
433
|
+
metrics: Mapping[Any, tuple[float, float, float]],
|
|
434
|
+
thresholds: Mapping[str, float],
|
|
435
|
+
) -> dict[Any, str]:
|
|
436
|
+
si_hi = float(thresholds.get("si_hi", 0.66))
|
|
437
|
+
si_lo = float(thresholds.get("si_lo", 0.33))
|
|
438
|
+
dnfr_hi = float(thresholds.get("dnfr_hi", 0.50))
|
|
439
|
+
|
|
440
|
+
base: dict[Any, str] = {}
|
|
441
|
+
for node, (Si, dnfr, _) in metrics.items():
|
|
442
|
+
if Si >= si_hi:
|
|
443
|
+
base[node] = "IL"
|
|
444
|
+
elif Si <= si_lo:
|
|
445
|
+
base[node] = "OZ" if dnfr > dnfr_hi else "ZHIR"
|
|
446
|
+
else:
|
|
447
|
+
base[node] = "NAV" if dnfr > dnfr_hi else "RA"
|
|
448
|
+
return base
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _param_base_worker(
|
|
452
|
+
args: tuple[Mapping[str, float], list[tuple[Any, tuple[float, float, float]]]]
|
|
453
|
+
) -> list[tuple[Any, str]]:
|
|
454
|
+
thresholds, chunk = args
|
|
455
|
+
return [
|
|
456
|
+
(node, _selector_base_choice(Si, dnfr, accel, thresholds))
|
|
457
|
+
for node, (Si, dnfr, accel) in chunk
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _compute_param_base_choices(
|
|
462
|
+
metrics: Mapping[Any, tuple[float, float, float]],
|
|
463
|
+
thresholds: Mapping[str, float],
|
|
464
|
+
n_jobs: int | None,
|
|
465
|
+
) -> dict[Any, str]:
|
|
466
|
+
if not metrics:
|
|
467
|
+
return {}
|
|
468
|
+
|
|
469
|
+
items = list(metrics.items())
|
|
470
|
+
if n_jobs is None or n_jobs <= 1:
|
|
471
|
+
return {
|
|
472
|
+
node: _selector_base_choice(Si, dnfr, accel, thresholds)
|
|
473
|
+
for node, (Si, dnfr, accel) in items
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
chunk_size = max(1, math.ceil(len(items) / n_jobs))
|
|
477
|
+
chunks = [
|
|
478
|
+
items[i : i + chunk_size]
|
|
479
|
+
for i in range(0, len(items), chunk_size)
|
|
480
|
+
]
|
|
481
|
+
base: dict[Any, str] = {}
|
|
482
|
+
args = ((thresholds, chunk) for chunk in chunks)
|
|
483
|
+
executor_cls = ProcessPoolExecutor
|
|
484
|
+
dynamics_module = sys.modules.get("tnfr.dynamics")
|
|
485
|
+
if dynamics_module is not None:
|
|
486
|
+
executor_cls = getattr(
|
|
487
|
+
dynamics_module, "ProcessPoolExecutor", ProcessPoolExecutor
|
|
488
|
+
)
|
|
489
|
+
with executor_cls(max_workers=n_jobs) as executor:
|
|
490
|
+
for result in executor.map(_param_base_worker, args):
|
|
491
|
+
for node, cand in result:
|
|
492
|
+
base[node] = cand
|
|
493
|
+
return base
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _prepare_selector_preselection(
|
|
497
|
+
G: TNFRGraph,
|
|
498
|
+
selector: GlyphSelector,
|
|
499
|
+
nodes: Sequence[NodeId],
|
|
500
|
+
) -> _SelectorPreselection | None:
|
|
501
|
+
if selector is default_glyph_selector:
|
|
502
|
+
return _build_default_preselection(G, nodes)
|
|
503
|
+
if selector is parametric_glyph_selector:
|
|
504
|
+
return _build_param_preselection(G, nodes)
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _resolve_preselected_glyph(
|
|
509
|
+
G: TNFRGraph,
|
|
510
|
+
n: NodeId,
|
|
511
|
+
selector: GlyphSelector,
|
|
512
|
+
preselection: _SelectorPreselection | None,
|
|
513
|
+
) -> GlyphCode:
|
|
514
|
+
if preselection is None:
|
|
515
|
+
return selector(G, n)
|
|
516
|
+
|
|
517
|
+
metrics = preselection.metrics.get(n)
|
|
518
|
+
if metrics is None:
|
|
519
|
+
return selector(G, n)
|
|
520
|
+
|
|
521
|
+
if preselection.kind == "default":
|
|
522
|
+
cand = preselection.base_choices.get(n)
|
|
523
|
+
return cand if cand is not None else selector(G, n)
|
|
524
|
+
|
|
525
|
+
if preselection.kind == "param":
|
|
526
|
+
Si, dnfr, accel = metrics
|
|
527
|
+
thresholds = preselection.thresholds or _selector_thresholds(G)
|
|
528
|
+
margin: float | None = preselection.margin
|
|
529
|
+
if margin is None:
|
|
530
|
+
margin = get_graph_param(G, "GLYPH_SELECTOR_MARGIN")
|
|
531
|
+
|
|
532
|
+
cand = preselection.base_choices.get(n)
|
|
533
|
+
if cand is None:
|
|
534
|
+
cand = _selector_base_choice(Si, dnfr, accel, thresholds)
|
|
535
|
+
|
|
536
|
+
nd = G.nodes[n]
|
|
537
|
+
hist_cand = _apply_selector_hysteresis(
|
|
538
|
+
nd, Si, dnfr, accel, thresholds, margin
|
|
539
|
+
)
|
|
540
|
+
if hist_cand is not None:
|
|
541
|
+
return hist_cand
|
|
542
|
+
|
|
543
|
+
score = _compute_selector_score(G, nd, Si, dnfr, accel, cand)
|
|
544
|
+
cand = _apply_score_override(cand, score, dnfr, thresholds["dnfr_lo"])
|
|
545
|
+
return _soft_grammar_prefilter(G, n, cand, dnfr, accel)
|
|
546
|
+
|
|
547
|
+
return selector(G, n)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _glyph_proposal_worker(
|
|
551
|
+
args: tuple[
|
|
552
|
+
list[NodeId],
|
|
553
|
+
TNFRGraph,
|
|
554
|
+
GlyphSelector,
|
|
555
|
+
_SelectorPreselection | None,
|
|
556
|
+
]
|
|
557
|
+
) -> list[tuple[NodeId, GlyphCode]]:
|
|
558
|
+
nodes, G, selector, preselection = args
|
|
559
|
+
return [
|
|
560
|
+
(n, _resolve_preselected_glyph(G, n, selector, preselection))
|
|
561
|
+
for n in nodes
|
|
562
|
+
]
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _apply_glyphs(G: TNFRGraph, selector: GlyphSelector, hist: HistoryState) -> None:
|
|
566
|
+
window = int(get_param(G, "GLYPH_HYSTERESIS_WINDOW"))
|
|
567
|
+
use_canon = bool(
|
|
568
|
+
get_graph_param(G, "GRAMMAR_CANON", dict).get("enabled", False)
|
|
569
|
+
)
|
|
570
|
+
al_max = get_graph_param(G, "AL_MAX_LAG", int)
|
|
571
|
+
en_max = get_graph_param(G, "EN_MAX_LAG", int)
|
|
572
|
+
|
|
573
|
+
nodes_data = list(G.nodes(data=True))
|
|
574
|
+
nodes = [n for n, _ in nodes_data]
|
|
575
|
+
if isinstance(selector, AbstractSelector):
|
|
576
|
+
selector.prepare(G, nodes)
|
|
577
|
+
preselection: _SelectorPreselection | None = None
|
|
578
|
+
else:
|
|
579
|
+
preselection = _prepare_selector_preselection(G, selector, nodes)
|
|
580
|
+
|
|
581
|
+
h_al = hist.setdefault("since_AL", {})
|
|
582
|
+
h_en = hist.setdefault("since_EN", {})
|
|
583
|
+
forced: dict[Any, str | Glyph] = {}
|
|
584
|
+
to_select: list[Any] = []
|
|
585
|
+
|
|
586
|
+
for n, _ in nodes_data:
|
|
587
|
+
h_al[n] = int(h_al.get(n, 0)) + 1
|
|
588
|
+
h_en[n] = int(h_en.get(n, 0)) + 1
|
|
589
|
+
|
|
590
|
+
if h_al[n] > al_max:
|
|
591
|
+
forced[n] = Glyph.AL
|
|
592
|
+
elif h_en[n] > en_max:
|
|
593
|
+
forced[n] = Glyph.EN
|
|
594
|
+
else:
|
|
595
|
+
to_select.append(n)
|
|
596
|
+
|
|
597
|
+
decisions: dict[Any, str | Glyph] = dict(forced)
|
|
598
|
+
forced_al_nodes = {n for n, choice in forced.items() if choice == Glyph.AL}
|
|
599
|
+
forced_en_nodes = {n for n, choice in forced.items() if choice == Glyph.EN}
|
|
600
|
+
if to_select:
|
|
601
|
+
n_jobs = _selector_parallel_jobs(G)
|
|
602
|
+
if n_jobs is None:
|
|
603
|
+
for n in to_select:
|
|
604
|
+
decisions[n] = _resolve_preselected_glyph(
|
|
605
|
+
G, n, selector, preselection
|
|
606
|
+
)
|
|
607
|
+
else:
|
|
608
|
+
chunk_size = max(1, math.ceil(len(to_select) / n_jobs))
|
|
609
|
+
chunks = [
|
|
610
|
+
to_select[idx : idx + chunk_size]
|
|
611
|
+
for idx in range(0, len(to_select), chunk_size)
|
|
612
|
+
]
|
|
613
|
+
dynamics_module = sys.modules.get("tnfr.dynamics")
|
|
614
|
+
executor_cls = ProcessPoolExecutor
|
|
615
|
+
if dynamics_module is not None:
|
|
616
|
+
executor_cls = getattr(
|
|
617
|
+
dynamics_module, "ProcessPoolExecutor", ProcessPoolExecutor
|
|
618
|
+
)
|
|
619
|
+
with executor_cls(max_workers=n_jobs) as executor:
|
|
620
|
+
args_iter = (
|
|
621
|
+
(chunk, G, selector, preselection) for chunk in chunks
|
|
622
|
+
)
|
|
623
|
+
for results in executor.map(_glyph_proposal_worker, args_iter):
|
|
624
|
+
for node, glyph in results:
|
|
625
|
+
decisions[node] = glyph
|
|
626
|
+
|
|
627
|
+
for n, _ in nodes_data:
|
|
628
|
+
g = decisions.get(n)
|
|
629
|
+
if g is None:
|
|
630
|
+
continue
|
|
631
|
+
|
|
632
|
+
if use_canon:
|
|
633
|
+
g = enforce_canonical_grammar(G, n, g)
|
|
634
|
+
|
|
635
|
+
apply_glyph(G, n, g, window=window)
|
|
636
|
+
if use_canon:
|
|
637
|
+
on_applied_glyph(G, n, g)
|
|
638
|
+
|
|
639
|
+
if n in forced_al_nodes:
|
|
640
|
+
h_al[n] = 0
|
|
641
|
+
h_en[n] = min(h_en[n], en_max)
|
|
642
|
+
continue
|
|
643
|
+
if n in forced_en_nodes:
|
|
644
|
+
h_en[n] = 0
|
|
645
|
+
continue
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
glyph_enum = g if isinstance(g, Glyph) else Glyph(str(g))
|
|
649
|
+
except ValueError:
|
|
650
|
+
glyph_enum = None
|
|
651
|
+
|
|
652
|
+
if glyph_enum is Glyph.AL:
|
|
653
|
+
h_al[n] = 0
|
|
654
|
+
h_en[n] = min(h_en[n], en_max)
|
|
655
|
+
elif glyph_enum is Glyph.EN:
|
|
656
|
+
h_en[n] = 0
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _apply_selector(G: TNFRGraph) -> GlyphSelector:
|
|
660
|
+
raw_selector = G.graph.get("glyph_selector")
|
|
661
|
+
|
|
662
|
+
selector: GlyphSelector
|
|
663
|
+
if isinstance(raw_selector, AbstractSelector):
|
|
664
|
+
selector = raw_selector
|
|
665
|
+
elif isinstance(raw_selector, type) and issubclass(raw_selector, AbstractSelector):
|
|
666
|
+
selector_obj = cast(AbstractSelector, raw_selector())
|
|
667
|
+
G.graph["glyph_selector"] = selector_obj
|
|
668
|
+
selector = selector_obj
|
|
669
|
+
elif raw_selector is None:
|
|
670
|
+
selector = default_glyph_selector
|
|
671
|
+
elif callable(raw_selector):
|
|
672
|
+
selector = cast(GlyphSelector, raw_selector)
|
|
673
|
+
else:
|
|
674
|
+
selector = default_glyph_selector
|
|
675
|
+
|
|
676
|
+
if isinstance(selector, ParametricGlyphSelector) or selector is parametric_glyph_selector:
|
|
677
|
+
_selector_norms(G)
|
|
678
|
+
_configure_selector_weights(G)
|
|
679
|
+
return selector
|
|
680
|
+
|