"""Ground-state ordering analysis: Luttinger-Tisza q₀ and spin direction assignment."""
from __future__ import annotations
import logging
from typing import Dict
import numpy as np
log = logging.getLogger(__name__)
[docs]
def analyse_ordering(Jq, evals, q_frac, q_cart, structure) -> Dict:
"""Find ``q₀ = argmin λ_min(q)`` and characterise the magnetic ordering.
The output dict contains:
``idx0, q0_cart, q0_frac, lam0, eigvec, ratios, character, sc_dims, commensurate``.
"""
lambda_min_q = evals[:, -1]
idx0 = int(np.argmin(lambda_min_q))
lam0 = float(lambda_min_q[idx0])
q0_c = q_cart[idx0]
q0_f = q_frac[idx0]
Jq_herm = 0.5 * (Jq[idx0] + Jq[idx0].T.conj())
_, ev_vecs = np.linalg.eigh(Jq_herm)
eigvec = ev_vecs[:, 0].real
def frac_to_ratio(x, max_denom=8):
x = float(x)
best, best_err = (0, 1), abs(x)
for m in range(1, max_denom + 1):
p = round(x * m)
err = abs(x - p / m)
if err < best_err:
best_err = err
best = (p, m)
return best
tol_q = 1.0 / (2 * max(len(lambda_min_q) ** (1/3), 10))
ratios = [frac_to_ratio(q) for q in q0_f]
sc_dims = [r[1] for r in ratios]
commensurate = all(abs(q0_f[i] - ratios[i][0]/ratios[i][1]) < tol_q
for i in range(3))
q0_norm = float(np.linalg.norm(q0_c))
if q0_norm < 0.01:
signs = np.sign(eigvec)
if np.all(signs == signs[0]):
character = "FM (Curie)"
else:
n_up = int(np.sum(signs > 0))
n_dn = int(np.sum(signs < 0))
character = f"Type-I AFM at q=0 [{n_up} up, {n_dn} down sublattice(s)]"
elif commensurate:
sc_str = "×".join(str(s) for s in sc_dims)
character = ("Commensurate ordering q₀=("
+ ", ".join(f"{p}/{m}" for p, m in ratios)
+ f") magnetic supercell: {sc_str}")
else:
character = ("Incommensurate spiral q₀=("
+ ", ".join(f"{q:.4f}" for q in q0_f)
+ ") — LSWT requires spin-spiral treatment")
return dict(idx0=idx0, q0_cart=q0_c, q0_frac=q0_f, lam0=lam0,
eigvec=eigvec, ratios=ratios, character=character,
sc_dims=sc_dims, commensurate=commensurate)
[docs]
def get_spin_dirs_from_q0(bond_data: Dict, q0_frac: np.ndarray) -> np.ndarray:
"""Assign collinear spin directions in the primitive cell from the ordering wavevector.
.. math:: s_α = \\mathrm{sign}\\bigl(\\cos(2π\\,q₀ · r_α)\\bigr)
where ``r_α`` is the fractional coordinate of site α. This is the TB2J
strategy: the LSWT dynamical matrix is then built directly in the
primitive cell, giving exactly ``n_sub`` magnon branches with a
Goldstone mode at ``k = q₀``. No supercell replication is required.
"""
prim_fracs = bond_data["prim_fracs"]
phases = 2.0 * np.pi * (prim_fracs @ q0_frac)
spin_dirs = np.sign(np.cos(phases))
spin_dirs[spin_dirs == 0.0] = 1.0
log.info(" Spin directions from cos(2π q₀·r_α): %d up, %d down",
int(np.sum(spin_dirs > 0)), int(np.sum(spin_dirs < 0)))
return spin_dirs