Conversation
added 4 commits
September 16, 2026 22:04
HamiltLCAO::updateHk() sets current_spin on the root operator via set_current_spin(isk[ik]), but the value was never forwarded to child operators. DeltaSpin did not toggle its own current_spin, so it always saw current_spin == 0 for nspin=2 and applied the same +lambda_z coefficient to both spin channels instead of +lambda_z/-lambda_z. The constraint therefore acted as a spin-independent potential and produced wrong magnetic moments and total energies. Propagate current_spin to the next operator in OperatorLCAO::init() before processing the current node, so every node in the chain shares the spin state set by the k-point loop. Regenerate tests/03_NAO_multik/scf_deltaspin2/result.ref, whose previous values encoded the buggy result.
cal_occupations() read the becp layout with the npol=2 stride (ib*2*nkb) and ignored the spin channel for nspin=2 (npol=1), so the projected atomic magnetization printed by print_orb_chg() was wrong for nspin=2. Index by the psi npol and store the spin-up/down occupancy in the up-up/down-down Pauli blocks so that Charge = occ[0]+occ[3] and Mag(z) = occ[0]-occ[3] print correctly; nspin=1 keeps a zero magnetization; nspin=4 keeps the interleaved spinor layout. Also build per-atom labels (Fe1, Fe2, ...) for the Total Magnetism / Magnetic force tables in print_orb_chg(), print_Mi() and print_Mag_Force(), instead of passing the per-type label vector (size ntype) to tables with nat rows. get_iat() is made const so the label helpers can read it from a const SpinConstrain reference.
The result.ref of these five PW DeltaSpin cases predates the DeltaSpin PW rework (they were last written in deepmodeling#7382) and no longer matched the code: 12/18 were off by 2.6/4.6 eV, while 19/21/41 differed only at the 1e-4-1e-7 eV level. Regenerate all five with the current code so the 17_DS_DFTU suite passes again. 12_PW_DS_S2_Z now agrees with the dedicated 01_PW/scf_deltaspin2 reference (-6369.19826815 eV), confirming the new value is the intended one.
The governance checker blocks PRs that increase GlobalV/GlobalC/PARAM usage. cal_occupations() read PARAM.inp.nspin twice; pass it as an explicit argument from ctrl_scf_pw() (which already holds the parsed Input_para) instead. Also keep the print_orb_chg() header line using the existing atom_label variable so the GlobalV::ofs_running line stays untouched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes two independent
nspin=2DeltaSpin (spin-constrained DFT) bugs — one in theLCAO operator chain and one in the PW occupation output — and refreshes stale
DeltaSpin PW reference values.
The LCAO bug made the spin constraint physically wrong (the constraint acted as a
spin-independent potential), producing incorrect magnetic moments and total
energies. The PW bug corrupted the projected atomic magnetization/occupation
output and the per-atom labels of the DeltaSpin tables.
Branch:
fix/deltaspin-lcao-pw-nspin2Base:
deepmodeling/abacus-develop:developRoot cause 1 — LCAO:
current_spinwas never propagated through the operator chainHamiltLCAO::updateHk()sets the spin index on the root operator only:OperatorLCAO::init()did not forward that value to the rest of the chain. Mostoperators (Veff, DFT+U, DeePKS) manage their own
current_spinby toggling, butDeltaSpindoes not — so fornspin=2itscurrent_spinwas always 0.DeltaSpin::contributeHR()then does:With
current_spin == 0for both channels,+lambda_zwas added to bothspin-up and spin-down, i.e. the constraint became a spin-independent potential
that does not split the spins. The lambda loop then optimized against a
Hamiltonian that had no spin response.
Fix
In
OperatorLCAO<TK, TR>::init(), propagatecurrent_spinto the next nodebefore processing the current one:
Instrumented before/after (first 8
contributeHRcalls):current_spin = 0,0,0,0,...current_spin = 0,0,1,1,...Impact (LCAO
Fe2,nspin=2, target M = ±2 µB)After the fix the LCAO result matches the accel branch to ~1e-9 eV and the lambda
loop no longer diverges.
Root cause 2 — PW:
cal_occupations()mis-read thebecplayout fornpol=1OnsiteProjector::cal_occupations()(called whenonsite_radius > 0) always usedthe
npol=2strideib*2*nkband filled all four Pauli blocks, ignoring the spinchannel for
nspin=2(npol=1). The projected atomic magnetization printed byprint_orb_chg()was therefore wrong.Fix
Index by the psi
npoland store the occupancy in the correct Pauli block:nspin=2(npol=1): spin-up →occ[0], spin-down →occ[3], so thatCharge = occ[0]+occ[3]andMag(z) = occ[0]-occ[3].nspin=1(npol=1): split evenly betweenocc[0]/occ[3]so the printedmagnetization is zero.
nspin=4(npol=2): keep the interleaved spinor layout (unchanged).Additionally:
print_orb_chg(),print_Mi(),print_Mag_Force()now build per-atomlabels (
Fe1,Fe2, …) instead of passing the per-type label vector(size
ntype) into tables withnatrows.SpinConstrain::get_iat()is madeconstso the label helpers can read it froma
const SpinConstrain&.cal_occupations()now takesnspinas an explicit argument (passed fromctrl_scf_pw(), which already holds the parsedInput_para) rather thanreading
PARAM.inp.nspin, keeping the repository's global-dependency budgetneutral (required by the governance check).
Test reference updates
tests/03_NAO_multik/scf_deltaspin2/result.ref— the previous values encoded thebuggy LCAO result.
tests/17_DS_DFTU/— five PW cases carriedresult.refvalues written in #7382that no longer matched the code:
12_PW_DS_S2_Z18_PW_DFTU_DS_S2_Z19_PW_DFTU_DS_S4_XY21_PW_DFTU_DS_S4_Z41_PW_DS_S4_Thr10_XY12_PW_DS_S2_Znow agrees with the dedicatedtests/01_PW/scf_deltaspin2reference (-6369.19826815 eV), confirming the refreshed value is the intended one.
Verification
tests/03_NAO_multik/scf_deltaspin2: etot / etotperatom / force / stress pass.tests/01_PW/scf_deltaspin2: etot / etotperatom / force / stress pass.tests/17_DS_DFTU12,18,08,11,19,21,41: pass.Agent Governancechecker:python3 tools/03_code_analysis/agent_governance_check.py --base <merge-base> --head <head>exits 0 (only the non-blocking "Documentation sync review" warning remains).
abacus_basic_para).Files changed
source/source_lcao/module_operator_lcao/operator_lcao.cppcurrent_spinthrough the operator chainsource/source_lcao/module_deltaspin/spin_constrain.hget_iat()constsource/source_lcao/module_deltaspin/lambda_loop_helper.cppprint_Mi/print_Mag_Forcesource/source_pw/module_pwdft/onsite_proj.hcal_occupations()takesnspinsource/source_pw/module_pwdft/onsite_proj_overlap.cppnpol=1becp indexing / Pauli blockssource/source_pw/module_pwdft/onsite_proj_print.cppprint_orb_chgsource/source_io/module_ctrl/ctrl_output_pw.cppinp.nspintocal_occupationstests/03_NAO_multik/scf_deltaspin2/result.reftests/17_DS_DFTU/{12,18,19,21,41}_*/result.refNotes
8c838a293; the PW fixcorresponds to accel's
8801a66ee.17_DS_DFTUPW references are intentionally refreshed in this PR; theyare test-data-only changes.