corticalfields 0.2.2__tar.gz → 0.2.3__tar.gz

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 (57) hide show
  1. {corticalfields-0.2.2/src/corticalfields.egg-info → corticalfields-0.2.3}/PKG-INFO +1 -1
  2. {corticalfields-0.2.2 → corticalfields-0.2.3}/pyproject.toml +1 -1
  3. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/__init__.py +7 -7
  4. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/backends.py +181 -190
  5. {corticalfields-0.2.2 → corticalfields-0.2.3/src/corticalfields.egg-info}/PKG-INFO +1 -1
  6. {corticalfields-0.2.2 → corticalfields-0.2.3}/LICENSE +0 -0
  7. {corticalfields-0.2.2 → corticalfields-0.2.3}/README.md +0 -0
  8. {corticalfields-0.2.2 → corticalfields-0.2.3}/setup.cfg +0 -0
  9. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/_pointcloud_legacy.py +0 -0
  10. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/analysis/__init__.py +0 -0
  11. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/analysis/bayesian.py +0 -0
  12. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/analysis/eda_qc.py +0 -0
  13. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/analysis/normative.py +0 -0
  14. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/analysis/stats.py +0 -0
  15. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/asymmetry.py +0 -0
  16. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/bayes_viz.py +0 -0
  17. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/bayesian.py +0 -0
  18. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/brainplots.py +0 -0
  19. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/datasets.py +0 -0
  20. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/distance_stats.py +0 -0
  21. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/eda_qc.py +0 -0
  22. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/features.py +0 -0
  23. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/functional_maps.py +0 -0
  24. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/graphs.py +0 -0
  25. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/hippocampus.py +0 -0
  26. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/kernels.py +0 -0
  27. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/normative.py +0 -0
  28. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/__init__.py +0 -0
  29. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/deep/__init__.py +0 -0
  30. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/deep/diffusion_net.py +0 -0
  31. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/deep/egnn.py +0 -0
  32. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/functional_maps.py +0 -0
  33. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/morphometrics.py +0 -0
  34. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/registration.py +0 -0
  35. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/spectral.py +0 -0
  36. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/transport.py +0 -0
  37. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud/viz.py +0 -0
  38. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/pointcloud.py +0 -0
  39. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/spectral.py +0 -0
  40. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/subcortical.py +0 -0
  41. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/surface.py +0 -0
  42. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/surprise.py +0 -0
  43. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/transport.py +0 -0
  44. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/utils.py +0 -0
  45. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/viz/__init__.py +0 -0
  46. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/viz/bayes.py +0 -0
  47. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/viz/brainplots.py +0 -0
  48. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/viz/graph_viz.py +0 -0
  49. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/viz/subcortical.py +0 -0
  50. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/viz/viz.py +0 -0
  51. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/viz.py +0 -0
  52. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields/viz_subcortical.py +0 -0
  53. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields.egg-info/SOURCES.txt +0 -0
  54. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields.egg-info/dependency_links.txt +0 -0
  55. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields.egg-info/requires.txt +0 -0
  56. {corticalfields-0.2.2 → corticalfields-0.2.3}/src/corticalfields.egg-info/top_level.txt +0 -0
  57. {corticalfields-0.2.2 → corticalfields-0.2.3}/tests/test_core.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: corticalfields
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Spectral cortical and subcortical analysis with statistical testing (RSA, CCA, PLS, PERMANOVA, TFCE, NBS, laterality classification), on meshes and point clouds — Laplace-Beltrami decomposition, atlas-free asymmetry, GPU-accelerated optimal transport, hippocampal subfield analysis (HippUnfold), ShapeDNA/BrainPrint spectral fingerprinting, geometric deep learning, Bayesian inference, and normative modeling for structural neuroimaging.
5
5
  Author-email: rdneuro <r.debona@ufrj.br>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "corticalfields"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Spectral cortical and subcortical analysis with statistical testing (RSA, CCA, PLS, PERMANOVA, TFCE, NBS, laterality classification), on meshes and point clouds — Laplace-Beltrami decomposition, atlas-free asymmetry, GPU-accelerated optimal transport, hippocampal subfield analysis (HippUnfold), ShapeDNA/BrainPrint spectral fingerprinting, geometric deep learning, Bayesian inference, and normative modeling for structural neuroimaging."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -6,7 +6,7 @@ functional maps, optimal-transport distances, and information-theoretic
