| """FourJ transforms between E(q), J(q), and J_ij.""" |
|
|
| from __future__ import annotations |
|
|
| import math |
| from dataclasses import dataclass |
|
|
| import numpy as np |
|
|
| from .constants import HARTREE_TO_MRY, MRY_TO_MEV |
|
|
|
|
| @dataclass(frozen=True) |
| class ExchangeTransformResult: |
| """Container for inverse-transform outputs. |
| |
| Attributes: |
| vectors: Integer real-space lattice vectors. |
| jij_mry: Pair exchange constants in mRy. |
| jq_delta_mry: Sampled :math:`J(0)-J(q)` values in mRy. |
| q: Symmetrized q-points in fractional reciprocal coordinates. |
| energy_hartree: Symmetrized energies in Hartree. |
| weights: Normalized q-integration weights. |
| """ |
|
|
| vectors: np.ndarray |
| jij_mry: np.ndarray |
| jq_delta_mry: np.ndarray |
| q: np.ndarray |
| energy_hartree: np.ndarray |
| weights: np.ndarray |
|
|
|
|
| class FrozenMagnonTransformer: |
| """Convert one-sublattice fourj energies to exchange quantities. |
| |
| Args: |
| theta_degrees: Spin-spiral cone angle in degrees. |
| e0_mode: Reference energy mode. `q0` uses the q-point nearest Gamma; |
| `min` uses the minimum energy in the supplied table. |
| """ |
|
|
| def __init__(self, theta_degrees: float = 90.0, e0_mode: str = "q0") -> None: |
| self.theta_degrees = theta_degrees |
| self.e0_mode = e0_mode |
|
|
| @property |
| def sin2(self) -> float: |
| value = math.sin(math.radians(self.theta_degrees)) ** 2 |
| if value <= 1e-14: |
| raise ValueError("theta gives sin(theta)^2 too close to zero") |
| return value |
|
|
| def reference_energy(self, q: np.ndarray, energy: np.ndarray) -> float: |
| """Return the reference energy used in `E(q)-E0`.""" |
| if self.e0_mode == "q0": |
| distances = np.linalg.norm(np.mod(q + 0.5, 1.0) - 0.5, axis=1) |
| return float(energy[int(np.argmin(distances))]) |
| if self.e0_mode == "min": |
| return float(np.min(energy)) |
| raise ValueError(f"Unknown e0 mode: {self.e0_mode}") |
|
|
| def inverse_transform(self, q: np.ndarray, energy: np.ndarray, weights: np.ndarray, vectors: np.ndarray) -> ExchangeTransformResult: |
| """Evaluate the inverse Fourier transform from `E(q)` to `J_ij`. |
| |
| Args: |
| q: q-points in fractional reciprocal coordinates. |
| energy: Energies in Hartree. |
| weights: Normalized q-integration weights. |
| vectors: Integer real-space vectors. |
| |
| Returns: |
| Exchange transform result with `J_ij` in mRy. |
| """ |
| j0_minus_jq_hartree = (energy - self.reference_energy(q, energy)) / self.sin2 |
| phase = np.exp(-2j * np.pi * (q @ vectors.T)) |
| jij = -(weights[:, None] * j0_minus_jq_hartree[:, None] * phase).sum(axis=0) * HARTREE_TO_MRY |
| return ExchangeTransformResult(vectors, jij, j0_minus_jq_hartree * HARTREE_TO_MRY, q, energy, weights) |
|
|
| def dft_spectrum_mry(self, q: np.ndarray, energy: np.ndarray) -> np.ndarray: |
| """Return `[E(q)-E0]/sin(theta)^2` in mRy.""" |
| return (energy - self.reference_energy(q, energy)) / self.sin2 * HARTREE_TO_MRY |
|
|
|
|
| class ExchangeSpectrum: |
| """Evaluate and scale spectra reconstructed from real-space exchange.""" |
|
|
| @staticmethod |
| def from_jij(q: np.ndarray, vectors: np.ndarray, jij_mry: np.ndarray) -> np.ndarray: |
| """Evaluate `J(0)-J(q)` from pair interactions in mRy.""" |
| phase = np.exp(2j * np.pi * (q @ vectors.T)) |
| return np.sum(jij_mry[None, :] * (1.0 - phase), axis=1).real |
|
|
| @staticmethod |
| def scale(spectrum_mry: np.ndarray, lswt_moment: float | None) -> tuple[np.ndarray, str, str]: |
| """Optionally apply the `4/M` LSWT prefactor and convert to meV.""" |
| if lswt_moment is None: |
| return spectrum_mry, r"$J(0)-J(q)$ from extracted $J_{ij}$ (mRy)", "exchange_mRy" |
| if lswt_moment <= 0.0: |
| raise ValueError("--lswt-moment must be positive") |
| scaled = (4.0 / lswt_moment) * spectrum_mry * MRY_TO_MEV |
| return scaled, rf"$4[J(0)-J(q)]/{lswt_moment:g}$ (meV)", "lswt_meV" |
|
|
|
|
| def symmetrize_real_space(vectors: np.ndarray, jij: np.ndarray) -> tuple[np.ndarray, np.ndarray]: |
| """Enforce the pair relation :math:`J(R)=J(-R)^*`. |
| |
| Args: |
| vectors: Integer real-space vectors. |
| jij: Complex exchange values aligned with `vectors`. |
| |
| Returns: |
| Symmetrized vectors and exchange values. |
| """ |
| values = {tuple(row): val for row, val in zip(vectors, jij)} |
| grouped = {} |
| for row, val in values.items(): |
| inv = tuple(-x for x in row) |
| vals = [val] |
| if inv in values: |
| vals.append(np.conjugate(values[inv])) |
| grouped[min(row, inv)] = np.mean(vals) |
|
|
| out_vectors, out_jij = [], [] |
| for key in sorted(grouped): |
| val = grouped[key] |
| for row, value in ((key, val), (tuple(-x for x in key), np.conjugate(val))): |
| if row in values and row not in out_vectors: |
| out_vectors.append(row) |
| out_jij.append(value) |
| return np.asarray(out_vectors, dtype=int), np.asarray(out_jij, dtype=complex) |
|
|