diff --git a/.jules/bolt.md b/.jules/bolt.md index 96776c7..d7d2565 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -5,3 +5,7 @@ ## 2025-11-01 - [Avoid Temporary Array Allocations in Reductions] **Learning:** In loops over parameters like temperature (`thermal_scan`), doing `np.sum(arr1 * arr2)` where array sizes match the system Hilbert space creates huge temporary arrays per loop iteration. Memory allocation dominates execution time. **Action:** Use `np.dot(arr1, arr2)` instead of `np.sum(arr1 * arr2)` for 1D arrays to evaluate reductions in C-level BLAS/NumPy functions, bypassing Python/NumPy array allocations and boosting performance significantly with less peak memory. + +## 2025-11-01 - [Schmidt Decomposition Optimization] +**Learning:** When calculating the Schmidt spectrum (singular values) without needing the singular vectors (`return_vecs=False`), using `scipy.linalg.svdvals(A)` is suboptimal, particularly for highly rectangular matrices. Computing the smaller reduced density matrix ($A A^\dagger$ or $A^\dagger A$) and finding its eigenvalues with `np.linalg.eigvalsh` can be 2x to 6x faster. +**Action:** Replace `la.svdvals(A)` with `np.linalg.eigvalsh` on the smaller density matrix dimension (`dA` vs `dB`). Ensure the values are correctly clipped `[0, 1]` and sorted in descending order to match the original output format. diff --git a/physics/density_matrix.py b/physics/density_matrix.py index 055a667..c03f80c 100644 --- a/physics/density_matrix.py +++ b/physics/density_matrix.py @@ -434,9 +434,21 @@ def schmidt( u, s, vh = la.svd(psi_mat, full_matrices=False) return s**2 if square else s, (u, vh), psi_mat else: - # For values only, use more efficient SVD call (no full decomposition) - s = la.svdvals(psi_mat) - return s**2 if square else s + # For values only, it is significantly faster to compute the smaller RDM + # and find its eigenvalues rather than calling svdvals, especially for + # highly rectangular matrices (e.g., small subsystems). + dA, dB = psi_mat.shape + if dA <= dB: + rho_small = psi_mat @ psi_mat.conj().T + else: + rho_small = psi_mat.conj().T @ psi_mat + + w = np.linalg.eigvalsh(rho_small) + w = np.clip(w, 0.0, 1.0) + + # Sort descending to match svdvals behavior exactly + w = np.sort(w)[::-1] + return w if square else np.sqrt(w) else: rho_A = rho(state, va, ns, local_dim, contiguous, fermionic=fermionic) if return_vecs: