Fixes issue 225 by explicit symmetrization of density matrices - #226
susilehtola wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new API semantics need public documentation and targeted regression coverage.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Projects trial density matrices onto their symmetric components to preserve Hermiticity in GGA/meta-GGA FXC contractions.
Changes:
- Symmetrizes RKS/UKS trial densities before contraction.
- Applies equivalent handling across host and device backends.
File summaries
| File | Description |
|---|---|
reference_replicated_xc_host_integrator_fxc_contraction.hpp |
Adds host-side trial-density symmetrization. |
incore_replicated_xc_device_integrator_fxc_contraction.hpp |
Mirrors symmetrization for device execution. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| out.resize( nbf*nbf ); | ||
| for( int64_t j = 0; j < nbf; ++j ) | ||
| for( int64_t i = 0; i < nbf; ++i ) | ||
| out[i + j*nbf] = 0.5 * ( A[i + j*lda] + A[j + i*lda] ); |
There was a problem hiding this comment.
Addressed in 9e17cd2.
Worth noting why the existing test was blind to this: every stored trial density in the reference data is exactly symmetric (max|tP - tPᵀ| = 0.0), so antisymmetrizing the stored matrices would also have given a vacuous test.
Instead, test_fxc_contractioin — the shared helper, so it runs for Host and every Device section — now builds a non-symmetric M whose symmetric part is bitwise the stored trial density (strict upper triangle doubled, lower zeroed), and checks, for RKS and both UKS channels:
F[M]reproduces the storedFXCreference, i.e.F[M] == F[(M+Mᵀ)/2]against independent data;F[(M−Mᵀ)/2]vanishes.
Verified on a host build that it discriminates:
| new checks | pre-existing checks | |
|---|---|---|
| without the fix | 12 fail — PBE0, SCAN (RKS), BLYP, SCAN (UKS), all 4e-4…3e-3 vs. 1e-10 tolerance | pass |
| with the fix | pass | pass |
LDA sections pass either way, as expected. 44/44 assertions with the fix. Device path not run (no GPU here).
| const value_type* tPs_use = symmetrize( tPs, ldtps, tPs_sym ); | ||
| const value_type* tPz_use = symmetrize( tPz, ldtpz, tPz_sym ); |
There was a problem hiding this comment.
Addressed in 9e17cd2. Both XCIntegrator::eval_fxc_contraction overloads (C++) and gauxc_integrator_eval_fxc_contraction_{rks,uks} (C) now document that the kernel is contracted with the symmetric part (tP + tPᵀ)/2 of each trial density, that an antisymmetric part contributes exactly zero, and that a linear-response caller must pass P1 = T + Tᵀ — a one-sided transition density T yields half that response. The C @param entries for the trial densities, which were copy-pasted from the reference density, now say "trial density" too.
Address review feedback on wavefunction91#226. Tests: the stored trial densities in the reference data are exactly symmetric, so the existing FXC reference comparison cannot detect how a non-symmetric trial density is handled -- which is why it passed throughout wavefunction91#225. For every functional, spin case and backend exercised by test_fxc_contractioin, build a non-symmetric trial matrix M whose symmetric part is bitwise the stored trial density (strict upper triangle doubled, lower zeroed), and check that * F[M] reproduces the stored FXC reference, and * F[(M - M^T)/2] vanishes. Without the symmetrization fix, all 12 new checks for PBE0, SCAN (RKS) and BLYP, SCAN (UKS) fail by 4e-4..3e-3 against a 1e-10 tolerance, while the LDA sections and all pre-existing checks still pass. With the fix, all 44 assertions pass (host build). Docs: state at the public C++ and C API entry points that the kernel is contracted with the symmetric part of the trial density, and that a linear-response caller must pass P1 = T + T^T; a one-sided transition density yields half that response. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sk6Km8XjNxNU8wyRDQ8sxx
Any asymmetry in the density matrix leaks into the GGA Hessian, this PR fixes the problem.
Closes #225
AI Summary
eval_fxc_contractionmishandles a non-symmetric trial density matrix inits density-gradient channel. Since a transition density matrix is not
symmetric, every GGA and meta-GGA matrix–vector product picks up a spurious
contribution from the trial matrix's antisymmetric part, which destroys the
Hermiticity of the Kohn–Sham Hessian. LDA is immune because it never reads the
density gradient — which is exactly why SVWN5 looked healthy and PBE did not.
The reference density is symmetric, so
eval_exc_vxcandeval_exc_gradarenot affected. The defect is confined to the fxc path, and it is present
identically in the host and CUDA backends.
Root cause
ReferenceLocalHostWorkDriver::eval_uvvar_gga_rks(and itsuks/mggasiblings; CUDA
uvvars_gga.hppis line-for-line the same) builds the densityand its gradient from
X = fac · P · B:Write the two channels out in terms of the matrix that was fed in:
nTr[P χχ]Tr[P χχ]✅ — already sees only the symmetric part∇n2·Tr[P χ ∂χ]Tr[(P + Pᵀ) χ ∂χ]The two agree only when
P = Pᵀ. That holds for the ground-state density,so the ground-state code paths are correct and have always been correct. It
does not hold for a trial density, and
eval_fxc_contractionreuses the verysame routine on one.
Consequently the kernel returns a non-zero result for a direction along which
Excdoes not vary at all:Benzene / cc-pVDZ / UltraFine, two random non-symmetric trial matrices. Every
rung above LDA is affected. The meta-GGA case is not marginal: an asymmetry of
1.872 relative means
Tr(X·F[Y])andTr(Y·F[X])came out with oppositesigns (−15.08 against the correct +7.36).
This is a structural defect, not a numerical one — an antisymmetric input must
map to exactly zero whatever the functional is — so it is worth demonstrating on
a well-behaved meta-GGA rather than on one with difficult numerics. LAK is used
here for that reason; it is reachable through ExchCXX's libxc-name constructor,
XCKernel(libxc_name_string("MGGA_X_LAK"), spin), without an ExchCXX enum entry.(The tau channel is not implicated:
τ = ½Tr[P ∇χ·∇χ]is a symmetricbilinear form in the basis pair, so like the density it annihilates an
antisymmetric input by itself. Meta-GGAs are affected because they carry a
gradient channel, not because of τ.)
Why the reproducer triggers it
demonstrate_nonhermitian_hessian.cpppasses a symmetric ground-state densityd—max|d − dᵀ| = 0— but the ten trial matrices are AO transitiondensity matrices, and those are not symmetric. Each is rank 1, i.e. an outer
product
c_occ ⊗ c_virt, whose antisymmetric part is as large as itssymmetric part:
A perturbed (response) density matrix
P¹ = T + Tᵀwould be symmetric byconstruction and would not have exposed this. A one-sided transition density
matrix does.
Fix
Symmetrize the trial density once, at both integrator entry points:
src/xc_integrator/replicated/host/reference_replicated_xc_host_integrator_fxc_contraction.hppsrc/xc_integrator/replicated/device/incore_replicated_xc_device_integrator_fxc_contraction.hppout[i + j*nbf] = 0.5 * ( A[i + j*lda] + A[j + i*lda] );One
nbf²copy against a grid loop, one place per backend, covering every rungand both spin cases. Doing it here rather than inside the
uvvarkernels keepsa single site per backend and leaves the ground-state paths untouched.
The fix cannot change any LDA result. Symmetrizing preserves the density
channel identically —
Tr[M χχ] = Tr[Mˢʸᵐ χχ]— and repairs the gradientchannel, since
Tr[(M + Mᵀ) χ ∂χ] = 2·Tr[Mˢʸᵐ χ ∂χ]is precisely what theexisting code computes from
Mˢʸᵐ. Verified: the SVWN5 numbers are bit-identicalbefore and after.
Verification
1. The reporter's own demonstrator, built against the tree with and without
the fix (
git revert --no-commit):2. Operator symmetry and the antisymmetric null direction, every rung,
before and after:
F[(M−Mᵀ)/2], must be 0Tr(X·F[Y]) − Tr(Y·F[X]), rel.and
F[M]vsF[(M+Mᵀ)/2]for PBE0: 9.7e-02 → 1.6e-16.3. Against a reference GauXC shares no algebra with. The shipped
tests/2nd_derivative_test.cxxcomparesFXCto stored values that GauXCgenerated itself, so it proves self-consistency only and passed throughout.
Comparing instead against a central difference of
eval_exc_vxc— which is whatthe contraction is defined to be — for a symmetric trial matrix:
unchanged before and after the fix (the residual is finite-difference
truncation).
4. GauXC's own suite, host build: all 3,640,979 assertions in 56 test
cases pass.
Note for callers
After the fix,
eval_fxc_contraction(P, M)returns the kernel applied to(M + Mᵀ)/2— the unique answer consistent withExcdepending only on thesymmetric part of a density matrix. A TDDFT driver that wants the response to
the perturbed density
P¹ = T + Tᵀmust therefore passT + Tᵀ, not theone-sided
T; passingTyields half of that. This contract is worth statingin the API docs, since nothing enforces it.
Suggested regression guard
The existing test cannot see this class of error. Three lines that can:
Out of scope but noticed
been compiled here (no GPU available).
scheme1_base.cxxis compiled for any device build and callseval_tmat_{lda,gga,mgga},eval_vvars_*_trial,eval_kern_vxc_fxc_{lda,gga,mgga},eval_kern_exc_vxc_mggaandexx_ek_screening_bfn_stats, none of which HIP defines.hip/hipify.shconverts a hard-coded file list that never gained
zmat_fxc.cu,uvvars_{lda,gga,mgga}.hpp,increment_exc_grad.cuorexx_ek_screening_bfn_stats.cu;hip/kernels/uvvars.hipstill carriespre-
integrator_ks_schemesignatures, so HIP predates UKS/GKS, meta-GGA andthe fxc contraction entirely. Worth its own issue.