Repository navigation
Expand file tree
/
Copy pathruntime.py
More file actions
1262 lines (1125 loc) · 44.2 KB
/
Copy pathruntime.py
File metadata and controls
1262 lines (1125 loc) · 44.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Runtime orchestration for TNFR dynamics."""
from __future__ import annotations
import inspect
import sys
from collections import deque
from collections.abc import Mapping, MutableMapping
from copy import deepcopy
from itertools import islice
from numbers import Integral
from typing import Any, cast
from .._exact_time import bounded_represented_real, finite_represented_real
from .._runtime_steps import prepare_runtime_step, runtime_step_scope
from .._spectral_expectation import (
finite_spectral_real,
resolve_compatibility_value,
spectral_expectation_metadata,
spectral_expectation_payload,
validate_spectral_operator,
)
from ..alias import get_attr
from ..config.defaults_metric import METRIC_DEFAULTS
from ..config.operator_names import BIFURCATION_WINDOW
from ..constants import get_graph_param, get_param
from ..errors import TNFRValueError
from ..glyph_history import ensure_history
from ..metrics.coherence import _stability_observation_revision
from ..metrics.sense_index import compute_Si
from ..operators import apply_remesh_if_globally_stable
from ..telemetry import publish_graph_cache_metrics
from ..types import HistoryState, NodeId, TNFRGraph, require_finite_real_scalar_epi
from ..utils import CallbackEvent, callback_manager, normalize_optional_int
from ..validation import apply_canonical_clamps
from ..validation.window import validate_window
from . import adaptation, coordination, integrators, selectors
from .aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_SI, ALIAS_THETA, ALIAS_VF
try: # pragma: no cover - optional NumPy dependency
from ..mathematics.unified_numerical import np
except ImportError: # pragma: no cover - optional dependency missing
np = None # type: ignore[assignment]
try: # pragma: no cover - optional math extras
from ..mathematics.dynamics import MathematicalDynamicsEngine
from ..mathematics.projection import BasicStateProjector
from ..mathematics.runtime import frequency_positive as runtime_frequency_positive
from ..mathematics.runtime import (
meets_spectral_expectation_threshold as runtime_spectral_threshold,
)
from ..mathematics.runtime import normalized as runtime_normalized
except Exception: # pragma: no cover - fallback when extras not available
MathematicalDynamicsEngine = None # type: ignore[assignment]
BasicStateProjector = None # type: ignore[assignment]
runtime_spectral_threshold = None # type: ignore[assignment]
runtime_frequency_positive = None # type: ignore[assignment]
runtime_normalized = None # type: ignore[assignment]
from .dnfr import _invoke_dnfr_hook, default_compute_delta_nfr
from .remesh_history import append_remesh_epi_history_snapshot
from .sampling import update_node_sample as _update_node_sample
__all__ = (
"ALIAS_VF",
"ALIAS_DNFR",
"ALIAS_EPI",
"ALIAS_SI",
"_normalize_job_overrides",
"_resolve_jobs_override",
"_refresh_delta_nfr",
"_prepare_dnfr",
"_record_mutation_flow_boundary",
"_update_nodes",
"_update_epi_hist",
"_maybe_remesh",
"_run_validators",
"_run_before_callbacks",
"_run_after_callbacks",
"step",
"run",
)
_MUTATION_TIME_HISTORY_KEY = "epi_time_history"
_MUTATION_TIME_HISTORY_MAXLEN = max(2, BIFURCATION_WINDOW + 1)
def _finite_mutation_sample_scalar(value: Any, field: str) -> float:
"""Return one finite non-Boolean runtime sample scalar."""
try:
return finite_represented_real(value, field)[0]
except (TypeError, ValueError) as exc:
raise TNFRValueError(
str(exc),
context={"field": field, "value": repr(value)},
) from exc
def _validated_mutation_time_history(
raw: Any, *, node: NodeId
) -> list[tuple[float, float]]:
"""Materialize one existing physical history without accepting ambiguity."""
if raw is None:
return []
if isinstance(raw, list):
entries = tuple(list.__iter__(raw))
elif isinstance(raw, tuple):
entries = tuple(tuple.__iter__(raw))
elif isinstance(raw, deque):
entries = tuple(deque.__iter__(raw))
else:
raise TNFRValueError(
"epi_time_history must be an indexed sequence of (time, EPI) pairs.",
context={"node": node, "history_type": type(raw).__name__},
)
samples: list[tuple[float, float]] = []
for index, entry in enumerate(entries):
if isinstance(entry, list):
pair = tuple(list.__iter__(entry))
elif isinstance(entry, tuple):
pair = tuple(tuple.__iter__(entry))
elif isinstance(entry, deque):
pair = tuple(deque.__iter__(entry))
else:
pair = ()
if len(pair) != 2:
raise TNFRValueError(
"epi_time_history entries must be (time, EPI) pairs.",
context={"node": node, "sample_index": index},
)
sample_time = _finite_mutation_sample_scalar(
pair[0], f"epi_time_history[{index}].time"
)
sample_epi = _finite_mutation_sample_scalar(
pair[1], f"epi_time_history[{index}].EPI"
)
if samples and sample_time <= samples[-1][0]:
raise TNFRValueError(
"epi_time_history timestamps must increase strictly.",
context={
"node": node,
"sample_index": index,
"previous_time": samples[-1][0],
"current_time": sample_time,
},
)
samples.append((sample_time, sample_epi))
return samples[-_MUTATION_TIME_HISTORY_MAXLEN:]
def _record_mutation_flow_boundary(G: TNFRGraph) -> None:
"""Record a physical EPI boundary atomically across all runtime nodes.
A changed EPI at the same timestamp is an instantaneous hybrid jump. Its
history is restarted at that right-hand endpoint so the next secant
measures only continuous nodal flow and cannot absorb the jump into
``dEPI/dt``. A custom integrator that changes EPI without advancing
``G.graph['_t']`` therefore creates no fabricated physical rate.
Samples require the signed scalar EPI chart; a rich BEPI magnitude cannot
replace its state or supply evidence for scalar Mutation admission.
"""
# The event executor calls this function inside a graph transaction. Use
# the same raw NetworkX layout primitives here so a mapping/list subclass
# cannot run user code merely because the recorder reads existing state.
from ..operators.network_stage import (
_networkx_runtime_layout,
_runtime_mapping_items,
_set_runtime_mapping_item,
)
layout = _networkx_runtime_layout(G)
graph_items = _runtime_mapping_items(layout.graph_mapping)
raw_time = next(
(value for key, value in graph_items if type(key) is str and key == "_t"),
0.0,
)
sample_time = _finite_mutation_sample_scalar(
raw_time,
"graph runtime time",
)
proposals: list[tuple[MutableMapping[Any, Any], deque[tuple[float, float]]]] = []
for node, node_data in layout.node_data:
node_id = cast(NodeId, node)
node_items = _runtime_mapping_items(node_data)
raw_epi = next(
(
value
for alias in ALIAS_EPI
for key, value in node_items
if type(key) is str and key == alias
),
0.0,
)
epi = require_finite_real_scalar_epi(
raw_epi,
f"node {node!r} EPI",
)
raw_history = next(
(
value
for key, value in node_items
if type(key) is str and key == _MUTATION_TIME_HISTORY_KEY
),
None,
)
samples = _validated_mutation_time_history(
raw_history,
node=node_id,
)
if samples:
previous_time, previous_epi = samples[-1]
if sample_time < previous_time:
raise TNFRValueError(
"Runtime time precedes the latest EPI sample.",
context={
"node": node,
"runtime_time": sample_time,
"history_time": previous_time,
},
)
if sample_time == previous_time:
if epi != previous_epi:
samples = [(sample_time, epi)]
else:
samples.append((sample_time, epi))
else:
samples.append((sample_time, epi))
proposals.append(
(
node_data,
deque(
samples[-_MUTATION_TIME_HISTORY_MAXLEN:],
maxlen=_MUTATION_TIME_HISTORY_MAXLEN,
),
)
)
for node_data, history in proposals:
_set_runtime_mapping_item(
node_data,
_MUTATION_TIME_HISTORY_KEY,
history,
)
def _normalize_job_overrides(
job_overrides: Mapping[str, Any] | None,
) -> dict[str, Any]:
"""Canonicalise job override keys for ΔNFR, νf and phase orchestration.
Parameters
----------
job_overrides : Mapping[str, Any] | None
User-provided mapping whose keys may use legacy ``*_N_JOBS`` forms or
mixed casing. The values tune the parallel workloads that update ΔNFR,
νf adaptation and global phase coordination.
Returns
-------
dict[str, Any]
A dictionary where keys are upper-cased without the ``_N_JOBS`` suffix,
ready for downstream lookup in the runtime schedulers.
Notes
-----
``None`` keys are silently skipped to preserve resiliency when
orchestrating ΔNFR workers.
Examples
--------
>>> _normalize_job_overrides({"dnfr_n_jobs": 2, "vf_adapt": 4})
{'DNFR': 2, 'VF_ADAPT': 4}
>>> _normalize_job_overrides(None)
{}
"""
if not job_overrides:
return {}
normalized: dict[str, Any] = {}
for key, value in job_overrides.items():
if key is None:
continue
key_str = str(key).upper()
if key_str.endswith("_N_JOBS"):
key_str = key_str[: -len("_N_JOBS")]
normalized[key_str] = value
return normalized
def _resolve_jobs_override(
overrides: Mapping[str, Any],
key: str,
graph_value: Any,
*,
allow_non_positive: bool,
) -> int | None:
"""Resolve job overrides prioritising user hints over graph defaults.
Parameters
----------
overrides : Mapping[str, Any]
Normalised overrides produced by :func:`_normalize_job_overrides` that
steer the ΔNFR computation, νf adaptation or phase coupling workers.
key : str
Logical subsystem key such as ``"DNFR"`` or ``"VF_ADAPT"``.
graph_value : Any
Baseline job count stored in the graph configuration.
allow_non_positive : bool
Propagated policy describing whether zero or negative values are valid
for the subsystem.
Returns
-------
int | None
Final job count that each scheduler will use, or ``None`` when no
explicit override or valid fallback exists.
Notes
-----
Preference resolution is pure and returns ``None`` instead of raising when
overrides cannot be coerced into valid integers.
Examples
--------
>>> overrides = _normalize_job_overrides({"phase": 0})
>>> _resolve_jobs_override(overrides, "phase", 2, allow_non_positive=True)
0
>>> _resolve_jobs_override({}, "vf_adapt", 4, allow_non_positive=False)
4
"""
norm_key = key.upper()
if overrides and norm_key in overrides:
return normalize_optional_int(
overrides.get(norm_key),
allow_non_positive=allow_non_positive,
strict=False,
sentinels=None,
)
return normalize_optional_int(
graph_value,
allow_non_positive=allow_non_positive,
strict=False,
sentinels=None,
)
_INTEGRATOR_CACHE_KEY = "_integrator_cache"
def _call_integrator_factory(factory: Any, G: TNFRGraph) -> Any:
"""Invoke an integrator factory respecting optional graph injection."""
try:
signature = inspect.signature(factory)
except (TypeError, ValueError):
return factory()
params = list(signature.parameters.values())
required = [
p
for p in params
if p.default is inspect._empty
and p.kind
in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
)
]
if any(p.kind is inspect.Parameter.KEYWORD_ONLY for p in required):
raise TypeError(
"Integrator factory cannot require keyword-only arguments",
)
positional_required = [
p
for p in required
if p.kind
in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
)
]
if len(positional_required) > 1:
raise TypeError(
"Integrator factory must accept at most one positional argument",
)
if positional_required:
return factory(G)
positional = [
p
for p in params
if p.kind
in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
)
]
if len(positional) == 1:
return factory(G)
elif len(positional) > 1:
raise TypeError(
"Integrator factory must accept at most one positional argument",
)
# Check for any remaining required positional or keyword-only arguments
remaining_required = [
p
for p in params
if p.default is inspect._empty
and p.kind
in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
)
]
if remaining_required:
raise TypeError(
f"Integrator factory requires arguments: {', '.join(p.name for p in remaining_required)}"
)
return factory()
def _resolve_integrator_instance(G: TNFRGraph) -> integrators.AbstractIntegrator:
"""Return an integrator instance configured on ``G`` or a default."""
cache_entry = G.graph.get(_INTEGRATOR_CACHE_KEY)
candidate = G.graph.get("integrator")
if (
isinstance(cache_entry, tuple)
and len(cache_entry) == 2
and cache_entry[0] is candidate
and isinstance(cache_entry[1], integrators.AbstractIntegrator)
):
return cache_entry[1]
if isinstance(candidate, integrators.AbstractIntegrator):
instance = candidate
elif inspect.isclass(candidate) and issubclass(
candidate, integrators.AbstractIntegrator
):
instance = candidate()
elif callable(candidate):
instance = cast(
integrators.AbstractIntegrator,
_call_integrator_factory(candidate, G),
)
elif candidate is None:
instance = integrators.DefaultIntegrator()
else:
raise TypeError(
"Graph integrator must be an AbstractIntegrator, subclass or callable",
)
if not isinstance(instance, integrators.AbstractIntegrator):
raise TypeError(
"Configured integrator must implement AbstractIntegrator.integrate",
)
G.graph[_INTEGRATOR_CACHE_KEY] = (candidate, instance)
return instance
def _preflight_runtime_parameters(
G: TNFRGraph,
*,
dt: float | None,
job_overrides: Mapping[str, Any],
) -> None:
"""Reject invalid supplied built-in policies before beginning a step.
The component owners validate again when consumed: callbacks can change
configuration in between. This is not full-step rollback and does not
instantiate or constrain an arbitrary custom integrator or its factory.
"""
vf_jobs = _resolve_jobs_override(
job_overrides,
"VF_ADAPT",
G.graph.get("VF_ADAPT_N_JOBS"),
allow_non_positive=False,
)
adaptation._validated_parameters(G, vf_jobs)
coordination._read_adaptive_params(G.graph)
candidate = G.graph.get("integrator")
if (
candidate is None
or candidate is integrators.DefaultIntegrator
or type(candidate) is integrators.DefaultIntegrator
):
integrators.prepare_integration_params(G, dt=dt)
def _run_before_callbacks(
G: TNFRGraph,
*,
step_idx: int,
dt: float | None,
use_Si: bool,
apply_glyphs: bool,
) -> None:
"""Notify ``BEFORE_STEP`` observers with execution context."""
callback_manager.invoke_callbacks(
G,
CallbackEvent.BEFORE_STEP.value,
{
"step": step_idx,
"dt": dt,
"use_Si": use_Si,
"apply_glyphs": apply_glyphs,
},
)
def _prepare_dnfr(
G: TNFRGraph,
*,
use_Si: bool,
job_overrides: Mapping[str, Any] | None = None,
) -> None:
"""Recompute ΔNFR (and optionally Si) ahead of an integration step."""
overrides = job_overrides or {}
n_jobs = _resolve_jobs_override(
overrides,
"DNFR",
G.graph.get("DNFR_N_JOBS"),
allow_non_positive=False,
)
_refresh_delta_nfr(G, n_jobs=n_jobs)
if use_Si:
si_jobs = _resolve_jobs_override(
overrides,
"SI",
G.graph.get("SI_N_JOBS"),
allow_non_positive=False,
)
dynamics_module = sys.modules.get("tnfr.dynamics")
compute_si_fn = (
getattr(dynamics_module, "compute_Si", None)
if dynamics_module is not None
else None
)
if compute_si_fn is None:
compute_si_fn = compute_Si
compute_si_fn(G, inplace=True, n_jobs=si_jobs)
def _refresh_delta_nfr(
G: TNFRGraph,
*,
n_jobs: int | None,
) -> Any:
"""Refresh stored pressure through the effective graph callback.
Signature-aware dispatch keeps legacy callbacks that accept only the graph
usable without retrying a callback that raised ``TypeError`` internally.
When a callable has no inspectable signature, a narrowly identified
argument-binding error retains the historical no-``n_jobs`` fallback.
The effective callback is returned so runtime evidence can identify it.
"""
compute_dnfr_cb = G.graph.get(
"compute_delta_nfr",
default_compute_delta_nfr,
)
_invoke_dnfr_hook(compute_dnfr_cb, G, n_jobs=n_jobs)
G.graph.pop("_sel_norms", None)
return compute_dnfr_cb
def _update_nodes(
G: TNFRGraph,
*,
dt: float | None,
use_Si: bool,
apply_glyphs: bool,
step_idx: int,
hist: HistoryState,
job_overrides: Mapping[str, Any] | None = None,
) -> float:
"""Advance nodal state and return the duration supplied to its integrator."""
# The left boundary makes the previous completed flow interval available
# to autonomous ZHIR selection. Any same-time EPI jump below restarts that
# node's history before integration.
_record_mutation_flow_boundary(G)
_update_node_sample(G, step=step_idx)
overrides = job_overrides or {}
_prepare_dnfr(G, use_Si=use_Si, job_overrides=overrides)
if apply_glyphs:
selector = selectors._apply_selector(G)
selectors._apply_glyphs(G, selector, hist)
_record_mutation_flow_boundary(G)
_dt = integrators._finite_output(get_param(G, "DT") if dt is None else dt, "dt")
method = get_graph_param(G, "INTEGRATOR_METHOD", str)
n_jobs = _resolve_jobs_override(
overrides,
"INTEGRATOR",
G.graph.get("INTEGRATOR_N_JOBS"),
allow_non_positive=True,
)
integrator = _resolve_integrator_instance(G)
integrator.integrate(
G,
dt=_dt,
t=None,
method=cast(str | None, method),
n_jobs=n_jobs,
)
for n, nd in G.nodes(data=True):
apply_canonical_clamps(cast(MutableMapping[str, Any], nd), G, cast(NodeId, n))
phase_jobs = _resolve_jobs_override(
overrides,
"PHASE",
G.graph.get("PHASE_N_JOBS"),
allow_non_positive=True,
)
coordination.coordinate_global_local_phase(G, None, None, n_jobs=phase_jobs)
vf_jobs = _resolve_jobs_override(
overrides,
"VF_ADAPT",
G.graph.get("VF_ADAPT_N_JOBS"),
allow_non_positive=False,
)
adaptation.adapt_vf_after_structural_stability(G, n_jobs=vf_jobs)
# Default integrators advance ``_t``. If a custom integrator does not,
# same-time EPI changes reset the history and cannot masquerade as flow.
_record_mutation_flow_boundary(G)
return _dt
def _update_epi_hist(G: TNFRGraph) -> None:
"""Maintain the rolling EPI history used by remeshing heuristics."""
append_remesh_epi_history_snapshot(G)
def _maybe_remesh(G: TNFRGraph) -> None:
"""Trigger remeshing when stability thresholds are satisfied."""
apply_remesh_if_globally_stable(G)
def _run_validators(G: TNFRGraph) -> None:
"""Execute registered validators ensuring canonical invariants hold."""
from ..validation import run_validators
run_validators(G)
def _run_after_callbacks(G, *, step_idx: int) -> None:
"""Notify ``AFTER_STEP`` observers with the latest structural metrics."""
h = ensure_history(G)
ctx = {"step": step_idx}
metric_pairs = [
("C", "C_steps"),
("stable_frac", "stable_frac"),
("phase_sync", "phase_sync"),
("glyph_disr", "glyph_load_disr"),
("Si_mean", "Si_mean"),
]
for dst, src in metric_pairs:
values = h.get(src)
if values:
ctx[dst] = values[-1]
callback_manager.invoke_callbacks(G, CallbackEvent.AFTER_STEP.value, ctx)
telemetry = G.graph.get("telemetry")
if isinstance(telemetry, MutableMapping):
history = telemetry.get("nu_f")
history_key = "nu_f_history"
if isinstance(history, list) and history_key not in telemetry:
telemetry[history_key] = history
payload = telemetry.get("nu_f_snapshot")
if isinstance(payload, Mapping):
bridge_raw = telemetry.get("nu_f_bridge")
try:
bridge = float(bridge_raw) if bridge_raw is not None else None
except (TypeError, ValueError):
bridge = None
nu_f_summary = {
"total_reorganisations": payload.get("total_reorganisations"),
"total_duration": payload.get("total_duration"),
"rate_hz_str": payload.get("rate_hz_str"),
"rate_hz": payload.get("rate_hz"),
"variance_hz_str": payload.get("variance_hz_str"),
"variance_hz": payload.get("variance_hz"),
"confidence_level": payload.get("confidence_level"),
"ci_hz_str": {
"lower": payload.get("ci_lower_hz_str"),
"upper": payload.get("ci_upper_hz_str"),
},
"ci_hz": {
"lower": payload.get("ci_lower_hz"),
"upper": payload.get("ci_upper_hz"),
},
"bridge": bridge,
}
telemetry["nu_f"] = nu_f_summary
math_summary = telemetry.get("math_engine")
if isinstance(math_summary, MutableMapping):
math_summary["nu_f"] = dict(nu_f_summary)
def _get_math_engine_config(G: TNFRGraph) -> MutableMapping[str, Any] | None:
"""Return the mutable math-engine configuration stored on ``G``."""
cfg_raw = G.graph.get("MATH_ENGINE")
if not isinstance(cfg_raw, Mapping) or not cfg_raw.get("enabled", False):
return None
if isinstance(cfg_raw, MutableMapping):
return cfg_raw
cfg_mutable: MutableMapping[str, Any] = dict(cfg_raw)
G.graph["MATH_ENGINE"] = cfg_mutable
return cfg_mutable
def _initialise_math_state(
G: TNFRGraph,
cfg: MutableMapping[str, Any],
*,
hilbert_space: Any,
projector: BasicStateProjector,
) -> np.ndarray | None:
"""Project graph nodes into the Hilbert space to seed the math engine."""
dimension = getattr(hilbert_space, "dimension", None)
if dimension is None:
raise AttributeError("Hilbert space configuration is missing 'dimension'.")
vectors: list[np.ndarray] = []
for _, nd in G.nodes(data=True):
# The projector admits primitive types and the signed form chart.
# Preconversion or silently omitting a failed node would replace the
# supplied state/support with a different auxiliary preparation.
epi = get_attr(nd, ALIAS_EPI, 0.0, strict=True, conv=lambda raw: raw)
nu_f = get_attr(nd, ALIAS_VF, 0.0, strict=True, conv=lambda raw: raw)
theta_val = get_attr(nd, ALIAS_THETA, 0.0, strict=True, conv=lambda raw: raw)
vector = projector(epi=epi, nu_f=nu_f, theta=theta_val, dim=int(dimension))
vectors.append(np.asarray(vector, dtype=np.complex128))
if not vectors:
return None
stacked = np.vstack(vectors)
averaged = np.mean(stacked, axis=0)
atol = float(getattr(projector, "atol", 1e-9))
norm = float(getattr(hilbert_space, "norm")(averaged))
if np.isclose(norm, 0.0, atol=atol):
averaged = vectors[0]
norm = float(getattr(hilbert_space, "norm")(averaged))
if np.isclose(norm, 0.0, atol=atol):
return None
normalised = averaged / norm
cfg.setdefault("_state_origin", "projected")
return normalised
def _advance_math_engine(
G: TNFRGraph,
*,
dt: float,
step_idx: int,
hist: HistoryState,
) -> None:
"""Advance the optional math engine and record spectral telemetry."""
cfg = _get_math_engine_config(G)
if cfg is None:
return
if (
np is None
or MathematicalDynamicsEngine is None
or runtime_normalized is None
or runtime_spectral_threshold is None
):
raise RuntimeError(
"Mathematical dynamics require NumPy and tnfr.mathematics "
"extras to be installed."
)
from ..mathematics._complex_arrays import (
backend_complex_array,
nonnegative_tolerance,
)
# Validation consumes the supplied tolerance, not a float coercion that
# could turn booleans/text into a permissive certification threshold.
atol = nonnegative_tolerance(
cfg.get("atol", getattr(cfg.get("dynamics_engine"), "atol", 1e-9))
)
hilbert_space = cfg.get("hilbert_space")
spectral_operator = resolve_compatibility_value(
cfg.get("spectral_operator"),
cfg.get("coherence_operator"),
canonical_name="MATH_ENGINE['spectral_operator']",
legacy_name="MATH_ENGINE['coherence_operator']",
)
threshold_raw = resolve_compatibility_value(
cfg.get("spectral_expectation_threshold"),
cfg.get("coherence_threshold"),
canonical_name="MATH_ENGINE['spectral_expectation_threshold']",
legacy_name="MATH_ENGINE['coherence_threshold']",
)
if hilbert_space is None or spectral_operator is None or threshold_raw is None:
raise ValueError(
"MATH_ENGINE requires 'hilbert_space', 'spectral_operator' and "
"'spectral_expectation_threshold' entries."
)
spectral_operator = validate_spectral_operator(
spectral_operator,
dimension=getattr(hilbert_space, "dimension", None),
label="MATH_ENGINE spectral_operator",
)
spectral_threshold = finite_spectral_real(
threshold_raw,
label="MATH_ENGINE spectral expectation threshold",
)
# Materialize canonical keys and keep historical aliases synchronized.
cfg["spectral_operator"] = spectral_operator
cfg["spectral_expectation_threshold"] = spectral_threshold
cfg["coherence_operator"] = spectral_operator
cfg["coherence_threshold"] = spectral_threshold
metadata_provenance = cfg.get(
"provenance", "tnfr.dynamics.runtime._advance_math_engine"
)
cfg.update(spectral_expectation_metadata(provenance=metadata_provenance))
if BasicStateProjector is None: # pragma: no cover - guarded by import above
raise RuntimeError(
"Mathematical dynamics require the BasicStateProjector helper."
)
projector = cfg.get("state_projector")
if not isinstance(projector, BasicStateProjector):
projector = BasicStateProjector()
cfg["state_projector"] = projector
engine = cfg.get("dynamics_engine")
if not isinstance(engine, MathematicalDynamicsEngine):
generator = cfg.get("generator_matrix")
if generator is None:
raise ValueError(
"MATH_ENGINE requires either a 'dynamics_engine' instance or a "
"'generator_matrix'."
)
engine = MathematicalDynamicsEngine(generator, hilbert_space=hilbert_space)
cfg["dynamics_engine"] = engine
state_vector = cfg.get("_state_vector")
if state_vector is None:
state_vector = _initialise_math_state(
G,
cfg,
hilbert_space=hilbert_space,
projector=projector,
)
if state_vector is None:
return
else:
state_vector = backend_complex_array(
state_vector, backend=engine.backend, label="MATH_ENGINE state vector"
)
dimension = getattr(hilbert_space, "dimension", state_vector.shape[0])
if state_vector.shape != (int(dimension),):
state_vector = _initialise_math_state(
G,
cfg,
hilbert_space=hilbert_space,
projector=projector,
)
if state_vector is None:
return
advanced = engine.step(state_vector, dt=float(dt), normalize=True)
cfg["_state_vector"] = advanced
# Observation adapters may use a different backend than the engine. Export
# explicitly; NumPy cannot implicitly coerce a retained CUDA tensor.
observed_state = engine.backend.to_numpy(advanced)
label = f"step[{step_idx}]"
normalized_passed, norm_value = runtime_normalized(
observed_state,
hilbert_space,
atol=atol,
label=label,
)
expectation_passed, expectation_value = runtime_spectral_threshold(
observed_state,
spectral_operator,
spectral_threshold,
normalise=False,
atol=atol,
label=label,
)
frequency_operator = cfg.get("frequency_operator")
frequency_summary: dict[str, Any] | None = None
if frequency_operator is not None:
if runtime_frequency_positive is None: # pragma: no cover - guarded above
raise RuntimeError(
"Frequency positivity checks require tnfr.mathematics extras."
)
freq_raw = runtime_frequency_positive(
observed_state,
frequency_operator,
normalise=False,
enforce=True,
atol=atol,
label=label,
)
frequency_summary = {
"passed": bool(freq_raw.get("passed", False)),
"value": float(freq_raw.get("value", 0.0)),
"projection_passed": bool(freq_raw.get("projection_passed", False)),
"spectrum_psd": bool(freq_raw.get("spectrum_psd", False)),
"enforced": bool(freq_raw.get("enforce", True)),
}
if "spectrum_min" in freq_raw:
frequency_summary["spectrum_min"] = float(freq_raw.get("spectrum_min", 0.0))
expectation = spectral_expectation_payload(
value=expectation_value,
threshold=spectral_threshold,
passed=expectation_passed,
provenance="tnfr.dynamics.runtime._advance_math_engine",
operator=spectral_operator,
)
summary = {
"step": step_idx,
"normalized": bool(normalized_passed),
"norm": float(norm_value),
"spectral_operator_expectation": expectation,
# Historical result key retained as the same explicitly scoped payload.
"coherence": expectation,
"frequency": frequency_summary,
}
hist.setdefault("math_engine_summary", []).append(summary)
hist.setdefault("math_engine_norm", []).append(summary["norm"])
hist.setdefault("math_engine_normalized", []).append(summary["normalized"])
hist.setdefault("math_engine_spectral_operator_expectation", []).append(
expectation["value"]
)
hist.setdefault("math_engine_spectral_expectation_passed", []).append(
expectation["passed"]
)
# Historical streams mirror the auxiliary spectral values only.
hist.setdefault("math_engine_coherence", []).append(expectation["value"])
hist.setdefault("math_engine_coherence_passed", []).append(expectation["passed"])
if frequency_summary is None:
hist.setdefault("math_engine_frequency", []).append(None)
hist.setdefault("math_engine_frequency_passed", []).append(None)
hist.setdefault("math_engine_frequency_projection_passed", []).append(None)
else:
hist.setdefault("math_engine_frequency", []).append(frequency_summary["value"])
hist.setdefault("math_engine_frequency_passed", []).append(
frequency_summary["passed"]
)
hist.setdefault("math_engine_frequency_projection_passed", []).append(
frequency_summary["projection_passed"]
)
cfg["last_summary"] = summary
telemetry = G.graph.setdefault("telemetry", {})
telemetry["math_engine"] = deepcopy(summary)
def step(
G: TNFRGraph,
*,
dt: float | None = None,
use_Si: bool = True,
apply_glyphs: bool = True,
n_jobs: Mapping[str, Any] | None = None,
) -> None:
"""Advance the runtime one ΔNFR step updating νf, phase and glyphs.
Parameters
----------
G : TNFRGraph
Graph whose nodes store EPI, νf and phase metadata. The graph must