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.
Files changed (170) hide show
  1. tnfr/__init__.py +270 -90
  2. tnfr/__init__.pyi +40 -0
  3. tnfr/_compat.py +11 -0
  4. tnfr/_version.py +7 -0
  5. tnfr/_version.pyi +7 -0
  6. tnfr/alias.py +631 -0
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +732 -0
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +381 -0
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +89 -0
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +199 -0
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +322 -0
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +34 -0
  19. tnfr/cli/utils.pyi +8 -0
  20. tnfr/config/__init__.py +12 -0
  21. tnfr/config/__init__.pyi +8 -0
  22. tnfr/config/constants.py +104 -0
  23. tnfr/config/constants.pyi +12 -0
  24. tnfr/config/init.py +36 -0
  25. tnfr/config/init.pyi +8 -0
  26. tnfr/config/operator_names.py +106 -0
  27. tnfr/config/operator_names.pyi +28 -0
  28. tnfr/config/presets.py +104 -0
  29. tnfr/config/presets.pyi +7 -0
  30. tnfr/constants/__init__.py +228 -0
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +158 -0
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.py +31 -0
  35. tnfr/constants/init.pyi +12 -0
  36. tnfr/constants/metric.py +102 -0
  37. tnfr/constants/metric.pyi +19 -0
  38. tnfr/constants_glyphs.py +16 -0
  39. tnfr/constants_glyphs.pyi +12 -0
  40. tnfr/dynamics/__init__.py +136 -0
  41. tnfr/dynamics/__init__.pyi +83 -0
  42. tnfr/dynamics/adaptation.py +201 -0
  43. tnfr/dynamics/aliases.py +22 -0
  44. tnfr/dynamics/coordination.py +343 -0
  45. tnfr/dynamics/dnfr.py +2315 -0
  46. tnfr/dynamics/dnfr.pyi +33 -0
  47. tnfr/dynamics/integrators.py +561 -0
  48. tnfr/dynamics/integrators.pyi +35 -0
  49. tnfr/dynamics/runtime.py +521 -0
  50. tnfr/dynamics/sampling.py +34 -0
  51. tnfr/dynamics/sampling.pyi +7 -0
  52. tnfr/dynamics/selectors.py +680 -0
  53. tnfr/execution.py +216 -0
  54. tnfr/execution.pyi +65 -0
  55. tnfr/flatten.py +283 -0
  56. tnfr/flatten.pyi +28 -0
  57. tnfr/gamma.py +320 -89
  58. tnfr/gamma.pyi +40 -0
  59. tnfr/glyph_history.py +337 -0
  60. tnfr/glyph_history.pyi +53 -0
  61. tnfr/grammar.py +23 -153
  62. tnfr/grammar.pyi +13 -0
  63. tnfr/helpers/__init__.py +151 -0
  64. tnfr/helpers/__init__.pyi +66 -0
  65. tnfr/helpers/numeric.py +88 -0
  66. tnfr/helpers/numeric.pyi +12 -0
  67. tnfr/immutable.py +214 -0
  68. tnfr/immutable.pyi +37 -0
  69. tnfr/initialization.py +199 -0
  70. tnfr/initialization.pyi +73 -0
  71. tnfr/io.py +311 -0
  72. tnfr/io.pyi +11 -0
  73. tnfr/locking.py +37 -0
  74. tnfr/locking.pyi +7 -0
  75. tnfr/metrics/__init__.py +41 -0
  76. tnfr/metrics/__init__.pyi +20 -0
  77. tnfr/metrics/coherence.py +1469 -0
  78. tnfr/metrics/common.py +149 -0
  79. tnfr/metrics/common.pyi +15 -0
  80. tnfr/metrics/core.py +259 -0
  81. tnfr/metrics/core.pyi +13 -0
  82. tnfr/metrics/diagnosis.py +840 -0
  83. tnfr/metrics/diagnosis.pyi +89 -0
  84. tnfr/metrics/export.py +151 -0
  85. tnfr/metrics/glyph_timing.py +369 -0
  86. tnfr/metrics/reporting.py +152 -0
  87. tnfr/metrics/reporting.pyi +12 -0
  88. tnfr/metrics/sense_index.py +294 -0
  89. tnfr/metrics/sense_index.pyi +9 -0
  90. tnfr/metrics/trig.py +216 -0
  91. tnfr/metrics/trig.pyi +12 -0
  92. tnfr/metrics/trig_cache.py +105 -0
  93. tnfr/metrics/trig_cache.pyi +10 -0
  94. tnfr/node.py +255 -177
  95. tnfr/node.pyi +161 -0
  96. tnfr/observers.py +154 -150
  97. tnfr/observers.pyi +46 -0
  98. tnfr/ontosim.py +135 -134
  99. tnfr/ontosim.pyi +33 -0
  100. tnfr/operators/__init__.py +452 -0
  101. tnfr/operators/__init__.pyi +31 -0
  102. tnfr/operators/definitions.py +181 -0
  103. tnfr/operators/definitions.pyi +92 -0
  104. tnfr/operators/jitter.py +266 -0
  105. tnfr/operators/jitter.pyi +11 -0
  106. tnfr/operators/registry.py +80 -0
  107. tnfr/operators/registry.pyi +15 -0
  108. tnfr/operators/remesh.py +569 -0
  109. tnfr/presets.py +10 -23
  110. tnfr/presets.pyi +7 -0
  111. tnfr/py.typed +0 -0
  112. tnfr/rng.py +440 -0
  113. tnfr/rng.pyi +14 -0
  114. tnfr/selector.py +217 -0
  115. tnfr/selector.pyi +19 -0
  116. tnfr/sense.py +307 -142
  117. tnfr/sense.pyi +30 -0
  118. tnfr/structural.py +69 -164
  119. tnfr/structural.pyi +46 -0
  120. tnfr/telemetry/__init__.py +13 -0
  121. tnfr/telemetry/verbosity.py +37 -0
  122. tnfr/tokens.py +61 -0
  123. tnfr/tokens.pyi +41 -0
  124. tnfr/trace.py +520 -95
  125. tnfr/trace.pyi +68 -0
  126. tnfr/types.py +382 -17
  127. tnfr/types.pyi +145 -0
  128. tnfr/utils/__init__.py +158 -0
  129. tnfr/utils/__init__.pyi +133 -0
  130. tnfr/utils/cache.py +755 -0
  131. tnfr/utils/cache.pyi +156 -0
  132. tnfr/utils/data.py +267 -0
  133. tnfr/utils/data.pyi +73 -0
  134. tnfr/utils/graph.py +87 -0
  135. tnfr/utils/graph.pyi +10 -0
  136. tnfr/utils/init.py +746 -0
  137. tnfr/utils/init.pyi +85 -0
  138. tnfr/utils/io.py +157 -0
  139. tnfr/utils/io.pyi +10 -0
  140. tnfr/utils/validators.py +130 -0
  141. tnfr/utils/validators.pyi +19 -0
  142. tnfr/validation/__init__.py +25 -0
  143. tnfr/validation/__init__.pyi +17 -0
  144. tnfr/validation/compatibility.py +59 -0
  145. tnfr/validation/compatibility.pyi +8 -0
  146. tnfr/validation/grammar.py +149 -0
  147. tnfr/validation/grammar.pyi +11 -0
  148. tnfr/validation/rules.py +194 -0
  149. tnfr/validation/rules.pyi +18 -0
  150. tnfr/validation/syntax.py +151 -0
  151. tnfr/validation/syntax.pyi +7 -0
  152. tnfr-6.0.0.dist-info/METADATA +135 -0
  153. tnfr-6.0.0.dist-info/RECORD +157 -0
  154. tnfr/cli.py +0 -322
  155. tnfr/config.py +0 -41
  156. tnfr/constants.py +0 -277
  157. tnfr/dynamics.py +0 -814
  158. tnfr/helpers.py +0 -264
  159. tnfr/main.py +0 -47
  160. tnfr/metrics.py +0 -597
  161. tnfr/operators.py +0 -525
  162. tnfr/program.py +0 -176
  163. tnfr/scenarios.py +0 -34
  164. tnfr/validators.py +0 -38
  165. tnfr-4.5.1.dist-info/METADATA +0 -221
  166. tnfr-4.5.1.dist-info/RECORD +0 -28
  167. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  168. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  169. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {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
+