6
6
  surprise maps on brain surface meshes. Designed for structural MRI (T1w)
7
7
  data in clinical neuroimaging, with emphasis on epilepsy (MTLE-HS).
8
8
 
9
- Subpackages (v0.2.1)
9
+ Subpackages (v0.2.3)
10
10
  ---------------------
11
11
  analysis : Statistical analysis & modeling
12
12
  analysis.stats — MCC, GLM, PERMANOVA, CCA/PLS, RSA, NBS,
@@ -29,7 +29,7 @@ surface, subcortical, hippocampus, spectral, kernels, surprise, features,
29
29
  graphs, distance_stats, asymmetry, transport, functional_maps, datasets, utils
30
30
  """
31
31
 
32
- __version__ = "0.2.2"
32
+ __version__ = "0.2.3"
33
33
  __author__ = "rdneuro"
34
34
 
35
35
 
@@ -100,7 +100,7 @@ def __getattr__(name: str):
100
100
  "to_latex_table": ("corticalfields.analysis.bayesian", "to_latex_table"),
101
101
  "elicit_prior": ("corticalfields.analysis.bayesian", "elicit_prior"),
102
102
  "enigma_informed_prior": ("corticalfields.analysis.bayesian", "enigma_informed_prior"),
103
- # ── analysis.stats (NEW v0.2.2) ─────────────────────────────────
103
+ # ── analysis.stats ─────────────────────────────────
104
104
  "StatResult": ("corticalfields.analysis.stats", "StatResult"),
105
105
  "MultipleComparisonResult": ("corticalfields.analysis.stats", "MultipleComparisonResult"),
106
106
  "fdr_correction": ("corticalfields.analysis.stats", "fdr_correction"),
@@ -128,7 +128,7 @@ def __getattr__(name: str):
128
128
  "conformal_prediction_intervals": ("corticalfields.analysis.stats", "conformal_prediction_intervals"),
129
129
  "bootstrap_gpu": ("corticalfields.analysis.stats", "bootstrap_gpu"),
130
130
  "permutation_matrix_gpu": ("corticalfields.analysis.stats", "permutation_matrix_gpu"),
131
- # ── graphs.py (REWRITTEN v0.2.2) ──────────────────────────────────
131
+ # ── graphs.py ──────────────────────────────────
132
132
  "GraphResult": ("corticalfields.graphs", "GraphResult"),
133
133
  "GraphMetrics": ("corticalfields.graphs", "GraphMetrics"),
134
134
  "morphometric_similarity_network": ("corticalfields.graphs", "morphometric_similarity_network"),
@@ -150,7 +150,7 @@ def __getattr__(name: str):
150
150
  "BrainGraphGCN": ("corticalfields.graphs", "BrainGraphGCN"),
151
151
  "spectral_morphometric_pipeline": ("corticalfields.graphs", "spectral_morphometric_pipeline"),
152
152
  "YEO7_COLORS": ("corticalfields.graphs", "YEO7_COLORS"),
153
- # ── viz.graph_viz (NEW v0.2.2) ───────────────────────────────────
153
+ # ── viz.graph_viz (NEW v0.2.3) ───────────────────────────────────
154
154
  "plot_glass_brain_connectome": ("corticalfields.viz.graph_viz", "plot_glass_brain_connectome"),
155
155
  "plot_adjacency_matrix": ("corticalfields.viz.graph_viz", "plot_adjacency_matrix"),
156
156
  "plot_edge_weight_distribution": ("corticalfields.viz.graph_viz", "plot_edge_weight_distribution"),
@@ -310,7 +310,7 @@ __all__ = [
310
310
  "estimate_n_eigenpairs", "gc_gpu", "vram_report", "vram_guard",
311
311
  "fetch_toy_dataset", "clear_toy_dataset",
312
312
  "load_example_surface", "ToyDataset",
313
- # ── graphs (v0.2.2) ──────────────────────────────────────────────
313
+ # ── graphs (v0.2.3) ──────────────────────────────────────────────
314
314
  "GraphResult", "GraphMetrics",
315
315
  "morphometric_similarity_network", "spectral_similarity_network",
316
316
  "mind_divergence_network", "wasserstein_spectral_network",
@@ -321,7 +321,7 @@ __all__ = [
321
321
  "persistent_homology", "nbs_morphometric", "group_metric_comparison",
322
322
  "to_pyg_data", "build_population_graph", "BrainGraphGCN",
323
323
  "spectral_morphometric_pipeline", "YEO7_COLORS",
324
- # ── viz.graph_viz (v0.2.2) ────────────────────────────────────────
324
+ # ── viz.graph_viz (v0.2.3) ────────────────────────────────────────
325
325
  "plot_glass_brain_connectome", "plot_adjacency_matrix",
326
326
  "plot_edge_weight_distribution", "plot_laplacian_spectrum",
327
327
  "plot_graph_layout", "plot_rich_club_curve", "plot_nbs_result",
@@ -439,8 +439,6 @@ def _eigsh_torch(
439
439
  3. **ChFSI outer loop** (typically 15–40 iterations):
440
440
  a. Apply degree-``d`` Chebyshev polynomial filter via 3-term
441
441
  SpMV recurrence (no matrix assembly — only matvecs).
442
- The filter amplifies components in ``[0, λ_cutoff]`` and
443
- damps the rest, concentrating V into the target eigenspace.
444
442
  b. Orthogonalise: ``V, _ = QR(filtered_V)``.
445
443
  c. Rayleigh–Ritz: ``H = Vᵀ A V`` (m×m dense eigh).
446
444
  d. Convergence check: max residual norm < tol.
@@ -448,11 +446,28 @@ def _eigsh_torch(
448
446
 
449
447
  Mixed precision
450
448
  ---------------
451
- SpMV and the Chebyshev filter run in **float32** for ~2× throughput
452
- on modern GPUs. The Rayleigh–Ritz projection (small m×m problem)
453
- is accumulated and solved in **float64** for numerical stability.
454
- This preserves eigenvalue accuracy to ~1e-7 for the first ~300
455
- Laplace–Beltrami eigenpairs while halving SpMV memory bandwidth.
449
+ SpMV and the Chebyshev filter run in **float32** for ~2× throughput.
450
+ The Rayleigh–Ritz projection (small m×m) is accumulated in **float64**.
451
+
452
+ GPU stability
453
+ -------------
454
+ The NVIDIA driver's GPU watchdog timer can trigger a PCIe bus hang
455
+ if the GPU command queue grows unboundedly without CPU sync points.
456
+ This function inserts ``torch.cuda.synchronize()`` at three levels:
457
+
458
+ - **After each Chebyshev filter pass** (every ~12 SpMV launches)
459
+ - **After every Rayleigh–Ritz step** (before convergence check)
460
+ - **Periodic ``empty_cache()``** every 5 outer iterations
461
+
462
+ These add ~1 ms overhead per sync but prevent the driver from
463
+ interpreting a deep async queue as a hung GPU. Critical on X570
464
+ chipsets (e.g. ASUS Crosshair Dark Hero) with drivers ≥ 560.x
465
+ and CUDA ≥ 12.x, where failed GPU resets cascade to PCIe bus
466
+ hangs and apparent filesystem loss.
467
+
468
+ All operations run inside ``torch.no_grad()`` to prevent autograd
469
+ from tracking the ~500 SpMV operations, which would otherwise
470
+ build a 10+ GB computation graph in RAM.
456
471
 
457
472
  VRAM budget (N = 150k, k = 300, m = 330)
458
473
  ------------------------------------------
@@ -461,29 +476,10 @@ def _eigsh_torch(
461
476
  - Chebyshev temps: 2 × N × m × 4 = ~396 MB (Y_prev, Y_curr)
462
477
  - Rayleigh–Ritz H: m × m × 8 = ~0.9 MB (float64)
463
478
  - **Peak total: ~609 MB** — fits in 8 GB VRAM with margin.
464
- - Previous lobpcg: 9 × N × k × 8 = ~3.2 GB — 5× higher.
465
-
466
- Performance (RTX 3090, N=150k, k=300)
467
- -------------------------------------
468
- - ChFSI (this): ~10–25 s (degree=12, 15–30 outer iters)
469
- - torch.lobpcg (old): ~60–120 s
470
- - CuPy eigsh: ~10–30 s (Thick-Restart Lanczos)
471
- - scipy eigsh: ~30–120 s (ARPACK shift-invert)
472
-
473
- Both individual and batch processing use this function. In batch
474
- mode, ``gc_gpu()`` is called between subjects by the caller
475
- (``_process_single_subject`` in ``spectral.py``), which frees
476
- VRAM for the next subject.
477
479
 
478
480
  Parameters
479
481
  ----------
480
- L : scipy.sparse.spmatrix (N, N) stiffness matrix
481
- M : scipy.sparse.spmatrix (N, N) — diagonal lumped mass matrix
482
- k : int — number of smallest eigenpairs to compute
483
- tol : float — convergence tolerance on max residual norm
484
- maxiter : int — maximum ChFSI outer iterations
485
- dtype : str — ``"float32"`` or ``"float64"`` for SpMV precision;
486
- Rayleigh–Ritz always uses float64 regardless.
482
+ L, M, k, tol, maxiter, dtype : see ``eigsh_solve``
487
483
 
488
484
  Returns
489
485
  -------
@@ -492,28 +488,25 @@ def _eigsh_torch(
492
488
 
493
489
  References
494
490
  ----------
495
- [1] Y. Zhou, Y. Saad, M.L. Tiago & J.R. Chelikowsky,
496
- "Self-consistent-field calculations using Chebyshev-filtered
497
- subspace iteration", J. Comput. Phys. 219 (2006) 172–184.
498
- [2] A.V. Knyazev, "Toward the optimal preconditioned eigensolver:
499
- LOBPCG", SIAM J. Sci. Comput. 23 (2001) 517–541.
491
+ [1] Y. Zhou, Y. Saad et al., "Chebyshev-filtered subspace iteration",
492
+ J. Comput. Phys. 219 (2006) 172–184.
500
493
  """
