Skip to content

Commit a350572

Browse files
committed
feat: SSG for nspin=4: Rm(i)=m(gi) fitting
1 parent c88ddb6 commit a350572

10 files changed

Lines changed: 425 additions & 128 deletions

File tree

source/Makefile.Objects

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ OBJS_SYMMETRY=symm_other.o\
523523
symm_hermite.o\
524524
symm_lattice.o\
525525
symm_magnetic.o\
526+
symm_spin_group.o\
526527
symm_pricell.o\
527528
symm_rho.o\
528529
symmetry.o\

source/source_cell/module_symmetry/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ add_library(
99
symm_hermite.cpp
1010
symm_lattice.cpp
1111
symm_magnetic.cpp
12+
symm_spin_group.cpp
1213
symm_pricell.cpp
1314
symm_rho.cpp
1415
symmetry.cpp

source/source_cell/module_symmetry/symm_analysis.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,17 @@ void Symmetry::analy_sys(const Lattice& lat, const Statistics& st, Atom* atoms,
304304
// the magnetization (pseudovector), so they are not applied in k-reduction / density symmetrization.
305305
if (nspin == 4)
306306
{
307-
this->analyze_magnetic_group_nspin4(atoms, st, latvec1);
307+
// (SSG, SOC off) decouple the spin rotation from space: fit an independent R_spin per op,
308+
// keeping more operations than the magnetic (Shubnikov) subgroup. Falls back to the magnetic
309+
// path when SOC is on (spin locked to space) or symmetry_ssg is disabled -> bit-for-bit unchanged.
310+
if (PARAM.inp.symmetry_ssg && !PARAM.inp.lspinorb)
311+
{
312+
this->analyze_spin_space_group_nspin4(atoms, st, latvec1);
313+
}
314+
else
315+
{
316+
this->analyze_magnetic_group_nspin4(atoms, st, latvec1);
317+
}
308318
}
309319

310320
// (nspin=2 collinear spin space group) split the full chemical group into the unitary magnetic

source/source_cell/module_symmetry/symm_magnetic.cpp

Lines changed: 0 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -188,101 +188,6 @@ void Symmetry::analyze_magnetic_group_nspin4(const Atom* atoms, const Statistics
188188
ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "MAGNETIC SPACE GROUP OPERATIONS", this->nrotk);
189189
}
190190

191-
void Symmetry::analyze_spin_space_group_nspin2(const Atom* atoms, const Statistics& st)
192-
{
193-
// (nspin=2 collinear spin space group) The FULL chemical space group is already in
194-
// gmatrix[0..nrotk). Split it, using the scalar collinear moments mag[iat], into:
195-
// - unitary magnetic subgroup : mag[iat] == mag[g(iat)] for every atom -> kept in gmatrix
196-
// - unitary spin-flip coset : mag[iat] == -mag[g(iat)] for every atom -> gmatrix_flip[]
197-
// The spin-flip coset elements are [C2_perp||g]: they swap the up/down spin channels while
198-
// rotating space by g. There is NO time reversal here (the collinear Hamiltonian is real, the two
199-
// spin blocks are independent), so this is a plain channel swap - distinct from the antiunitary
200-
// Theta*g coset built by analyze_magnetic_group_nspin4. Operations that neither preserve nor exactly
201-
// flip the moment pattern are dropped.
202-
std::vector<int> keep; // unitary magnetic subgroup
203-
keep.reserve(this->nrotk);
204-
std::vector<int> flip; // unitary spin-flip coset [C2_perp||g]
205-
flip.reserve(this->nrotk);
206-
int nrot_new = 0;
207-
for (int isym = 0; isym < this->nrotk; ++isym)
208-
{
209-
bool preserve = true;
210-
for (int iat = 0; iat < this->nat && preserve; ++iat)
211-
{
212-
const double mi = atoms[st.iat2it[iat]].mag[st.iat2ia[iat]];
213-
const int jat = this->get_rotated_atom(isym, iat);
214-
const double mj = atoms[st.iat2it[jat]].mag[st.iat2ia[jat]];
215-
if (!this->equal(mi, mj)) { preserve = false; }
216-
}
217-
if (preserve)
218-
{
219-
keep.push_back(isym);
220-
if (isym < this->nrot) { ++nrot_new; } // pure point-group rotations are the first nrot ops
221-
continue;
222-
}
223-
// g does not preserve the moment pattern; check whether it exactly FLIPS it, i.e.
224-
// mag[iat] = -mag[g(iat)] for every atom. Then [C2_perp||g] (spatial g + up<->down swap) is a symmetry.
225-
bool flip_ok = true;
226-
for (int iat = 0; iat < this->nat && flip_ok; ++iat)
227-
{
228-
const double mi = atoms[st.iat2it[iat]].mag[st.iat2ia[iat]];
229-
const int jat = this->get_rotated_atom(isym, iat);
230-
const double mj = atoms[st.iat2it[jat]].mag[st.iat2ia[jat]];
231-
if (!this->equal(mi, -mj)) { flip_ok = false; }
232-
}
233-
if (flip_ok) { flip.push_back(isym); }
234-
}
235-
236-
// Capture the spin-flip coset BEFORE the unitary arrays are compacted in place below.
237-
this->nrotk_flip = static_cast<int>(flip.size());
238-
this->spin_flip_nspin2 = (this->nrotk_flip > 0);
239-
if (this->nrotk_flip > 0)
240-
{
241-
this->isym_rotiat_flip_.resize(this->nrotk_flip);
242-
for (int j = 0; j < this->nrotk_flip; ++j)
243-
{
244-
const int isym = flip[j];
245-
this->gmatrix_flip[j] = this->gmatrix[isym];
246-
this->kgmatrix_flip[j] = this->kgmatrix[isym];
247-
this->gtrans_flip[j] = this->gtrans[isym];
248-
this->isym_rotiat_flip_[j] = this->isym_rotiat_[isym];
249-
}
250-
ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running,
251-
"SPIN-FLIP COSET OPERATIONS (nspin=2 SSG)", this->nrotk_flip);
252-
}
253-
254-
const int nrotk_new = static_cast<int>(keep.size());
255-
if (nrotk_new != this->nrotk)
256-
{
257-
// compact the unitary subgroup in ascending order (keeps the rotations-first layout).
258-
for (int i = 0; i < nrotk_new; ++i)
259-
{
260-
const int isym = keep[i];
261-
if (i != isym)
262-
{
263-
this->gmatrix[i] = this->gmatrix[isym];
264-
this->kgmatrix[i] = this->kgmatrix[isym];
265-
this->gtrans[i] = this->gtrans[isym];
266-
this->isym_rotiat_[i] = this->isym_rotiat_[isym];
267-
}
268-
}
269-
this->isym_rotiat_.resize(nrotk_new);
270-
this->nrot = nrot_new;
271-
this->nrotk = nrotk_new;
272-
273-
// refresh the point-/space-group labels for the reduced (unitary magnetic) group
274-
this->pointgroup(this->nrot, this->pgnumber, this->pgname, this->gmatrix, GlobalV::ofs_running, nullptr);
275-
this->pointgroup(this->nrotk, this->spgnumber, this->spgname, this->gmatrix, GlobalV::ofs_running, nullptr);
276-
ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "MAGNETIC POINT GROUP (unitary, nspin=2)", this->pgname);
277-
ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running, "MAGNETIC SPACE GROUP OPERATIONS", this->nrotk);
278-
}
279-
if (this->spin_flip_nspin2)
280-
{
281-
ModuleBase::GlobalFunc::OUT(GlobalV::ofs_running,
282-
"SPIN SPACE GROUP OPERATIONS (unitary + spin-flip)", this->nrotk + this->nrotk_flip);
283-
}
284-
}
285-
286191
bool Symmetry::magmom_same_check(const Atom* atoms)const
287192
{
288193
ModuleBase::TITLE("Symmetry", "magmom_same_check");
@@ -334,32 +239,3 @@ int Symmetry::density_sym_ops(std::vector<ModuleBase::Matrix3>& kgmat,
334239
return nu + na;
335240
}
336241

337-
int Symmetry::spin_flip_sym_ops(std::vector<ModuleBase::Matrix3>& kgmat,
338-
std::vector<ModuleBase::Vector3<double>>& gtr,
339-
std::vector<double>& flip_sign) const
340-
{
341-
// (nspin=2 collinear SSG) Assemble the full spin space group used to symmetrize the collinear
342-
// density and to fold the k-points: the nrotk unitary operations (flip_sign +1) followed by the
343-
// nrotk_flip spatial parts of the spin-flip coset [C2_perp||g] (flip_sign -1). The combined set
344-
// is the full chemical space group -- a group, closed under inverse -- so the invmap/grouping in
345-
// rhog_symmetry* remains valid. In the (charge, mag) basis the charge is invariant (all ops act
346-
// as ordinary space-group operations) and the magnetization flips sign under the coset.
347-
const int nu = this->nrotk;
348-
const int nf = (this->spin_flip_nspin2 ? this->nrotk_flip : 0);
349-
kgmat.resize(nu + nf);
350-
gtr.resize(nu + nf);
351-
flip_sign.assign(nu + nf, 1.0);
352-
for (int i = 0; i < nu; ++i)
353-
{
354-
kgmat[i] = this->kgmatrix[i];
355-
gtr[i] = this->gtrans[i];
356-
}
357-
for (int j = 0; j < nf; ++j)
358-
{
359-
kgmat[nu + j] = this->kgmatrix_flip[j];
360-
gtr[nu + j] = this->gtrans_flip[j];
361-
flip_sign[nu + j] = -1.0;
362-
}
363-
return nu + nf;
364-
}
365-

source/source_cell/module_symmetry/symm_rot_spin.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,60 @@ ModuleBase::Matrix3 spin_so3(const ModuleBase::Matrix3& gmatc)
125125
return proper_part(gmatc).Transpose();
126126
}
127127

