-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstructural_diffusion.py
More file actions
2225 lines (1908 loc) · 83.3 KB
/
Copy pathstructural_diffusion.py
File metadata and controls
2225 lines (1908 loc) · 83.3 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
r"""TNFR Structural Diffusion — the transport content of the nodal equation.
This module makes explicit, and verifies, that the TNFR nodal equation
∂EPI/∂t = νf · ΔNFR(t)
is **structurally a diffusion equation on the network**. This is not an
analogy imported from another paradigm: it is the literal content of the
canonical ΔNFR computation.
THE NODAL EQUATION IS GRAPH DIFFUSION
=====================================
The canonical ΔNFR (:func:`tnfr.dynamics.default_compute_delta_nfr`) is a
weighted sum of *neighbour-mean-minus-self* gradients, one per structural
channel (see :mod:`tnfr.dynamics.dnfr`):
g_epi(i) = mean_{j∈N(i)} EPI(j) − EPI(i)
g_phase(i) = −angle_diff(θ(i), mean θ neighbours) / π
g_vf(i) = mean νf(neighbours) − νf(i)
g_topo(i) = mean deg(neighbours) − deg(i)
Each ``neighbour-mean − self`` term is exactly the action of the
**random-walk graph Laplacian** L_rw = I − D⁻¹W on that field:
g_epi = −(L_rw · EPI) (verified to machine precision).
So the EPI channel of the nodal equation is
∂EPI/∂t = νf · ΔNFR_epi = −νf · L_rw · EPI,
i.e. the **discrete diffusion (heat) equation** with diffusivity νf. The
structural form EPI spreads across the network exactly as heat or a
concentration diffuses; ΔNFR is the diffusive gradient (the structural
pressure) driving the flux, and νf is the mobility / diffusivity.
WHAT EMERGES (empirically-grounded, in TNFR's own terms)
========================================================
- **Structural diffusion** (EPI channel): the form relaxes to a uniform
field; each Laplacian eigenmode decays as exp(−νf·λ_k·t); the slowest
rate is set by the spectral gap λ₂ (the Fiedler value).
- **Conserved structural total**: the random-walk Laplacian conserves the
**degree-weighted total** Σ_i deg(i)·EPI(i) (its left null vector is the
degree vector), the analogue of the conserved amount of diffusing
substance.
- **Equilibrium ⟺ no gradients**: ΔNFR = 0 ⟺ the field is uniform across
neighbourhoods — the diffusive steady state.
- **Synchronization** (phase channel): the phase term aligns θ to the
neighbour mean, driving Kuramoto-type synchronization (R → 1).
These are the registers whose existence is established by the strictest
empirical method — diffusion (Fourier 1822, Fick 1855, Einstein 1905) and
synchronization (Kuramoto; observed in fireflies, pacemaker cells, neurons,
Josephson junctions). They are reproduced here as the **same mathematics**
(the graph Laplacian is the discrete diffusion operator), not as a
metaphor.
THE MECHANICAL REGIME IS OVERDAMPED DRIFT (not inertial)
========================================================
Because the nodal equation is **first order in time**, the mechanical
regime it produces directly is the **overdamped drift law**, not Newtonian
inertia. Reading EPI as a position-like coordinate q and ΔNFR as the
structural pressure F, the nodal equation is
q̇ = νf · F,
i.e. **velocity proportional to applied force**, with νf the **mobility**.
Under a sustained structural pressure the field drifts at *constant*
velocity (linear in time), it does **not** accelerate. This is the
empirically-demonstrated mobility / drift law — Stokes drag (1851),
Einstein's mobility relation (1905), terminal velocity, sedimentation,
electrophoresis — where νf is the mobility, NOT an inverse inertial mass.
The **inertial** Newtonian regime (second order, q̈ = F/m, oscillation)
is a *different* structure: it lives in the conservative **symplectic
substrate** Hamiltonian flow (:mod:`tnfr.physics.symplectic_substrate`,
where the flow is q̈ = −q per conjugate pair). The bare nodal equation is
the **overdamped projection** of that substrate flow. So:
bare nodal equation (1st order) → overdamped drift v = νf·F
symplectic substrate (2nd order) → inertial oscillation q̈ = −∂V/∂q
both empirically grounded, but distinct regimes — a single first-order
nodal equation cannot, by itself, be Newton's second law.
DISCRETE MODES ARE THE BOUNDED-MANIFOLD STANDING WAVES
======================================================
On a **bounded** structural manifold (a finite graph) the diffusion
operator has a **discrete** spectrum of eigenmodes — the same structure as
the discrete harmonics of a bounded vibrating medium. The symmetric
normalized Laplacian L_sym = I − D^{-1/2} W D^{-1/2} shares the diffusion
operator L_rw's spectrum {λ_k} but has **orthonormal** eigenvectors v_k:
- **Discrete spectrum**: a finite manifold supports a finite, discrete set
of eigenvalues {λ_k} (not a continuum) — the structural origin of
"discrete modes". λ_1 = 0 is the uniform mode (the conserved diffusion
mode); λ_2 (the spectral gap) is the first non-trivial mode.
- **Standing-wave shapes**: the eigenvectors v_k are orthonormal standing
waves. On a path graph they are exactly the cosine standing waves of a
vibrating string (overlap 1.0 to machine precision).
- **Nodal-domain ordering** (Courant): the number of sign changes (nodal
domains) grows with the mode index k — the structural "mode number" k
emerges from the bounded geometry, not from a postulate.
- **Two time-regimes, same modes**: under diffusion (first order) mode k
relaxes as exp(−νf·λ_k·t); under the wave/substrate flow (second order)
it oscillates at the standing-wave frequency ω_k = √λ_k.
This is the discrete-harmonic structure of a bounded elastic medium —
vibrating strings (Pythagoras), Chladni plate modes (1787), molecular
vibrational spectra — all established by the strictest empirical method.
The discreteness is a consequence of the **bounded structural geometry**,
not an imported quantum postulate.
STRUCTURAL STABILITY: THE DISPERSION RELATION
=============================================
The growth or decay of each structural eigenmode under diffusion plus a
local reaction rate r is governed by the **dispersion relation**
σ_k = r − νf · λ_k,
the universal linear-stability law (the same tool that governs every
instability and pattern-forming system — convective instability, the
onset of pattern formation). From the canonical Laplacian spectrum {λ_k}:
- **Pure diffusion** (r = 0): σ_k = −νf·λ_k ≤ 0, so every non-uniform mode
*decays* — structural equilibrium is stable, the integral ∫νf·ΔNFR dt
converges. This is the linear-stability content of "diffusion relaxes
to uniform".
- **Structural instability threshold** r_c = νf·λ_2 (the spectral gap, the
Fiedler value, times the diffusivity). For 0 < r < r_c only the uniform
mode grows (global amplification, no spatial structure); for r > r_c the
**Fiedler mode** (k = 1) also grows — the first *structural* pattern.
- **The first structural pattern is the Fiedler partition**: the Fiedler
eigenvector splits the network along its **weakest structural cut** (the
two most weakly-connected communities) — the empirically-validated
spectral-clustering result.
- **U2 grammar, spectrally**: a destabilizing reaction raises r, a
stabilizer lowers it; bounded evolution (U2) ⟺ keeping r below r_c.
Above r_c the Fiedler mode grows unboundedly → fragmentation (the
U2-violation the grammar prevents).
The reaction rate r is a generic local rate; in TNFR the operators supply
it (stabilizers lower r, destabilizers raise it). A *two-channel*
structural diffusion with **differential diffusivity** and an
activator–inhibitor coupling supports a finite-wavelength (Turing)
instability — the empirically-demonstrated pattern-formation mechanism
(Belousov–Zhabotinsky, morphogenesis); those kinetics are a model input,
not TNFR-derived, so only the dispersion-relation mechanism is certified
here.
THE STRUCTURAL RANDOM WALK AND RESISTANCE GEOMETRY
==================================================
The diffusion operator is **literally the generator of a random walk** on
the network: L_rw = I − D^{-1}W = I − P, where P = D^{-1}W is the
random-walk transition matrix (verified exactly). So the structural
transport is **Brownian motion on the network** — the empirically-
demonstrated random walk (Einstein 1905, Perrin 1908, the proof of atoms):
- **Stationary distribution ∝ degree**: the random walk converges to
π_i = deg(i) / Σ deg — exactly the **degree-weighted total** the
diffusion conserves. The conserved quantity *is* the equilibrium
measure.
- **Effective resistance** (Ohm's law): treating the network as a
resistor network (the combinatorial Laplacian L = D − W is the
conductance matrix — Kirchhoff 1847), the effective resistance
R_eff(i,j) = L⁺_ii + L⁺_jj − 2L⁺_ij (L⁺ the pseudoinverse) is a
**transport metric** (symmetric, non-negative, triangle inequality) —
the structural "difficulty of transport" between two nodes.
- **Commute time = 2m·R_eff**: the expected round-trip time of the random
walk between two nodes equals 2m times the effective resistance (m the
number of edges) — the exact link between the diffusion random walk and
the resistance geometry (Chandra et al. 1996), confirmed against
Monte-Carlo walks.
These are the same mathematics as Brownian motion (random walk) and
electrical networks (Ohm/Kirchhoff resistance) — both established by the
strictest empirical method.
THE STRUCTURAL FLOW: CURRENT, KIRCHHOFF, AND CONTINUITY
======================================================
The transport carries a **structural current**: the diffusion edge current
J_ij = EPI_i − EPI_j (Fick's law — flux from high to low, antisymmetric).
Its node-level balance is **Kirchhoff's current law**, which *is* the
discrete continuity equation:
div(J)(i) = Σ_{j∼i} J_ij = (L·EPI)(i),
so the net outflow at a node equals the combinatorial Laplacian acting on
EPI. Hence the diffusion continuity equation ∂EPI/∂t + div(J) = 0 holds,
and for a closed network (no sources) the total flux balances, Σ_i div(J)
= 0 (L has zero column sums — the structural-conservation analogue here).
Under an injected unit current from s to t the induced potential drop is
the **effective resistance** R_eff(s,t) (Ohm's law) — tying the current to
the resistance geometry above. These are Fick diffusion, Kirchhoff's
circuit laws, and Ohm's law — all empirically demonstrated.
This is the EPI-channel current; it complements the tetrad-field
continuity of :mod:`tnfr.physics.conservation` (which tracks the charge
ρ = Φ_s + K_φ and the current J = (J_φ, J_ΔNFR)).
HONEST SCOPE
============
- The identity ΔNFR_epi = −L_rw·EPI is EXACT (machine precision), a
mathematical fact about the canonical ΔNFR.
- The full ΔNFR is multi-channel: EPI **diffusion** + phase
**synchronization** + νf/topology **homogenization**. This module
isolates and certifies the diffusion (EPI) channel and reports the
synchronization channel qualitatively.
- **λ₂ is topological, NOT tied to the canonical constants.** The spectral
gap λ₂ (which governs relaxation, stability, and the instability
threshold) is a purely spectral/topological quantity — determined by N,
degree, and connectivity (e.g. ring λ₂ = 1 − cos(2π/N), complete-graph
λ₂ = n/(n−1)). The overlay threshold scales of the tetrad fields do
**not** enter the Laplacian spectrum; any numerical proximity is
coincidental (the 2π/N in a ring is a geometric polygon angle, not a
structural scale). Measured
negative result — do not assert a λ₂ ↔ constant relation.
- This characterises the transport content of the nodal dynamics; it does
not, by itself, resolve any open program (Riemann G4, Navier–Stokes).
References
----------
- :mod:`tnfr.dynamics.dnfr` — the canonical ΔNFR neighbour-mean gradients
- :func:`tnfr.observers.kuramoto_order` — the synchronization order R
- :mod:`tnfr.physics.conservation` — the structural continuity equation
- AGENTS.md §"Foundational Physics" — the nodal equation
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from ..alias import get_attr
from ..constants.aliases import ALIAS_DNFR, ALIAS_EPI, ALIAS_VF
from ..mathematics.unified_numerical import np
__all__ = [
"StructuralDiffusionCertificate",
"OverdampedRegimeCertificate",
"OverdampedProjectionCertificate",
"UndampedLimitCertificate",
"DiscreteModeCertificate",
"StructuralStabilityCertificate",
"RandomWalkCertificate",
"StructuralFlowCertificate",
"structural_diffusion_operator",
"symmetric_normalized_laplacian",
"structural_field",
"structural_diffusivity",
"relaxation_spectrum",
"structural_frequency_rank",
"degree_weighted_total",
"structural_eigenmodes",
"nodal_domain_count",
"compute_emergent_pulse",
"compute_nodal_pulse",
"dispersion_relation",
"instability_threshold",
"fiedler_partition",
"random_walk_matrix",
"stationary_distribution",
"effective_resistance",
"commute_time",
"structural_current",
"current_divergence",
"verify_structural_diffusion",
"verify_overdamped_regime",
"damped_wave_rates",
"verify_overdamped_projection",
"verify_undamped_limit",
"verify_discrete_modes",
"verify_structural_stability",
"verify_structural_random_walk",
"verify_structural_flow",
]
def _ordered_nodes(G: Any) -> list:
"""Stable node ordering for the matrix representation."""
return list(G.nodes())
def structural_diffusion_operator(G: Any) -> tuple[list, Any]:
r"""Return the random-walk graph Laplacian L_rw = I − D⁻¹W.
This is the operator whose action on a field is exactly the canonical
ΔNFR ``neighbour-mean − self`` gradient: g = −L_rw·field. Built from
the (optionally weighted) adjacency; isolated nodes (degree 0) get a
zero row (no diffusion).
Parameters
----------
G : TNFRGraph
Returns
-------
(nodes, L_rw) : tuple[list, np.ndarray]
The node ordering and the N×N random-walk Laplacian.
"""
nodes = _ordered_nodes(G)
index = {n: i for i, n in enumerate(nodes)}
n = len(nodes)
lap = np.zeros((n, n), dtype=float)
for node in nodes:
i = index[node]
neigh = list(G.neighbors(node))
if not neigh:
continue
# weighted degree (weight defaults to 1.0 when absent)
weights = [float(G[node][m].get("weight", 1.0)) for m in neigh]
deg = sum(weights)
if deg <= 0.0:
continue
lap[i, i] = 1.0
for m, w in zip(neigh, weights):
lap[i, index[m]] -= w / deg
return nodes, lap
def symmetric_normalized_laplacian(
G: Any, nodes: list | None = None
) -> tuple[list, Any]:
r"""Return the symmetric normalized Laplacian L_sym = I − D^{-1/2} W D^{-1/2}.
L_sym shares the spectrum of the canonical diffusion operator
L_rw = I − D⁻¹W (:func:`structural_diffusion_operator`) but is symmetric, so
it has an orthonormal eigenbasis and real eigenvalues — the canonical choice
for the relaxation spectrum (its λ₂ is the structural ``diffusion_gap``).
Isolated nodes (degree 0) get a zero row.
Parameters
----------
G : TNFRGraph
nodes : list, optional
Node ordering; defaults to the stable ``list(G.nodes())`` order.
Returns
-------
(nodes, L_sym) : tuple[list, np.ndarray]
The node ordering and the N×N symmetric normalized Laplacian.
"""
if nodes is None:
nodes = _ordered_nodes(G)
index = {nd: i for i, nd in enumerate(nodes)}
n = len(nodes)
deg = np.zeros(n, dtype=float)
for node in nodes:
deg[index[node]] = sum(
float(G[node][m].get("weight", 1.0)) for m in G.neighbors(node)
)
# Compute D^{-1/2} only on connected nodes; isolated nodes (deg = 0) keep
# d_inv_sqrt = 0. Masked assignment avoids evaluating 1/sqrt(0) (which the
# np.where form does for every entry before selecting, emitting a warning).
d_inv_sqrt = np.zeros(n, dtype=float)
positive = deg > 0.0
d_inv_sqrt[positive] = 1.0 / np.sqrt(deg[positive])
lap = np.zeros((n, n), dtype=float)
for node in nodes:
i = index[node]
if deg[i] <= 0.0:
continue
lap[i, i] = 1.0
for m in G.neighbors(node):
j = index[m]
w = float(G[node][m].get("weight", 1.0))
lap[i, j] -= w * d_inv_sqrt[i] * d_inv_sqrt[j]
return nodes, lap
def structural_field(G: Any, nodes: list | None = None) -> Any:
r"""Return the EPI field as a vector aligned with ``nodes``."""
if nodes is None:
nodes = _ordered_nodes(G)
return np.array(
[float(get_attr(G.nodes[n], ALIAS_EPI, 0.0)) for n in nodes],
dtype=float,
)
def structural_diffusivity(G: Any) -> float:
r"""Mean structural frequency νf — the diffusion coefficient (mobility).
In ∂EPI/∂t = −νf·L_rw·EPI, νf plays the role of the diffusivity: the
larger the structural frequency, the faster the form spreads.
"""
nodes = _ordered_nodes(G)
vf = [float(get_attr(G.nodes[n], ALIAS_VF, 0.0)) for n in nodes]
return float(np.mean(vf)) if vf else 0.0
def degree_weighted_total(G: Any) -> float:
r"""The conserved structural total Σ_i deg(i)·EPI(i).
The random-walk Laplacian conserves the degree-weighted total (its left
null vector is the degree vector), the analogue of the conserved amount
of a diffusing substance.
This is the **EPI-channel** conserved quantity (of the diffusion
∂EPI/∂t = −νf·L_rw·EPI). It is **distinct** from the tetrad Noether charge
Q = Σ(Φ_s + K_φ)
(:func:`tnfr.physics.conservation.compute_noether_charge`), conserved under
grammar U1–U6: TNFR carries two distinct conservation laws, on the EPI
field and on the tetrad fields respectively (see
STRUCTURAL_CONSERVATION_THEOREM §8.7).
"""
nodes = _ordered_nodes(G)
total = 0.0
for node in nodes:
neigh = list(G.neighbors(node))
deg = sum(float(G[node][m].get("weight", 1.0)) for m in neigh)
total += deg * float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
return float(total)
def relaxation_spectrum(G: Any) -> Any:
r"""Diffusion relaxation rates νf·λ_k (sorted ascending).
The eigenvalues λ_k of the random-walk Laplacian L_rw scaled by the
diffusivity νf give the decay rates of the diffusion eigenmodes:
mode k relaxes as exp(−νf·λ_k·t). λ₁ = 0 (the conserved uniform mode);
λ₂ (the spectral gap / Fiedler value) sets the slowest relaxation.
Returns
-------
np.ndarray
The rates νf·λ_k sorted ascending (real parts).
"""
# L_sym and L_rw share the spectrum; reuse the cached symmetric
# eigendecomposition (ascending, clipped >= 0). The rates are nu_f*lambda_k.
eig, _ = _cached_eigh(G)
return structural_diffusivity(G) * eig
def structural_frequency_rank(G: Any, decimals: int = 8) -> int:
r"""Number of distinct structural frequencies (the structural rank).
The distinct eigenvalues of the canonical random-walk Laplacian L_rw are
the network's structural frequencies (the relaxation rates of
∂EPI/∂t = −νf·L_rw·EPI, up to the νf scale). This returns their count s(G)
— the size of the distinct-frequency spectrum — complementing
``relaxation_spectrum`` (which returns the rates themselves).
For a connected graph, s distinct eigenvalues bound the diameter by s−1,
and s = 2 iff the graph is complete (regular case). On arithmetic Cayley
networks the rank is a primality / cyclotomy diagnostic (see
:mod:`tnfr.mathematics.number_theory`): the quadratic-residue network on an
odd prime has rank 3, and the k-th power residue network on a prime p has
rank ``gcd(k, p-1) + 1``.
Note
----
For large or dense graphs the distinct-eigenvalue count is sensitive to the
``decimals`` rounding (floating-point noise in ``eigvals`` can split truly
equal eigenvalues). For arithmetic residue networks the exact multiplicative
:func:`tnfr.mathematics.number_theory.quadratic_residue_annotated_rank` is
the robust closed-form object; this scalar count agrees with it for small
moduli.
Parameters
----------
G : TNFRGraph
decimals : int
Rounding applied to the real and imaginary parts before counting
distinct values (the spectrum may be complex for directed graphs).
Returns
-------
int
The number of distinct eigenvalues of L_rw.
"""
_, lap = structural_diffusion_operator(G)
eig = np.linalg.eigvals(lap)
rounded = np.round(eig.real, decimals) + 1j * np.round(eig.imag, decimals)
return int(np.unique(rounded).size)
@dataclass(frozen=True)
class StructuralDiffusionCertificate:
r"""Verification that the nodal equation's EPI channel is graph diffusion.
Attributes
----------
n_nodes : int
dnfr_is_graph_laplacian : bool
The canonical ΔNFR (EPI channel) equals −L_rw·EPI.
max_laplacian_residual : float
Max |ΔNFR_epi − (−L_rw·EPI)| over the nodes (≈ 0).
diffusivity : float
Mean νf (the diffusion coefficient / mobility).
spectral_gap : float
λ₂ of L_rw (the Fiedler value); sets the slowest relaxation.
slowest_relaxation_rate : float
νf·λ₂ — the slowest diffusion decay rate.
degree_weighted_conserved : bool
Σ deg·EPI is conserved under the diffusion flow.
max_conservation_drift : float
Max drift of the degree-weighted total over the sampled flow.
relaxes_to_uniform : bool
The field relaxes to a spatially uniform diffusive equilibrium.
final_field_std : float
Std of the field after the sampled diffusion flow (≈ 0).
"""
n_nodes: int
dnfr_is_graph_laplacian: bool
max_laplacian_residual: float
diffusivity: float
spectral_gap: float
slowest_relaxation_rate: float
degree_weighted_conserved: bool
max_conservation_drift: float
relaxes_to_uniform: bool
final_field_std: float
@property
def is_valid_diffusion(self) -> bool:
"""True when the nodal EPI channel verifies as graph diffusion."""
return (
self.dnfr_is_graph_laplacian
and self.degree_weighted_conserved
and self.relaxes_to_uniform
)
def summary(self) -> str:
"""Human-readable one-line verdict."""
ok = "VALID" if self.is_valid_diffusion else "INVALID"
return (
f"Structural diffusion [{ok}]: "
f"ΔNFR_epi = −L_rw·EPI={self.dnfr_is_graph_laplacian} "
f"(res {self.max_laplacian_residual:.1e}), "
f"diffusivity νf={self.diffusivity:.4f}, "
f"spectral gap λ₂={self.spectral_gap:.4f}, "
f"slowest rate νf·λ₂={self.slowest_relaxation_rate:.4f}, "
f"deg-weighted conserved={self.degree_weighted_conserved} "
f"(drift {self.max_conservation_drift:.1e}), "
f"relaxes to uniform={self.relaxes_to_uniform} "
f"(final std {self.final_field_std:.1e})"
)
def _dnfr_epi_channel(G: Any, nodes: list) -> Any:
r"""Canonical ΔNFR restricted to the EPI channel, on a clean replica.
Isolates the EPI diffusion channel by computing the canonical ΔNFR with
weights (phase=0, epi=1, vf=0, topo=0) on a minimal structural replica
(nodes + edges + EPI/θ/νf only), so the caller's graph is never mutated
and the non-copyable runtime caches are not duplicated.
"""
from ..dynamics import default_compute_delta_nfr
g2 = G.__class__()
for node in nodes:
data = G.nodes[node]
g2.add_node(
node,
EPI=float(get_attr(data, ALIAS_EPI, 0.0)),
theta=float(data.get("theta", 0.0)),
nu_f=float(get_attr(data, ALIAS_VF, 0.0)),
)
for u, v, data in G.edges(data=True):
g2.add_edge(u, v, weight=float(data.get("weight", 1.0)))
g2.graph["DNFR_WEIGHTS"] = {
"phase": 0.0,
"epi": 1.0,
"vf": 0.0,
"topo": 0.0,
}
default_compute_delta_nfr(g2)
return np.array(
[float(get_attr(g2.nodes[n], ALIAS_DNFR, 0.0)) for n in nodes],
dtype=float,
)
def verify_structural_diffusion(
G: Any,
*,
dt: float = 0.1,
steps: int = 400,
tolerance: float = 1e-9,
) -> StructuralDiffusionCertificate:
r"""Verify the nodal equation's EPI channel is graph diffusion.
Confirms (1) the canonical ΔNFR EPI channel equals −L_rw·EPI to machine
precision, (2) the degree-weighted total is conserved under the
diffusion flow, and (3) the field relaxes to a uniform diffusive
equilibrium; and reports the diffusivity νf and the relaxation spectrum.
The caller's graph is never mutated (the ΔNFR check runs on a copy).
Parameters
----------
G : TNFRGraph
dt : float
Forward-Euler step for the diffusion-flow checks.
steps : int
Number of diffusion steps for the relaxation / conservation checks.
tolerance : float
Maximum allowed Laplacian residual and conservation drift.
Returns
-------
StructuralDiffusionCertificate
"""
nodes, lap = structural_diffusion_operator(G)
n = len(nodes)
epi = structural_field(G, nodes)
# (1) ΔNFR (epi channel) == −L_rw·EPI ?
try:
dnfr_epi = _dnfr_epi_channel(G, nodes)
residual = float(np.max(np.abs(dnfr_epi - (-(lap @ epi)))))
is_laplacian = residual < max(tolerance, 1e-12)
except Exception:
residual = float("nan")
is_laplacian = False
# diffusivity and spectrum
diffusivity = structural_diffusivity(G)
eig = np.linalg.eigvals(lap).real
eig.sort()
spectral_gap = float(eig[1]) if n > 1 else 0.0
slowest_rate = diffusivity * spectral_gap
# degree vector for the conserved weighted total
deg = np.array(
[
sum(float(G[node][m].get("weight", 1.0)) for m in G.neighbors(node))
for node in nodes
],
dtype=float,
)
# (2)+(3) integrate the pure diffusion flow e ← e − dt·L_rw·e
e = epi.copy()
w0 = float(deg @ e)
max_drift = 0.0
for _ in range(steps):
e = e - dt * (lap @ e)
max_drift = max(max_drift, abs(float(deg @ e) - w0))
conserved = max_drift < max(tolerance, 1e-9 * (abs(w0) + 1e-12))
final_std = float(np.std(e))
relaxes = final_std < max(1e-3, 1e-2 * float(np.std(epi) + 1e-12))
return StructuralDiffusionCertificate(
n_nodes=n,
dnfr_is_graph_laplacian=is_laplacian,
max_laplacian_residual=residual,
diffusivity=diffusivity,
spectral_gap=spectral_gap,
slowest_relaxation_rate=slowest_rate,
degree_weighted_conserved=conserved,
max_conservation_drift=max_drift,
relaxes_to_uniform=relaxes,
final_field_std=final_std,
)
# ---------------------------------------------------------------------------
# The overdamped drift regime: the bare nodal equation is first-order
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class OverdampedRegimeCertificate:
r"""Verification that the bare nodal equation is the overdamped drift law.
The nodal equation ∂EPI/∂t = νf·ΔNFR is **first order in time**, so —
reading EPI as a position q and ΔNFR as the structural pressure F — it
is the **mobility / drift law** q̇ = νf·F: velocity proportional to
force, with νf the mobility. Under a sustained pressure the field
drifts at *constant* velocity (linear in time), it does not accelerate.
This is the empirically-demonstrated overdamped regime (Stokes 1851,
Einstein 1905, terminal velocity, sedimentation, electrophoresis). The
inertial Newtonian regime (q̈ = F/m, second order) is the separate
:mod:`tnfr.physics.symplectic_substrate` Hamiltonian flow; the nodal
equation is its overdamped projection.
Attributes
----------
drift_velocity : float
v = νf·F evaluated at the reference (νf, F).
velocity_is_constant : bool
Under sustained pressure, dEPI/dt is constant (first-order/drift).
max_velocity_variation : float
Max |dEPI/dt − v| over the held-pressure integration (≈ 0).
position_linear_in_time : bool
EPI(t) grows linearly (slope = drift), not quadratically.
position_slope : float
Measured slope of EPI(t) (= the drift velocity).
mobility_linear_in_nu_f : bool
v ∝ νf (the mobility law): v/νf is constant across νf.
drift_linear_in_pressure : bool
v ∝ F: v/F is constant across F.
is_second_order : bool
Whether the bare equation is second order (always False — it is the
overdamped, first-order regime).
"""
drift_velocity: float
velocity_is_constant: bool
max_velocity_variation: float
position_linear_in_time: bool
position_slope: float
mobility_linear_in_nu_f: bool
drift_linear_in_pressure: bool
is_second_order: bool
@property
def is_overdamped_drift(self) -> bool:
"""True when the bare nodal equation verifies as overdamped drift."""
return (
self.velocity_is_constant
and self.position_linear_in_time
and self.mobility_linear_in_nu_f
and self.drift_linear_in_pressure
and not self.is_second_order
)
def summary(self) -> str:
"""Human-readable one-line verdict."""
ok = "VALID" if self.is_overdamped_drift else "INVALID"
return (
f"Overdamped drift regime [{ok}]: "
f"q̇ = νf·F = {self.drift_velocity:.4f} "
f"(mobility law); "
f"velocity constant={self.velocity_is_constant} "
f"(var {self.max_velocity_variation:.1e}), "
f"position linear={self.position_linear_in_time} "
f"(slope {self.position_slope:.4f}), "
f"v∝νf={self.mobility_linear_in_nu_f}, "
f"v∝F={self.drift_linear_in_pressure}, "
f"second-order={self.is_second_order}"
)
def verify_overdamped_regime(
*,
nu_f: float = 0.7,
pressure: float = 1.3,
dt: float = 0.01,
steps: int = 300,
tolerance: float = 1e-9,
) -> OverdampedRegimeCertificate:
r"""Verify the bare nodal equation is the overdamped drift law q̇ = νf·F.
Integrates the canonical nodal equation
(:func:`tnfr.dynamics.canonical.compute_canonical_nodal_derivative`)
under a *sustained* structural pressure and measures that the EPI
coordinate drifts at constant velocity v = νf·F (first-order, mobility
law), linear in νf (mobility) and in the pressure F. Uses the canonical
nodal-equation function — no formula is re-implemented here.
Parameters
----------
nu_f : float
Reference structural frequency (mobility).
pressure : float
Sustained structural pressure ΔNFR (= F).
dt : float
Integration step.
steps : int
Number of integration steps.
tolerance : float
Maximum allowed velocity variation / linearity residual.
Returns
-------
OverdampedRegimeCertificate
"""
from ..dynamics.canonical import compute_canonical_nodal_derivative
# integrate the bare nodal equation under a held pressure
epi = 0.0
velocities = []
positions = []
for _ in range(steps):
v = compute_canonical_nodal_derivative(nu_f, pressure).derivative
epi = epi + dt * v
velocities.append(v)
positions.append(epi)
vel = np.array(velocities, dtype=float)
pos = np.array(positions, dtype=float)
drift = float(vel[0])
vel_var = float(np.max(np.abs(vel - drift)))
vel_constant = vel_var < tolerance
# position grows linearly with slope = drift (first-order, not quadratic)
t = np.arange(steps, dtype=float) * dt
slope, _ = np.polyfit(t, pos, 1)
quad = np.polyfit(t, pos, 2)[0] # leading quadratic coefficient ≈ 0
pos_linear = abs(float(slope) - drift) < max(tolerance, 1e-6 * abs(drift)) and abs(
float(quad)
) < max(tolerance, 1e-6 * abs(drift) + 1e-9)
# mobility law: v ∝ νf (v/νf constant across νf)
ratios_nu = [
compute_canonical_nodal_derivative(nf, pressure).derivative / nf
for nf in (0.2, 0.5, 1.0, 1.5)
]
mobility_linear = float(np.std(ratios_nu)) < tolerance
# drift ∝ F (v/F constant across F)
ratios_f = [
compute_canonical_nodal_derivative(nu_f, f).derivative / f
for f in (0.3, 0.8, 1.3, 2.0)
]
pressure_linear = float(np.std(ratios_f)) < tolerance
return OverdampedRegimeCertificate(
drift_velocity=drift,
velocity_is_constant=vel_constant,
max_velocity_variation=vel_var,
position_linear_in_time=pos_linear,
position_slope=float(slope),
mobility_linear_in_nu_f=mobility_linear,
drift_linear_in_pressure=pressure_linear,
is_second_order=False,
)
# ---------------------------------------------------------------------------
# Overdamped projection: the bridge from the conservative substrate wave
# to the dissipative structural diffusion
# ---------------------------------------------------------------------------
def damped_wave_rates(G: Any, gamma: float) -> tuple[Any, Any, Any]:
r"""Per-mode slow/fast rates of the damped graph wave q̈ + γq̇ + Lq = 0.
The conservative symplectic substrate carries the graph **wave**
equation q̈ = −L q (second order, mode k oscillating at √λ_k — the
standing-wave ``discrete modes`` of :func:`verify_discrete_modes`).
Adding a damping γ gives the damped oscillator q̈ + γq̇ + L q = 0, whose
per-mode characteristic equation is
s² + γ s + λ_k = 0 ⟹ s± = ½(−γ ± √(γ² − 4λ_k)).
For γ² > 4λ_k (overdamped per mode) both roots are real: a **slow** root
s₋ → −λ_k/γ (the diffusion rate) and a **fast** root s₊ → −γ (a
transient that dies immediately). This is the spectral content of the
overdamped projection: after the fast transient, mode k relaxes at
λ_k/γ = ν_f·λ_k with ν_f = 1/γ.
Parameters
----------
G : TNFRGraph
gamma : float
Damping coefficient. Its inverse is the effective diffusivity
(mobility) ν_f = 1/γ.
Returns
-------
(lambdas, s_slow, s_fast) : tuple[np.ndarray, np.ndarray, np.ndarray]
Sorted Laplacian eigenvalues and the (real-part) slow/fast roots.
"""
_, lap = structural_diffusion_operator(G)
lambdas = np.sort(np.linalg.eigvals(lap).real)
lambdas = np.clip(lambdas, 0.0, None)
disc = gamma * gamma - 4.0 * lambdas + 0j
root = np.sqrt(disc)
s_slow = ((-gamma + root) / 2.0).real
s_fast = ((-gamma - root) / 2.0).real
return lambdas, s_slow, s_fast
@dataclass(frozen=True)
class OverdampedProjectionCertificate:
r"""Verification that structural diffusion is the overdamped projection
of the conservative symplectic-substrate wave flow.
The conservative substrate (:mod:`tnfr.physics.symplectic_substrate`)
carries the graph wave q̈ = −L q (second order). Damping it and taking
the strong-damping (Smoluchowski) limit collapses it onto the
first-order structural diffusion q̇ = −(1/γ) L q, with the
identification **ν_f = 1/γ** (structural frequency = inverse damping =
mobility). Both endpoints are canonical TNFR objects; this certificate
measures the bridge between them.
Attributes
----------
n_nodes : int
gamma : float
Damping coefficient used for the projection.
nu_f_effective : float
The effective diffusivity 1/γ recovered by the projection.
spectral_gap : float
λ₂ of L_rw (the Fiedler value).
lambda_max : float
Largest Laplacian eigenvalue (sets the bridge error scale).
max_rate_rel_error : float
Max over modes of |s_slow + λ_k/γ| / (λ_k/γ): how far the damped
slow rate is from the diffusion rate.
rate_error_times_gamma_sq : float
``max_rate_rel_error · γ²`` — converges to ≈ λ_max, confirming the
bridge error scales as O(λ_max/γ²).
slowest_slow_rate : float
Overdamped slow rate of the Fiedler mode, −s_slow(λ₂).
slowest_diffusion_rate : float
The diffusion spectral gap ν_f·λ₂ = λ₂/γ.
trajectory_max_rel_error : float
Max relative L² error between the damped-wave trajectory and the
diffusion trajectory exp(−L t/γ)·q₀ over an overdamped time window.
projects_to_diffusion : bool
Whether both the rate and trajectory errors fall within tolerance.
"""
n_nodes: int
gamma: float
nu_f_effective: float
spectral_gap: float
lambda_max: float
max_rate_rel_error: float
rate_error_times_gamma_sq: float
slowest_slow_rate: float
slowest_diffusion_rate: float
trajectory_max_rel_error: float
projects_to_diffusion: bool
@property
def is_valid_projection(self) -> bool:
"""True when the damped substrate wave projects onto diffusion."""
return self.projects_to_diffusion
def summary(self) -> str:
"""Human-readable one-line verdict."""
ok = "VALID" if self.is_valid_projection else "INVALID"
return (
f"Overdamped projection [{ok}]: damped substrate wave "
f"projects onto diffusion with nu_f=1/gamma="
f"{self.nu_f_effective:.4f}; rate error "
f"{self.max_rate_rel_error:.2e} (x gamma^2="
f"{self.rate_error_times_gamma_sq:.3f} ~ lambda_max="
f"{self.lambda_max:.3f}), slow gap {self.slowest_slow_rate:.5f} "
f"vs diffusion gap {self.slowest_diffusion_rate:.5f}, "
f"trajectory error {self.trajectory_max_rel_error:.2e}"
)
def verify_overdamped_projection(
G: Any,
*,
gamma: float = 50.0,
n_time_samples: int = 40,
tolerance: float = 1e-2,
) -> OverdampedProjectionCertificate:
r"""Verify diffusion is the overdamped projection of the substrate wave.
Builds the canonical random-walk Laplacian L_rw, forms the damped graph
wave q̈ + γq̇ + L q = 0, and measures two things in the strong-damping
limit: (i) the per-mode **slow rate** converges to the diffusion rate
λ_k/γ = ν_f·λ_k (ν_f = 1/γ), with error scaling as O(λ_max/γ²); and
(ii) the damped-wave **trajectory** (from q₀ at rest) collapses onto the
structural-diffusion trajectory exp(−L t/γ)·q₀. No field formula is
re-implemented — L_rw comes from
:func:`structural_diffusion_operator` and the orthonormal eigenbasis
from the symmetric normalized Laplacian.
Parameters
----------
G : TNFRGraph
gamma : float
Damping coefficient (effective diffusivity ν_f = 1/γ). Must satisfy
γ² > 4·λ_max for every mode to be overdamped.
n_time_samples : int
Time samples in the overdamped window for the trajectory check.
tolerance : float
Maximum relative error (rate and trajectory) for a valid projection.
Returns
-------
OverdampedProjectionCertificate
"""
nodes, lap = structural_diffusion_operator(G)
n = len(nodes)
lambdas = np.sort(np.linalg.eigvals(lap).real)
lambdas = np.clip(lambdas, 0.0, None)
lam_max = float(lambdas[-1]) if n else 0.0
nonzero = lambdas[lambdas > 1e-9]
lam2 = float(nonzero[0]) if nonzero.size else 0.0
nu_f = 1.0 / gamma
# (i) per-mode slow rate vs diffusion rate
disc = gamma * gamma - 4.0 * lambdas + 0j
s_slow = ((-gamma + np.sqrt(disc)) / 2.0).real
diff_rate = lambdas / gamma # = nu_f * lambda_k
mask = lambdas > 1e-9
if np.any(mask):
rel = np.abs(s_slow[mask] + diff_rate[mask]) / diff_rate[mask]
max_rate_rel = float(np.max(rel))
else:
max_rate_rel = 0.0
# Fiedler (slowest) mode. Complex-safe: an under-damped gamma (gamma^2 <
# 4*lam2, reachable when a caller fits gamma from oscillatory data) yields
# a complex root whose real part -gamma/2 is the envelope decay rate.
if lam2 > 0.0:
s_gap = (
(-gamma + np.sqrt(gamma * gamma - 4.0 * lam2 + 0j)) / 2.0