501
494
  import torch
502
495
 
503
496
  # ── Precision setup ─────────────────────────────────────────────
504
- # SpMV in float32 for throughput; Rayleigh-Ritz in float64 for accuracy
505
- spmv_np_dtype = np.float32 if dtype != "float64" else np.float32
497
+ spmv_np_dtype = np.float32
506
498
  spmv_torch_dtype = torch.float32
507
499
  ritz_torch_dtype = torch.float64
508
500
 
509
501
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
502
+ is_cuda = device.type == "cuda"
510
503
  N = L.shape[0]
511
504
 
512
505
  # ChFSI hyperparameters — calibrated for LBO meshes
513
- EXTRA = min(30, max(10, k // 10)) # oversampling for Ritz stability
514
- m = k + EXTRA # subspace dimension
515
- CHEB_DEGREE = 12 # Chebyshev filter polynomial degree
516
- POWER_ITERS = 30 # for λ_max estimation
506
+ EXTRA = min(30, max(10, k // 10))
507
+ m = k + EXTRA
508
+ CHEB_DEGREE = 12
509
+ POWER_ITERS = 30
517
510
 
518
511
  logger.info(
519
512
  " torch ChFSI eigensolver: N=%d, k=%d, m=%d, degree=%d, "
@@ -524,11 +517,11 @@ def _eigsh_torch(
524
517
  # ── Step 1: Generalised → standard via M^{−½} (on CPU) ─────────
525
518
  M_diag = np.array(M.diagonal()).ravel().astype(np.float64)
526
519
  M_diag = np.maximum(M_diag, 1e-16)
527
- M_inv_sqrt_np = (1.0 / np.sqrt(M_diag)) # float64 for precision
520
+ M_inv_sqrt_np = 1.0 / np.sqrt(M_diag) # float64 for precision
528
521
 
529
522
  D_sp = sp.diags(M_inv_sqrt_np.astype(spmv_np_dtype), format="csc")
530
523
  A_cpu = (D_sp @ L.tocsc().astype(spmv_np_dtype) @ D_sp).tocsr()
531
- del D_sp # free CPU temp
524
+ del D_sp
532
525
 
533
526
  # ── Helper: scipy CSR → torch sparse CSR on device ──────────────
534
527
  def _scipy_to_torch_csr(mat_csr):
@@ -540,161 +533,159 @@ def _eigsh_torch(
540
533
  dtype=spmv_torch_dtype,
541
534
  )
542
535
 
543
- # ── Helper: sparse matvec A @ X on GPU ──────────────────────────
544
- def _spmm(A_t, X):
545
- """Sparse × dense matrix multiply, shape (N, m)."""
546
- return torch.sparse.mm(A_t, X)
547
-
548
536
  try:
549
- # ── Step 2: Transfer A to GPU ───────────────────────────────
550
537
  A_t = _scipy_to_torch_csr(A_cpu)
551
- del A_cpu # free CPU copy (~14 MB saved)
552
-
553
- # ── Step 3: Estimate λ_max via power iteration ──────────────
554
- # 30 iters is overkill for Rayleigh quotient convergence on
555
- # a mesh Laplacian, but costs only ~15 ms and gives a tight
556
- # bound that improves Chebyshev filter quality.
557
- torch.manual_seed(42)
558
- v = torch.randn(N, 1, dtype=spmv_torch_dtype, device=device)
559
- v = v / v.norm()
560
- for _ in range(POWER_ITERS):
561
- v = _spmm(A_t, v)
538
+ del A_cpu
539
+
540
+ # Everything below is inference no autograd needed.
541
+ # torch.no_grad() prevents building a massive computation graph
542
+ # from the ~500 SpMV calls (would leak ~10+ GB of RAM otherwise).
543
+ with torch.no_grad():
544
+
545
+ # ── Step 2: Estimate λ_max via power iteration ──────────
546
+ torch.manual_seed(42)
547
+ v = torch.randn(N, 1, dtype=spmv_torch_dtype, device=device)
562
548
  v = v / v.norm()
563
- # Rayleigh quotient in float64 for a precise λ_max
564
- v64 = v.to(ritz_torch_dtype)
565
- Av64 = _spmm(A_t, v).to(ritz_torch_dtype)
566
- lambda_max = float((v64.T @ Av64).item()) * 1.05 # 5% safety
567
- del v, v64, Av64
568
- logger.info(" λ_max ≈ %.4f", lambda_max)
569
-
570
- # ── Step 4: ChFSI outer loop ───────────────────────────────
571
- # Initial random subspace
572
- torch.manual_seed(42)
573
- V = torch.randn(N, m, dtype=spmv_torch_dtype, device=device)
574
- V, _ = torch.linalg.qr(V)
575
-
576
- # Chebyshev filter interval: we want eigenvalues in [0, λ_cut]
577
- # where λ_cut is a rough upper bound for the k-th eigenvalue.
578
- # Heuristic: Weyl's law gives λ_k ∝ k for 2D surfaces, so
579
- # λ_cut λ_max × (2 * m / N) is a conservative estimate.
580
- # We refine after the first Ritz step.
581
- lambda_cut = lambda_max * (2.0 * m / N)
582
- lambda_cut = max(lambda_cut, lambda_max * 0.01) # floor
583
-
584
- converged = False
585
- for outer in range(maxiter):
586
- # ── Chebyshev filter: T_d(scaled_A) @ V ────────────────
587
- # Maps A from [λ_cut, λ_max] → [−1, 1], then applies
588
- # Chebyshev polynomial that is ~0 on [−1, 1] (unwanted
589
- # eigenvalues) and large on (−∞, −1) (wanted eigenvalues).
590
- #
591
- # Scaling: σ = (λ_max − λ_cut) / 2
592
- # c = (λ_max + λ_cut) / 2
593
- # A_scaled = (A c·I) / σ
594
- #
595
- # 3-term recurrence:
596
- # Y₀ = V
597
- # Y₁ = (1/σ)(A c·I) V = (A·V − c·V) / σ
598
- # Y_{j+1} = (2/σ)(A c·I) Y_j − Y_{j−1}
599
- # = (2(A·Y_j − c·Y_j) / σ) − Y_{j−1}
600
-
601
- e = (lambda_max - lambda_cut) / 2.0
602
- c = (lambda_max + lambda_cut) / 2.0
603
-
604
- # Safeguard: e must be positive
605
- if e < 1e-10:
606
- e = lambda_max * 0.5
607
- c = lambda_max * 0.5
608
-
609
- sigma = e / c if abs(c) > 1e-12 else 1.0
610
- sigma1 = sigma
611
-
612
- # Y₀ = V (reuse V buffer)
613
- # Y₁ = σ₁/e · (A·V − c·V)
614
- AV = _spmm(A_t, V) # (N, m) f32
615
- Y_prev = V # alias, no copy
616
- Y_curr = (sigma1 / e) * (AV - c * V) # (N, m) f32
617
- del AV
618
-
619
- for d in range(2, CHEB_DEGREE + 1):
620
- sigma_new = 1.0 / (2.0 / sigma - sigma1)
621
- AY = _spmm(A_t, Y_curr) # (N, m) f32
622
- Y_next = (2.0 * sigma_new / e) * (AY - c * Y_curr) \
623
- - (sigma * sigma_new) * Y_prev
624
- Y_prev = Y_curr
625
- Y_curr = Y_next
626
- sigma = sigma_new
627
- del AY
628
-
629
- del Y_prev # free (N, m) buffer
630
-
631
- # ── Orthogonalise filtered subspace ────────────────────
632
- V, _ = torch.linalg.qr(Y_curr)
633
- del Y_curr
634
-
635
- # ── Rayleigh–Ritz in float64 ──────────────────────────
636
- # AV in float32 for speed, then upcast for the small eigh
637
- AV = _spmm(A_t, V) # (N, m) f32
638
- V64 = V.to(ritz_torch_dtype) # (N, m) f64
639
- AV64 = AV.to(ritz_torch_dtype) # (N, m) f64
640
- del AV
641
-
642
- H = V64.T @ AV64 # (m, m) f64
643
- H = 0.5 * (H + H.T) # symmetrise
644
- ritz_vals, ritz_vecs = torch.linalg.eigh(H) # sorted ascending
645
-
646
- # ── Convergence check: max residual norm ───────────────
647
- # residual_i = A·z_i − λ_i·z_i where z_i = V @ s_i
648
- eigvecs_m = V64 @ ritz_vecs[:, :k] # (N, k) f64
649
- Aeigvecs = AV64 @ ritz_vecs[:, :k] # (N, k) f64
650
- residuals = Aeigvecs - eigvecs_m * ritz_vals[:k].unsqueeze(0)
651
- max_res = float(residuals.norm(dim=0).max().item())
652
-
653
- del eigvecs_m, Aeigvecs, residuals, V64, AV64
654
-
655
- if outer % 5 == 0 or max_res < tol:
656
- logger.info(
657
- " ChFSI iter %2d: max_residual=%.2e, λ_cut=%.4f",
658
- outer, max_res, lambda_cut,
549
+ for pi in range(POWER_ITERS):
550
+ v = torch.sparse.mm(A_t, v)
551
+ v = v / v.norm()
552
+ # Sync every 10 iters to keep driver watchdog happy
553
+ if is_cuda and pi % 10 == 9:
554
+ torch.cuda.synchronize()
555
+
556
+ # Rayleigh quotient in float64 for a precise λ_max
557
+ v64 = v.to(ritz_torch_dtype)
558
+ Av64 = torch.sparse.mm(A_t, v).to(ritz_torch_dtype)
559
+ lambda_max = float((v64.T @ Av64).item()) * 1.05
560
+ del v, v64, Av64
561
+ if is_cuda:
562
+ torch.cuda.synchronize()
563
+ logger.info(" λ_max %.4f", lambda_max)
564
+
565
+ # ── Step 3: ChFSI outer loop ────────────────────────────
566
+ torch.manual_seed(42)
567
+ V = torch.randn(N, m, dtype=spmv_torch_dtype, device=device)
568
+ V, _ = torch.linalg.qr(V)
569
+
570
+ lambda_cut = lambda_max * (2.0 * m / N)
571
+ lambda_cut = max(lambda_cut, lambda_max * 0.01)
572
+
573
+ converged = False
574
+ max_res = float("inf")
575
+
576
+ for outer in range(maxiter):
577
+
578
+ # ── Chebyshev filter: T_d(scaled_A) @ V ─────────────
579
+ e = (lambda_max - lambda_cut) / 2.0
580
+ c_coeff = (lambda_max + lambda_cut) / 2.0
581
+
582
+ if e < 1e-10:
583
+ e = lambda_max * 0.5
584
+ c_coeff = lambda_max * 0.5
585
+
586
+ sigma = e / c_coeff if abs(c_coeff) > 1e-12 else 1.0
587
+ sigma1 = sigma
588
+
589
+ # Y₁ = (σ₁/e) · (A·V − c·V)
590
+ AV = torch.sparse.mm(A_t, V)
591
+ Y_prev = V
592
+ Y_curr = (sigma1 / e) * (AV - c_coeff * V)
593
+ del AV
594
+
595
+ for d in range(2, CHEB_DEGREE + 1):
596
+ sigma_new = 1.0 / (2.0 / sigma - sigma1)
597
+ AY = torch.sparse.mm(A_t, Y_curr)
598
+ Y_next = (2.0 * sigma_new / e) * (AY - c_coeff * Y_curr) \
599
+ - (sigma * sigma_new) * Y_prev
600
+ Y_prev = Y_curr
601
+ Y_curr = Y_next
602
+ sigma = sigma_new
603
+ del AY
604
+ # Mid-filter sync every 4 SpMV to prevent driver timeout
605
+ if is_cuda and d % 4 == 0:
606
+ torch.cuda.synchronize()
607
+
608
+ del Y_prev
609
+
610
+ # ── SYNC: end of Chebyshev filter ───────────────────
611
+ if is_cuda:
612
+ torch.cuda.synchronize()
613
+
614
+ # ── Orthogonalise filtered subspace ─────────────────
615
+ V, _ = torch.linalg.qr(Y_curr)
616
+ del Y_curr
617
+
618
+ # ── Rayleigh–Ritz in float64 ────────────────────────
619
+ AV = torch.sparse.mm(A_t, V)
620
+ V64 = V.to(ritz_torch_dtype)
621
+ AV64 = AV.to(ritz_torch_dtype)
622
+ del AV
623
+
624
+ H = V64.T @ AV64
625
+ H = 0.5 * (H + H.T)
626
+ ritz_vals, ritz_vecs = torch.linalg.eigh(H)
627
+
628
+ # ── Convergence check ───────────────────────────────
629
+ eigvecs_m = V64 @ ritz_vecs[:, :k]
630
+ Aeigvecs = AV64 @ ritz_vecs[:, :k]
631
+ residuals = Aeigvecs - eigvecs_m * ritz_vals[:k].unsqueeze(0)
632
+ max_res = float(residuals.norm(dim=0).max().item())
633
+
634
+ del eigvecs_m, Aeigvecs, residuals, V64, AV64
635
+
636
+ # ── SYNC: end of Ritz step ──────────────────────────
637
+ if is_cuda:
638
+ torch.cuda.synchronize()
639
+
640
+ if outer % 5 == 0 or max_res < tol:
641
+ logger.info(
642
+ " ChFSI iter %2d: max_residual=%.2e, λ_cut=%.4f",
643
+ outer, max_res, lambda_cut,
644
+ )
645
+
646
+ if max_res < tol:
647
+ converged = True
648
+ break
649
+
650
+ # Update subspace
651
+ V = V @ ritz_vecs[:, :m].to(spmv_torch_dtype)
652
+
653
+ # Refine λ_cut from current Ritz estimates
654
+ if ritz_vals.shape[0] > k:
655
+ lambda_cut = float(ritz_vals[m - 1].item()) * 1.5
656
+ lambda_cut = min(lambda_cut, lambda_max * 0.95)
657
+
658
+ # Periodic VRAM housekeeping — prevents fragmentation
659
+ if is_cuda and outer % 5 == 4:
660
+ torch.cuda.empty_cache()
661
+
662
+ # ── end outer loop ──────────────────────────────────────
663
+
664
+ if not converged:
665
+ logger.warning(
666
+ " ChFSI did not converge in %d iters "
667
+ "(max_residual=%.2e > tol=%.1e). "
668
+ "Results may be approximate.",
669
+ maxiter, max_res, tol,
659
670
  )
660
671
 
661
- if max_res < tol:
662
- converged = True
663
- break
664
-
665
- # ── Update subspace: rotate V into Ritz basis ──────────
666
- V = V @ ritz_vecs[:, :m].to(spmv_torch_dtype)
667
-
668
- # ── Refine λ_cut from current Ritz estimates ───────────
669
- # Use 1.5× the m-th Ritz value as the new cutoff
670
- if ritz_vals.shape[0] > k:
671
- lambda_cut = float(ritz_vals[m - 1].item()) * 1.5
672
- lambda_cut = min(lambda_cut, lambda_max * 0.95)
673
-
674
- if not converged:
675
- logger.warning(
676
- " ChFSI did not converge in %d iters "
677
- "(max_residual=%.2e > tol=%.1e). Results may be approximate.",
678
- maxiter, max_res, tol,
679
- )
680
-
681
- # ── Step 5: Extract final eigenpairs ────────────────────────
682
- evals_t = ritz_vals[:k] # (k,) f64 on GPU
683
- # Final eigenvectors: V @ ritz_vecs[:, :k] in float64
684
- evecs_t = V.to(ritz_torch_dtype) @ ritz_vecs[:, :k] # (N, k) f64
685
-
686
- # ── Step 6: Undo mass-matrix transform: φ = M^{−½} · y ────
687
- M_inv_sqrt_t = torch.from_numpy(
688
- M_inv_sqrt_np
689
- ).to(dtype=ritz_torch_dtype, device=device).unsqueeze(1) # (N, 1)
690
-
691
- evecs_t = evecs_t * M_inv_sqrt_t # (N, k) f64
692
- del M_inv_sqrt_t, V, ritz_vals, ritz_vecs
693
-
694
- # Move to CPU
695
- evals = evals_t.cpu().numpy().astype(np.float64)
696
- evecs = evecs_t.cpu().numpy().astype(np.float64)
697
- del evals_t, evecs_t
672
+ # ── Extract final eigenpairs ────────────────────────────
673
+ evals_t = ritz_vals[:k]
674
+ evecs_t = V.to(ritz_torch_dtype) @ ritz_vecs[:, :k]
675
+
676
+ M_inv_sqrt_t = torch.from_numpy(
677
+ M_inv_sqrt_np
678
+ ).to(dtype=ritz_torch_dtype, device=device).unsqueeze(1)
679
+
680
+ evecs_t = evecs_t * M_inv_sqrt_t
681
+ del M_inv_sqrt_t, V, ritz_vals, ritz_vecs
682
+
683
+ # Move to CPU
684
+ if is_cuda:
685
+ torch.cuda.synchronize()
686
+ evals = evals_t.cpu().numpy().astype(np.float64)
687
+ evecs = evecs_t.cpu().numpy().astype(np.float64)
688
+ del evals_t, evecs_t
698
689
 
699
690
  finally:
700
691
  # Guarantee GPU cleanup even on error — critical for batch mode
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: corticalfields
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Spectral cortical and subcortical analysis with statistical testing (RSA, CCA, PLS, PERMANOVA, TFCE, NBS, laterality classification), on meshes and point clouds — Laplace-Beltrami decomposition, atlas-free asymmetry, GPU-accelerated optimal transport, hippocampal subfield analysis (HippUnfold), ShapeDNA/BrainPrint spectral fingerprinting, geometric deep learning, Bayesian inference, and normative modeling for structural neuroimaging.
5
5
  Author-email: rdneuro <r.debona@ufrj.br>
6
6
  License: MIT
File without changes
File without changes
File without changes