128+
ModuleBase::Matrix3 fit_spin_rotation(const std::vector<ModuleBase::Vector3<double>>& from,
129+
const std::vector<ModuleBase::Vector3<double>>& to,
130+
bool& ok,
131+
const double tol)
132+
{
133+
ok = false;
134+
const ModuleBase::Matrix3 identity(1, 0, 0, 0, 1, 0, 0, 0, 1);
135+
const int n = static_cast<int>(from.size());
136+
if (n == 0 || static_cast<int>(to.size()) != n) { return identity; }
137+
138+
// A norm below this is treated as a zero (non-magnetic) moment: it constrains nothing.
139+
const double nrm_eps = 1e-6;
140+
141+
// 1st frame axis: the first non-zero moment.
142+
int a = -1;
143+
for (int i = 0; i < n; ++i) { if (from[i].norm() > nrm_eps) { a = i; break; } }
144+
if (a < 0) { return identity; } // all moments zero: no constraint (handled by has_moment gate upstream)
145+
const ModuleBase::Vector3<double> e1 = from[a] * (1.0 / from[a].norm());
146+
const ModuleBase::Vector3<double> f1 = to[a] * (1.0 / std::max(to[a].norm(), nrm_eps));
147+
148+
// 2nd frame axis: the first moment with a component perpendicular to e1 (rank >= 2).
149+
int b = -1;
150+
ModuleBase::Vector3<double> e2, f2;
151+
for (int i = 0; i < n; ++i)
152+
{
153+
const ModuleBase::Vector3<double> perp = from[i] - e1 * (e1 * from[i]);
154+
if (perp.norm() > nrm_eps) { b = i; e2 = perp * (1.0 / perp.norm()); break; }
155+
}
156+
if (b < 0) { return identity; } // rank-1 (collinear moments): degenerate, out of nspin=4 SSG scope
157+
const ModuleBase::Vector3<double> fperp = to[b] - f1 * (f1 * to[b]);
158+
if (fperp.norm() <= nrm_eps) { return identity; } // target lost its perpendicular part -> not a rotation
159+
f2 = fperp * (1.0 / fperp.norm());
160+
161+
// 3rd axis by right-handed cross product -> both frames right-handed -> det(R)=+1.
162+
const ModuleBase::Vector3<double> e3 = e1 ^ e2;
163+
const ModuleBase::Vector3<double> f3 = f1 ^ f2;
164+
165+
// R maps e_k -> f_k : R = F E^T, R_ij = sum_k f_k[i] e_k[j]. Acts as column vectors: R * m.
166+
const ModuleBase::Matrix3 R(
167+
f1.x * e1.x + f2.x * e2.x + f3.x * e3.x, f1.x * e1.y + f2.x * e2.y + f3.x * e3.y, f1.x * e1.z + f2.x * e2.z + f3.x * e3.z,
168+
f1.y * e1.x + f2.y * e2.x + f3.y * e3.x, f1.y * e1.y + f2.y * e2.y + f3.y * e3.y, f1.y * e1.z + f2.y * e2.z + f3.y * e3.z,
169+
f1.z * e1.x + f2.z * e2.x + f3.z * e3.x, f1.z * e1.y + f2.z * e2.y + f3.z * e3.y, f1.z * e1.z + f2.z * e2.z + f3.z * e3.z);
170+
171+
// Verify R exactly maps EVERY moment (this is what makes the fit correct AND, for rank>=2
172+
// moments, guarantees group closure: R is the unique proper map of the moment set).
173+
for (int i = 0; i < n; ++i)
174+
{
175+
const ModuleBase::Vector3<double> d = R * from[i] - to[i];
176+
if (std::fabs(d.x) > tol || std::fabs(d.y) > tol || std::fabs(d.z) > tol) { return identity; }
177+
}
178+
ok = true;
179+
return R;
180+
}
181+
128182
ModuleBase::Matrix3 pauli_rotation_matrix(const Su2& U)
129183
{
130184
// sigma matrices (row-major 2x2)

source/source_cell/module_symmetry/symm_rot_spin.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,29 @@ Su2 so3_to_su2(const ModuleBase::Matrix3& gmatc, const double eps = 1e-6);
5252
/// Computed directly from gmatc as W = R_proper^T (R_proper = proper part).
5353
ModuleBase::Matrix3 spin_so3(const ModuleBase::Matrix3& gmatc);
5454

55+
/// @brief (nspin=4 SSG) Fit the INDEPENDENT proper spin rotation R (det=+1) that maps one
56+
/// set of moment vectors onto another: R * from[i] == to[i] for all i, using the
57+
/// same column-vector action `R * m` as spin_so3() (so it drops in for W).
58+
///
59+
/// Unlike spin_so3(gmatc), R is NOT derived from any spatial operation -- it is found from
60+
/// the moment configuration alone, which is exactly the spin/space decoupling of the spin
61+
/// space group (valid with SOC off). Built by an orthonormal-frame (Gram-Schmidt) construction
62+
/// from two linearly independent moment pairs, so no SVD is needed and det(R)=+1 is guaranteed:
63+
/// e1=norm(from[a]), e2=norm(from[b] perp e1), e3=e1 x e2; f_k likewise from to[]; R = F E^T.
64+
/// Then R is VERIFIED to map every moment within `tol`; `ok` is set false if the verification
65+
/// fails or the moments are rank-deficient (collinear -> no independent second axis; that is the
66+
/// nspin=2 collinear regime, out of Phase-2 scope). A false `ok` means "reject this operation".
67+
///
68+
/// @param from source moments m_i (all atoms)
69+
/// @param to target moments (m_{g(i)} for the unitary test, -m_{g(i)} for the antiunitary test)
70+
/// @param[out] ok true iff a proper rotation exactly mapping all moments (within tol) was found
71+
/// @param tol absolute tolerance for the per-component moment match (default matches Symmetry::epsilon usage)
72+
/// @return the fitted rotation R (identity when ok is false; caller must check ok)
73+
ModuleBase::Matrix3 fit_spin_rotation(const std::vector<ModuleBase::Vector3<double>>& from,
74+
const std::vector<ModuleBase::Vector3<double>>& to,
75+
bool& ok,
76+
const double tol = 1e-5);
77+
5578
/// @brief The same W matrix computed independently from a given SU(2) matrix U via
5679
/// W_ij = (1/2) Tr(sigma_i U sigma_j U^dagger). Used for verification.
5780
ModuleBase::Matrix3 pauli_rotation_matrix(const Su2& U);

0 commit comments

Comments
 (0)