Skip to content

Commit 4f17ffd

Browse files
authored
Merge branch 'deepmodeling:develop' into dfpt-csj
2 parents c42f4f2 + f29968d commit 4f17ffd

13 files changed

Lines changed: 251 additions & 58 deletions

File tree

source/source_cell/module_symmetry/test/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ AddTest(
2121
TARGET MODULE_CELL_SYMMETRY_rho_soc
2222
LIBS parameter base ${math_libs} device symmetry
2323
SOURCES symm_rho_soc_test.cpp
24+
${ABACUS_SOURCE_DIR}/source_estate/module_dm/density_matrix.cpp
25+
${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/base_matrix.cpp
26+
${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/hcontainer.cpp
27+
${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/atom_pair.cpp
28+
${ABACUS_SOURCE_DIR}/source_basis/module_ao/parallel_orbitals.cpp
2429
)

source/source_cell/module_symmetry/test/symm_rho_soc_test.cpp

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "../symmetry.h"
77
#include "../symm_rot_spin.h"
88
#include "source_cell/unitcell.h"
9+
#include "source_estate/module_dm/density_matrix.h" // real func_xyz_to_updown
910

1011
/************************************************
1112
* unit test of Symmetry::rhog_symmetry_nspin4
@@ -187,30 +188,37 @@ TEST(RhogSymmetrySoc, GroupInvariance)
187188
}
188189

189190
// ---------------------------------------------------------------------------
190-
// Coupling test (nonzero m_y): the spin-density rotation W used for the grid
191-
// symmetrization (spin_so3) MUST agree with the SU(2) rotation of the spinor
192-
// density block followed by the Pauli decomposition convention that the rest of
193-
// the code uses (func_xyz_to_updown, #7664):
194-
// rho_0 = Re(uu+dd), rho_x = Re(ud+du),
195-
// rho_y = -Im(ud) + Im(du), rho_z = Re(uu-dd).
196-
// This is the check that the self-referential GroupInvariance test above cannot
197-
// make (it uses the same wspin as oracle). A y-channel handedness mismatch
198-
// between spin_so3 and this sigma_y=[[0,-i],[i,0]] convention shows up here.
191+
// Coupling test (nonzero m_y): the spin-density rotation W=spin_so3 used by psymmg_soc for the
192+
// grid symmetrization MUST agree with the SU(2) rotation of the physical spinor state followed by
193+
// the REAL func_xyz_to_updown extraction (which reads the conj-first stored DM, DM=conj(P), and
194+
// uses the bare +Im(ud)-Im(du)). This test now calls the actual func_xyz_to_updown rather than a
195+
// local re-implementation, so the grid-rotation and DM-extraction conventions cannot drift apart
196+
// silently (it fails on the #7664 m_y flip). The self-referential GroupInvariance test above
197+
// cannot catch this because it uses the same wspin as its own oracle.
199198
// ---------------------------------------------------------------------------
200199
namespace
201200
{
202201
using cd = std::complex<double>;
203-
// spinor block D = r0*I + m.sigma (sigma_y = [[0,-i],[i,0]]); layout {uu,ud,du,dd}
202+
// PHYSICAL spinor block P = r0*I + m.sigma (sigma_y = [[0,-i],[i,0]]); layout {uu,ud,du,dd}
204203
ModuleSymmetry::SpinRotation::Su2 block_from_pauli(double r0, double mx, double my, double mz)
205204
{
206205
return {cd(r0 + mz, 0.0), cd(mx, -my), cd(mx, my), cd(r0 - mz, 0.0)};
207206
}
208-
// func_xyz_to_updown extraction (NEW / #7664 convention); factor of 2 vs. m is harmless.
209-
void pauli_from_block(const ModuleSymmetry::SpinRotation::Su2& D, double& rx, double& ry, double& rz)
207+
// The runtime stores the DM conj-first (DM = conj(P), cal_dm_psi); this is what func_xyz_to_updown
208+
// actually consumes. Given a physical block P, the stored block is its element-wise conjugate.
209+
ModuleSymmetry::SpinRotation::Su2 stored_dm_from_phys(const ModuleSymmetry::SpinRotation::Su2& P)
210210
{
211-
rx = (D[1] + D[2]).real(); // Re(ud+du)
212-
ry = -D[1].imag() + D[2].imag(); // -Im(ud)+Im(du)
213-
rz = (D[0] - D[3]).real(); // Re(uu-dd)
211+
return {std::conj(P[0]), std::conj(P[1]), std::conj(P[2]), std::conj(P[3])};
212+
}
213+
// call the REAL func_xyz_to_updown on a 2x2 stored-DM block; return (m_x, m_y, m_z)
214+
ModuleBase::Vector3<double> real_extract(const ModuleSymmetry::SpinRotation::Su2& Dstored)
215+
{
216+
const cd tmp[4] = {Dstored[0], Dstored[1], Dstored[2], Dstored[3]}; // {uu,ud,du,dd}
217+
const int col_size = 2;
218+
const int step_trace[4] = {0, 1, col_size, col_size + 1};
219+
double out[4] = {0.0, 0.0, 0.0, 0.0}; // rho0/x/y/z written at icol=0
220+
elecstate::DensityMatrix_Tools::func_xyz_to_updown<double>(tmp, 0, step_trace, out);
221+
return ModuleBase::Vector3<double>(out[step_trace[1]], out[step_trace[2]], out[step_trace[3]]);
214222
}
215223
} // namespace
216224

@@ -233,19 +241,30 @@ TEST(RhogSymmetrySoc, SpinConventionCoupling)
233241
EXPECT_NEAR(Wgrid.e31, Wpauli.e31, TOL) << "g=" << g; EXPECT_NEAR(Wgrid.e32, Wpauli.e32, TOL) << "g=" << g;
234242
EXPECT_NEAR(Wgrid.e33, Wpauli.e33, TOL) << "g=" << g;
235243

236-
// (2) rotate the spinor block, extract Pauli comps (new convention), compare to Wgrid*m
244+
// (2) End-to-end with the REAL func_xyz_to_updown, exactly the runtime data flow:
245+
// physical block P(m) --conj--> stored DM (conj-first) --func_xyz_to_updown--> grid m.
246+
// Rotate the PHYSICAL block by the spinor SU(2) U (U P U^dagger, i.e. the physical state
247+
// rotation), conj to the stored block, extract again -> m'. psymmg_soc rotates the grid
248+
// components with Wgrid=spin_so3, so we must have m' == Wgrid * m. This catches any
249+
// mismatch (e.g. the #7664 m_y flip) between func_xyz_to_updown and spin_so3.
237250
for (const auto& m : mtest)
238251
{
239-
const ModuleSymmetry::SpinRotation::Su2 D = block_from_pauli(2.0, m[0], m[1], m[2]);
240-
const ModuleSymmetry::SpinRotation::Su2 Dp = ModuleSymmetry::SpinRotation::rotate_spin_block(D, U);
241-
double rx, ry, rz;
242-
pauli_from_block(Dp, rx, ry, rz);
243-
// Wgrid acts on the physical m; the block carries 2*m, so compare against 2*(Wgrid*m).
244-
const ModuleBase::Vector3<double> mv(m[0], m[1], m[2]);
245-
const ModuleBase::Vector3<double> mrot = Wgrid * mv;
246-
EXPECT_NEAR(rx, 2.0 * mrot.x, TOL) << "g=" << g;
247-
EXPECT_NEAR(ry, 2.0 * mrot.y, TOL) << "g=" << g << " (y-channel handedness)";
248-
EXPECT_NEAR(rz, 2.0 * mrot.z, TOL) << "g=" << g;
252+
const ModuleSymmetry::SpinRotation::Su2 P = block_from_pauli(2.0, m[0], m[1], m[2]);
253+
const ModuleSymmetry::SpinRotation::Su2 Pp = ModuleSymmetry::SpinRotation::rotate_spin_block(P, U);
254+
255+
const ModuleBase::Vector3<double> mF = real_extract(stored_dm_from_phys(P));
256+
const ModuleBase::Vector3<double> mFp = real_extract(stored_dm_from_phys(Pp));
257+
258+
// (2a) extraction recovers the physical magnetization (block carries 2*m)
259+
EXPECT_NEAR(mF.x, 2.0 * m[0], TOL) << "g=" << g;
260+
EXPECT_NEAR(mF.y, 2.0 * m[1], TOL) << "g=" << g << " (m_y extraction)";
261+
EXPECT_NEAR(mF.z, 2.0 * m[2], TOL) << "g=" << g;
262+
263+
// (2b) grid rotation spin_so3 agrees with the SU(2) block rotation + real extraction
264+
const ModuleBase::Vector3<double> mrot = Wgrid * mF;
265+
EXPECT_NEAR(mFp.x, mrot.x, TOL) << "g=" << g;
266+
EXPECT_NEAR(mFp.y, mrot.y, TOL) << "g=" << g << " (y-channel handedness)";
267+
EXPECT_NEAR(mFp.z, mrot.z, TOL) << "g=" << g;
249268
}
250269
}
251270
}

source/source_estate/module_charge/symm_rhog.cpp

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,16 @@ void Symmetry_rho::psymmg_soc(std::complex<double>* rhog_x, std::complex<double>
9090
// index [0,nrotk) unitary, [nrotk, nrotk+nrotk_anti) the spatial parts of the
9191
// antiunitary elements Theta*g -- same layout as density_sym_ops().
9292
const int na = symm.magnetic_nspin4 ? symm.nrotk_anti : 0;
93-
// PR 7664 flipped the spinor->Pauli rho^y sign (func_xyz_to_updown), so the grid
94-
// magnetization rho^y = chr.rho[2] consumed by rhog_symmetry_nspin4 now uses the
95-
// standard sigma_y=[[0,-i],[i,0]] convention. Relative to that convention the
96-
// pseudovector rotation applied to (rho^x,rho^y,rho^z) must be conjugated by
97-
// S=diag(1,-1,1) (i.e. the y-channel handedness is flipped); without this the
98-
// density symmetrization is out of sync only in the transverse y-channel.
99-
// spin_so3() itself is left untouched (it is still the physical pseudovector
100-
// rotation used for magnetic-group detection on the STRU moments).
101-
auto yflip = [](ModuleBase::Matrix3 W) {
102-
W.e12 = -W.e12; W.e21 = -W.e21; W.e23 = -W.e23; W.e32 = -W.e32; return W; };
10393
std::vector<ModuleBase::Matrix3> wspin(symm.nrotk + na);
10494
for (int i = 0; i < symm.nrotk; ++i)
10595
{
10696
const ModuleBase::Matrix3 gmatc = ilatvec * symm.gmatrix[i] * latvec;
107-
wspin[i] = yflip(ModuleSymmetry::SpinRotation::spin_so3(gmatc));
97+
wspin[i] = ModuleSymmetry::SpinRotation::spin_so3(gmatc);
10898
}
10999
for (int j = 0; j < na; ++j)
110100
{
111101
const ModuleBase::Matrix3 gmatc = ilatvec * symm.gmatrix_anti[j] * latvec;
112-
wspin[symm.nrotk + j] = yflip(ModuleSymmetry::SpinRotation::spin_so3(gmatc));
102+
wspin[symm.nrotk + j] = ModuleSymmetry::SpinRotation::spin_so3(gmatc);
113103
}
114104
return wspin;
115105
};

source/source_estate/module_dm/density_matrix.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,12 @@ void DensityMatrix_Tools::func_xyz_to_updown<double>(const std::complex<double>
655655
{
656656
target_DMR_mat[icol + step_trace[0]] = tmp[0].real() + tmp[3].real(); // rho_0 = (rho_upup + rho_downdown).real()
657657
target_DMR_mat[icol + step_trace[1]] = tmp[1].real() + tmp[2].real(); // rho_x = (rho_updown + rho_downup).real()
658-
target_DMR_mat[icol + step_trace[2]] = -tmp[1].imag() + tmp[2].imag(); // rho_y = -Im(rho_updown) + Im(rho_downup)
658+
// rho_y: the stored DM block is the complex conjugate of the physical 1-RDM P (cal_dm_psi builds
659+
// DM_{ab}=sum conj(c_a) c_b = conj(P), so tmp[1]=DM_{ud}=conj(P_{ud})). Extracting m_y from the
660+
// CONJUGATED block therefore carries the opposite sign of the bare-textbook formula; m_x/m_z read
661+
// Re() and are conjugation-invariant. Using the bare formula (PR #7664) sign-flips m_y and quenches
662+
// in-plane non-collinear moments (e.g. Mn3Sn 120-deg AFM); see issue #7831.
663+
target_DMR_mat[icol + step_trace[2]] = tmp[1].imag() - tmp[2].imag(); // rho_y = Im(P_updown) - Im(P_downup)
659664
target_DMR_mat[icol + step_trace[3]] = tmp[0].real() - tmp[3].real(); // rho_z = (rho_upup - rho_downdown).real()
660665
}
661666

@@ -664,7 +669,8 @@ void DensityMatrix_Tools::func_xyz_to_updown<std::complex<double>>(const std::co
664669
{
665670
target_DMR_mat[icol + step_trace[0]] = tmp[0] + tmp[3]; // rho_0 = (rho_upup + rho_downdown)
666671
target_DMR_mat[icol + step_trace[1]] = tmp[1] + tmp[2]; // rho_x = (rho_updown + rho_downup)
667-
target_DMR_mat[icol + step_trace[2]] = ModuleBase::IMAG_UNIT * (tmp[1] - tmp[2]); // rho_y = i*(rho_updown - rho_downup)
672+
// rho_y sign accounts for the conjugated stored DM block (conj(P)); see the <double> specialization above.
673+
target_DMR_mat[icol + step_trace[2]] = -ModuleBase::IMAG_UNIT * (tmp[1] - tmp[2]); // rho_y = -i*(rho_updown - rho_downup)
668674
target_DMR_mat[icol + step_trace[3]] = tmp[0] - tmp[3]; // rho_z = (rho_upup - rho_downdown)
669675
}
670676

source/source_estate/module_dm/test/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,13 @@ AddTest(
4848
${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/atom_pair.cpp
4949
${ABACUS_SOURCE_DIR}/source_basis/module_ao/parallel_orbitals.cpp
5050
)
51+
52+
AddTest(
53+
TARGET MODULE_ESTATE_dm_soc_magnetization_roundtrip_test
54+
LIBS parameter base device
55+
SOURCES test_soc_magnetization_roundtrip.cpp ../density_matrix.cpp ../density_matrix_io.cpp tmp_mocks.cpp
56+
${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/base_matrix.cpp
57+
${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/hcontainer.cpp
58+
${ABACUS_SOURCE_DIR}/source_hamilt/module_hcontainer/atom_pair.cpp
59+
${ABACUS_SOURCE_DIR}/source_basis/module_ao/parallel_orbitals.cpp
60+
)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#include "gtest/gtest.h"
2+
#include "source_estate/module_dm/density_matrix.h"
3+
4+
#include <complex>
5+
#include <cmath>
6+
7+
/************************************************************************
8+
* Regression test for the nspin=4 (non-collinear/SOC) magnetization
9+
* round-trip through the density-matrix pipeline.
10+
*
11+
* Physical invariant (must hold regardless of internal sign conventions):
12+
* the magnetization <sigma> of the occupied one-electron state that is
13+
* encoded in the density matrix must be recovered, with the CORRECT SIGN
14+
* in ALL THREE cartesian components, by func_xyz_to_updown().
15+
*
16+
* Why this test exists (regression for the #7664 nspin=4 m_y sign flip):
17+
* ABACUS builds the k-space DM as DM_{ab} = sum_n w_n conj(c_{n,a}) c_{n,b}
18+
* (cal_dm_psi.cpp: the conj() is applied to the FIRST index a). Hence the
19+
* stored DM block is the complex conjugate of the physical 1-RDM P:
20+
* DM_{up,dn} = conj(c_up) c_dn = conj(P_{up,dn}).
21+
* Since m_x, m_z read Re() (conjugation-invariant) but m_y reads Im(),
22+
* ONLY m_y is sensitive to this conjugation. func_xyz_to_updown() must be
23+
* consistent with that stored convention. PR #7664 set the m_y extraction
24+
* to the "bare" textbook formula (valid for P, not for conj(P)), which
25+
* flips m_y for in-plane moments and quenches non-collinear order
26+
* (e.g. Mn3Sn 120-degree AFM). This test pins m_y down.
27+
*
28+
* The helper build_DM_block_as_cal_dm_psi() MUST mirror cal_dm_psi.cpp. If
29+
* that convention is ever changed (e.g. the "upstream" fix that makes the DM
30+
* hold the physical P), update the helper in the SAME commit so this test
31+
* keeps asserting the physical invariant.
32+
************************************************************************/
33+
34+
namespace
35+
{
36+
using cd = std::complex<double>;
37+
38+
// spinor of the occupied state with <sigma> = mhat (the +1 eigenstate of mhat.sigma)
39+
void spinor_from_direction(const double mhat[3], cd c[2])
40+
{
41+
// |+n> = (cos(th/2), sin(th/2) e^{i ph}); n=(sin th cos ph, sin th sin ph, cos th)
42+
const double th = std::acos(std::max(-1.0, std::min(1.0, mhat[2])));
43+
const double ph = std::atan2(mhat[1], mhat[0]);
44+
c[0] = cd(std::cos(0.5 * th), 0.0);
45+
c[1] = std::sin(0.5 * th) * cd(std::cos(ph), std::sin(ph));
46+
}
47+
48+
// Build the 4 spinor-block DM elements EXACTLY as cal_dm_psi.cpp stores them:
49+
// DM_{a,b} = sum_occ w * conj(c_a) * c_b (conj on the first index)
50+
// layout tmp = {uu, ud, du, dd}
51+
void build_DM_block_as_cal_dm_psi(const cd c[2], double w, cd tmp[4])
52+
{
53+
tmp[0] = w * std::conj(c[0]) * c[0]; // uu
54+
tmp[1] = w * std::conj(c[0]) * c[1]; // ud
55+
tmp[2] = w * std::conj(c[1]) * c[0]; // du
56+
tmp[3] = w * std::conj(c[1]) * c[1]; // dd
57+
}
58+
59+
// physical magnetization of a normalized spinor: m_i = <c| sigma_i |c>
60+
void physical_m(const cd c[2], double m[3])
61+
{
62+
m[0] = 2.0 * std::real(std::conj(c[0]) * c[1]);
63+
m[1] = 2.0 * std::imag(std::conj(c[0]) * c[1]);
64+
m[2] = std::norm(c[0]) - std::norm(c[1]);
65+
}
66+
} // namespace
67+
68+
TEST(SocMagnetizationRoundtrip, ExtractRecoversPhysicalMagnetization)
69+
{
70+
// several magnetization directions, all with a nonzero transverse (y) part
71+
const double dirs[5][3] = {
72+
{0.0, 1.0, 0.0}, // pure +y (the critical case)
73+
{0.0, -1.0, 0.0}, // pure -y (like Mn3Sn atom-1)
74+
{0.6, 0.8, 0.0}, // in-plane 120-deg-like
75+
{0.36, 0.48, -0.8}, // general 3D
76+
{-0.5, 0.5, 0.70710678}, // general 3D
77+
};
78+
79+
// step_trace for a single 2x2 spinor block written contiguously as a 2x2 (col_size=2)
80+
const int col_size = 2;
81+
const int step_trace[4] = {0, 1, col_size, col_size + 1};
82+
83+
for (const auto& mhat : dirs)
84+
{
85+
cd c[2];
86+
spinor_from_direction(mhat, c);
87+
88+
double m_ref[3];
89+
physical_m(c, m_ref); // the TRUE magnetization encoded in the state
90+
91+
cd tmp[4];
92+
build_DM_block_as_cal_dm_psi(c, 1.0, tmp);
93+
94+
// 2x2 output buffer (row-major), func writes rho0/x/y/z into step_trace slots at icol=0
95+
double out[4] = {0, 0, 0, 0};
96+
elecstate::DensityMatrix_Tools::func_xyz_to_updown<double>(tmp, 0, step_trace, out);
97+
98+
const double mx = out[step_trace[1]];
99+
const double my = out[step_trace[2]];
100+
const double mz = out[step_trace[3]];
101+
102+
EXPECT_NEAR(mx, m_ref[0], 1e-10) << "m_x wrong for dir (" << mhat[0] << "," << mhat[1] << "," << mhat[2] << ")";
103+
EXPECT_NEAR(my, m_ref[1], 1e-10) << "m_y SIGN/VALUE wrong (transverse channel, #7664 regression) for dir ("
104+
<< mhat[0] << "," << mhat[1] << "," << mhat[2] << ")";
105+
EXPECT_NEAR(mz, m_ref[2], 1e-10) << "m_z wrong for dir (" << mhat[0] << "," << mhat[1] << "," << mhat[2] << ")";
106+
}
107+
}
108+
109+
// Same invariant for the <complex> (multi-k) specialization, which is changed identically.
110+
// For a single occupied state the 2x2 block is Hermitian, so the extracted Pauli components come
111+
// out real and must equal the physical magnetization; the imaginary parts must vanish.
112+
TEST(SocMagnetizationRoundtrip, ComplexSpecializationRecoversPhysicalMagnetization)
113+
{
114+
const double dirs[4][3] = {
115+
{0.0, 1.0, 0.0}, {0.0, -1.0, 0.0}, {0.6, 0.8, 0.0}, {0.36, 0.48, -0.8},
116+
};
117+
const int col_size = 2;
118+
const int step_trace[4] = {0, 1, col_size, col_size + 1};
119+
120+
for (const auto& mhat : dirs)
121+
{
122+
cd c[2];
123+
spinor_from_direction(mhat, c);
124+
double m_ref[3];
125+
physical_m(c, m_ref);
126+
127+
cd tmp[4];
128+
build_DM_block_as_cal_dm_psi(c, 1.0, tmp);
129+
130+
cd out[4] = {cd(0, 0), cd(0, 0), cd(0, 0), cd(0, 0)};
131+
elecstate::DensityMatrix_Tools::func_xyz_to_updown<std::complex<double>>(tmp, 0, step_trace, out);
132+
133+
EXPECT_NEAR(out[step_trace[1]].real(), m_ref[0], 1e-10) << "m_x";
134+
EXPECT_NEAR(out[step_trace[2]].real(), m_ref[1], 1e-10) << "m_y (complex specialization)";
135+
EXPECT_NEAR(out[step_trace[3]].real(), m_ref[2], 1e-10) << "m_z";
136+
EXPECT_NEAR(out[step_trace[1]].imag(), 0.0, 1e-10);
137+
EXPECT_NEAR(out[step_trace[2]].imag(), 0.0, 1e-10);
138+
EXPECT_NEAR(out[step_trace[3]].imag(), 0.0, 1e-10);
139+
}
140+
}
141+
142+
int main(int argc, char** argv)
143+
{
144+
testing::InitGoogleTest(&argc, argv);
145+
return RUN_ALL_TESTS();
146+
}

source/source_hamilt/module_xc/xc_grad.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
#include "xc_grad_internal.h"
1818
#include "source_base/timer.h"
1919

20+
#ifdef __LIBXC
21+
#include <xc_funcs.h>
22+
#endif
23+
2024
void XC_Functional::gradcorr(
2125
double &etxc,
2226
double &vtxc,

source/source_io/module_mulliken/output_mulliken.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ void Output_Mulliken<TK>::collect_MW(ModuleBase::matrix& MecMulP, const ModuleBa
438438
MecMulP(0, j) += mud(ic, ir).real();
439439
MecMulP(3, j) += mud(ic, ir).real();
440440
}
441+
// WARNING (pre-existing, predates #7664, since 3.7.0 commit a339356): this M_y sign
442+
// is suspect. mud is DM.S with the same conj-first DM convention as cal_dm_psi, so the
443+
// bare Im formula here likely yields -m_y. It must NOT be used as an oracle for the DM
444+
// convention (it is probably why #7664's DM m_y flip went unnoticed). Fix + unit test
445+
// should be a separate PR after verifying against a case with nonzero in-plane moment.
441446
if (this->ParaV_->in_this_processor(k1, k2))
442447
{
443448
const int ir = this->ParaV_->global2local_row(k1);

0 commit comments

Comments
 (0)