Source code for magnopy._lswt

# ================================== LICENSE ===================================
# Magnopy - Python package for magnons.
#
# Copyright (C) 2023 Magnopy Team
#
# e-mail: anry@uv.es, web: magnopy.org
#
# This program is free software: you  can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the  Free Software
# Foundation,  either  version 3  of the License,  or (at your option) any later
# version.
#
# This program is distributed in the  hope  that it will be useful,  but WITHOUT
# ANY WARRANTY;  without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the  GNU General Public License  along with
# this program.  If not, see <https://www.gnu.org/licenses/>.
# ================================ END LICENSE =================================


import numpy as np

from magnopy._diagonalization import solve_via_colpa
from magnopy._exceptions import ColpaFailed
from magnopy._local_rf import span_local_rfs

from magnopy._data_validation import _validated_units
from magnopy._constants._units import _ENERGY_UNITS, _MAGNON_ENERGY_UNITS
from magnopy._parameters._interaction_parameters import _InteractionParametersIterator
from magnopy._parameters._renormalization import _renormalized_parameters
from magnopy._spinham._convention import Convention


# Save local scope at this moment
old_dir = set(dir())
old_dir.add("old_dir")


[docs] class LSWT: r""" Linear Spin Wave theory. It is created from some spin Hamiltonian and set of direction vectors, that defines the ground state. Parameters ---------- spinham : :py:class:`.SpinHamiltonian` Spin Hamiltonian. spin_directions : (M, 3) |array-like|_ Directions of spin vectors. Only directions of vectors are used, modulus is ignored. If spin Hamiltonian contains non-magnetic atom, then only the spin directions for the magnetic atoms are expected. The order of spin directions is the same as the order of magnetic atoms in :py:attr:`SpinHamiltonian.magnetic_atoms`. See Notes for more details. Attributes ---------- z : (M, 3) :numpy:`ndarray` Spin directions (directions of local quantization axes). p : (M, 3) :numpy:`ndarray` Hybridized x and y components of the local coordinate system :math:`\mathbf{p} = \mathbf{x} + i \mathbf{y}`. M : int Number of spins in the unit cell cell : (3, 3) :numpy:`ndarray` Unit cell. Rows are vectors, columns are cartesian components. spins : (M, ) :numpy:`ndarray` Spin values of the magnetic centers. Notes ----- Let the spin Hamiltonian contain three atoms Cr1, Br, Cr3 in that order. Assume that two atoms are magnetic (Cr1 and Cr3), one atom is not (Br). Then ``spin_directions`` is a (2, 3) array with ``spin_directions[0]`` being the direction for spin of Cr1 and ``spin_directions[1]`` being the direction of spin for Cr3. Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) """ def __init__(self, spinham, spin_directions): spin_directions = np.array(spin_directions, dtype=float) spin_directions /= np.linalg.norm(spin_directions, axis=1)[:, np.newaxis] x, y, self.z = span_local_rfs( directional_vectors=spin_directions, hybridize=False ) self.p = x + 1j * y self.spins = np.array(spinham.magnetic_atoms.spins, dtype=float) self.M = spinham.M self.cell = spinham.cell initial_units = spinham.units initial_convention = spinham.convention self._convention = Convention( spin_normalized=False, multiple_counting=True, c1=1, c21=1, c22=1, c31=1, c32=1, c33=1, c41=1, c42=1, c43=1, c44=1, c45=1, ) spinham.units = "mev" spinham.convention = self._convention self._parameters = spinham._parameters.copy() spinham.units = initial_units spinham.convention = initial_convention for i, ((n, p_n, nus, alphas), _) in enumerate(self._parameters._container): alphas = tuple(spinham.map_to_magnetic[alpha] for alpha in alphas) self._parameters._container[i][0] = (n, p_n, nus, alphas) self._parameters = _renormalized_parameters( parameters=self._parameters, convention=self._convention, spin_directions=spin_directions, spin_values=self.spins, ) self.A1 = np.zeros((self.M), dtype=float) for _, alphas, parameter in _InteractionParametersIterator( self._parameters, n=1, p_n=1 ): self.A1[alphas[0]] = 0.5 * parameter @ self.z[alphas[0]] self.A2 = {} self.B2 = {} for nus, alphas, parameter in _InteractionParametersIterator( self._parameters, n=2 ): nu = nus[0] if nu not in self.A2: self.A2[nu] = np.zeros((self.M, self.M), dtype=complex) self.B2[nu] = np.zeros((self.M, self.M), dtype=complex) self.A2[nu][alphas[0], alphas[1]] = ( 0.5 * np.sqrt(self.spins[alphas[0]] * self.spins[alphas[1]]) * (self.p[alphas[0]] @ parameter @ np.conjugate(self.p[alphas[1]])) ) self.B2[nu][alphas[0], alphas[1]] = ( 0.5 * np.sqrt(self.spins[alphas[0]] * self.spins[alphas[1]]) * ( np.conjugate(self.p[alphas[0]])
[docs] @ parameter @ np.conjugate(self.p[alphas[1]]) ) ) def E_2(self, units="meV") -> float: r""" Computes the correction to the ground state energy that arises from the LSWT. Parameters ---------- units : str, default "meV" .. versionadded:: 0.3.0 Units of energy. See :ref:`user-guide_usage_units_energy` for the full list of supported units. Returns ------- E_2 : float Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> # Default units are meV >>> lswt.E_2() -1.5 """ result = float(np.sum(self.A1)) # Convert units if necessary if units != "meV": units = _validated_units(units=units, supported_units=_ENERGY_UNITS) result = result * _ENERGY_UNITS["mev"] / _ENERGY_UNITS[units] return result
[docs] def O(self, units="meV"): # noqa E743 r""" Computes coefficient of the one-operator terms. Parameters ---------- units : str, default "meV" .. versionadded:: 0.3.0 Units of energy. See :ref:`user-guide_usage_units_energy` for the full list of supported units. Returns ------- O : (M, ) :numpy:`ndarray` Elements are complex numbers. Notes ----- Before the diagonalization, the magnon Hamiltonian has the form .. math:: \mathcal{H} = \dots + \sqrt{N} \sum_{\alpha} \Bigl( O_{\alpha} a_{\alpha}(\boldsymbol{0}) + \overline{O_{\alpha}} a^{\dagger}_{\alpha}(\boldsymbol{0}) \Bigr) + \dots where overline denotes complex conjugation. This function computes the coefficients :math:`O_{\alpha}`. Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> lswt.O() array([0.+0.j]) """ result = np.zeros((self.M), dtype=complex) for _, alphas, parameter in _InteractionParametersIterator( self._parameters, n=1, p_n=1 ): result[alphas[0]] = ( np.sqrt(self.spins[alphas[0]] / 2) * np.conjugate(self.p[alphas[0]])
[docs] @ parameter ) # Convert units if necessary if units != "meV": units = _validated_units(units=units, supported_units=_ENERGY_UNITS) result = result * _ENERGY_UNITS["mev"] / _ENERGY_UNITS[units] return result
def A(self, k, relative=False, units="meV"): r""" Computes part of the grand dynamical matrix. Parameters ---------- k : (3,) |array-like|_ Reciprocal vector relative : bool, default False If ``relative=True``, then ``k`` is interpreted as given relative to the reciprocal unit cell. Otherwise it is interpreted as given in absolute coordinates. units : str, default "meV" .. versionadded:: 0.3.0 Units of energy. See :ref:`user-guide_usage_units_energy` for the full list of supported units. Returns ------- A : (M, M) :numpy:`ndarray` :math:`A_{\alpha\beta}(\boldsymbol{k})`. Notes ----- Before the diagonalization, the magnon Hamiltonian has the form .. math:: \mathcal{H} = \dots + \sum_{\boldsymbol{k}, \alpha} \boldsymbol{\mathcal{A}}(\boldsymbol{k})^{\dagger} \begin{pmatrix} \boldsymbol{A}(\boldsymbol{k}) & \boldsymbol{B}^{\dagger}(\boldsymbol{k}) \\ \boldsymbol{B}(\boldsymbol{k}) & \overline{\boldsymbol{A}(-\boldsymbol{k})} \end{pmatrix} \boldsymbol{\mathcal{A}}(\boldsymbol{k}) where .. math:: \boldsymbol{\mathcal{A}}(\boldsymbol{k}) = \begin{pmatrix} a_1(\boldsymbol{k}), \dots, a_M(\boldsymbol{k}), a^{\dagger}_1(-\boldsymbol{k}), \dots, a^{\dagger}_M(-\boldsymbol{k}), \end{pmatrix} This function computes the matrix :math:`\boldsymbol{A}(\boldsymbol{k})`. See Also -------- LSWT.B LSWT.GDM Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> lswt.A(k=[0, 0, 0.5], relative=True) array([[1.+0.j]]) """ k = np.array(k) result = np.zeros((self.M, self.M), dtype=complex) for nu in self.A2: if relative: phase = 2 * np.pi * (k @ nu) else: phase = k @ (nu @ self.cell) result = result + self.A2[nu] * np.exp(1j * phase) result = result - np.diag(self.A1) # Convert units if necessary if units != "meV": units = _validated_units(units=units, supported_units=_ENERGY_UNITS) result = result * _ENERGY_UNITS["mev"] / _ENERGY_UNITS[units] return result
[docs] def B(self, k, relative=False, units="meV"): r""" Computes part of the grand dynamical matrix. Parameters ---------- k : (3,) |array-like|_ Reciprocal vector. relative : bool, default False If ``relative=True``, then ``k`` is interpreted as given relative to the reciprocal unit cell. Otherwise it is interpreted as given in absolute coordinates. units : str, default "meV" .. versionadded:: 0.3.0 Units of energy. See :ref:`user-guide_usage_units_energy` for the full list of supported units. Returns ------- B : (M, M) :numpy:`ndarray` :math:`B_{\alpha\beta}(\boldsymbol{k})`. Notes ----- Before the diagonalization, the magnon Hamiltonian has the form .. math:: \mathcal{H} = \dots + \sum_{\boldsymbol{k}, \alpha} \boldsymbol{\mathcal{A}}(\boldsymbol{k})^{\dagger} \begin{pmatrix} \boldsymbol{A}(\boldsymbol{k}) & \boldsymbol{B}^{\dagger}(\boldsymbol{k}) \\ \boldsymbol{B}(\boldsymbol{k}) & \overline{\boldsymbol{A}(-\boldsymbol{k})} \end{pmatrix} \boldsymbol{\mathcal{A}}(\boldsymbol{k}) where .. math:: \boldsymbol{\mathcal{A}}(\boldsymbol{k}) = \begin{pmatrix} a_1(\boldsymbol{k}), \dots, a_M(\boldsymbol{k}), a^{\dagger}_1(-\boldsymbol{k}), \dots, a^{\dagger}_M(-\boldsymbol{k}), \end{pmatrix} This function computes the matrix :math:`\boldsymbol{B}(\boldsymbol{k})`. See Also -------- LSWT.A LSWT.GDM Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> lswt.B(k=[0, 0, 0.5], relative=True) array([[0.+0.j]]) """ k = np.array(k) result = np.zeros((self.M, self.M), dtype=complex) for nu in self.B2: if relative: phase = 2 * np.pi * (k @ nu) else: phase = k @ (nu @ self.cell) result = result + self.B2[nu] * np.exp(1j * phase) # Convert units if necessary if units != "meV": units = _validated_units(units=units, supported_units=_ENERGY_UNITS) result = result * _ENERGY_UNITS["mev"] / _ENERGY_UNITS[units] return result
[docs] def GDM(self, k, relative=False, units="meV"): r""" Computes grand dynamical matrix. Parameters ---------- k : (3,) |array-like|_ Reciprocal vector. relative : bool, default False If ``relative=True``, then ``k`` is interpreted as given relative to the reciprocal unit cell. Otherwise it is interpreted as given in absolute coordinates. units : str, default "meV" .. versionadded:: 0.3.0 Units of energy. See :ref:`user-guide_usage_units_energy` for the full list of supported units. Returns ------- gdm : (2M, 2M) :numpy:`ndarray` Gran dynamical matrix. Notes ----- Before the diagonalization, the magnon Hamiltonian has the form .. math:: \mathcal{H} = \dots + \sum_{\boldsymbol{k}, \alpha} \boldsymbol{\mathcal{A}}(\boldsymbol{k})^{\dagger} \begin{pmatrix} \boldsymbol{A}(\boldsymbol{k}) & \boldsymbol{B}^{\dagger}(\boldsymbol{k}) \\ \boldsymbol{B}(\boldsymbol{k}) & \overline{\boldsymbol{A}(-\boldsymbol{k})} \end{pmatrix} \boldsymbol{\mathcal{A}}(\boldsymbol{k}) where .. math:: \boldsymbol{\mathcal{A}}(\boldsymbol{k}) = \begin{pmatrix} a_1(\boldsymbol{k}), \dots, a_M(\boldsymbol{k}), a^{\dagger}_1(-\boldsymbol{k}), \dots, a^{\dagger}_M(-\boldsymbol{k}), \end{pmatrix} This function computes the grand dynamical matrix :math:`\boldsymbol{D}(\boldsymbol{k})` .. math:: \boldsymbol{D}(\boldsymbol{k}) = \begin{pmatrix} \boldsymbol{A}(\boldsymbol{k}) & \boldsymbol{B}^{\dagger}(\boldsymbol{k}) \\ \boldsymbol{B}(\boldsymbol{k}) & \overline{\boldsymbol{A}(-\boldsymbol{k})} \end{pmatrix} See Also -------- LSWT.A LSWT.B Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> lswt.GDM(k=[0,0,0.5], relative=True) # doctest: +SKIP array([[1.+0.j, 0.-0.j], [0.+0.j, 1.-0.j]]) """ k = np.array(k, dtype=float) A = self.A(k=k, relative=relative, units=units) A_m = self.A(k=-k, relative=relative, units=units) B = self.B(k=k, relative=relative, units=units) left = np.concatenate((A, np.conjugate(B).T), axis=0) right = np.concatenate((B, np.conjugate(A_m)), axis=0) gdm = np.concatenate((left, right), axis=1) return gdm
[docs] def diagonalize(self, k, relative=False, units="meV"): r""" Diagonalizes the Hamiltonian for the given ``k`` point. Parameters ---------- k : (3,) |array-like|_ Reciprocal vector. relative : bool, default False If ``relative=True``, then ``k`` is interpreted as given relative to the reciprocal unit cell. Otherwise it is interpreted as given in absolute coordinates. units : str, default "meV" .. versionadded:: 0.3.0 Units of energy. See :ref:`user-guide_usage_units_magnon-energy` for the full list of supported units. Returns ------- omegas : (M, ) :numpy:`ndarray` Array of omegas. Note, that data type is complex. If the ground state is correct, then the complex part should be zero. delta : float Constant energy term that results from diagonalization. Note, that data type is complex. If the ground state is correct, then the complex part should be zero. G : (M, 2M) :numpy:`ndarray` Transformation matrix from the original boson operators. .. math:: \begin{pmatrix} b_1(\boldsymbol{k}) \\ \dots \\ b_M(\boldsymbol{k}) \\ \end{pmatrix} = \mathcal{G} \begin{pmatrix} a_1(\boldsymbol{k}) \\ \dots \\ a_M(\boldsymbol{k}) \\ a^{\dagger}_1(-\boldsymbol{k}) \\ \dots \\ a^{\dagger}_M(-\boldsymbol{k}) \\ \end{pmatrix} See Also -------- LSWT.omega LSWT.delta LSWT.G Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> lswt.diagonalize(k=[0, 0, 0.5], relative=True) # doctest: +SKIP (array([2.+0.j]), 0j, array([[1.+0.j, 0.+0.j]])) """ GDM = self.GDM(np.array(k, dtype=float), relative=relative) # Diagonalize via Colpa's method try: E, G = solve_via_colpa(GDM) except ColpaFailed: # Try to diagonalize with suspected Goldstone mode try: E, G = solve_via_colpa( GDM + (1e-10) * np.eye(GDM.shape[0], dtype=float), ) # Return NaNs if it still fails except ColpaFailed: # Try to diagonalize for the negative GDMs # Note: solve_via_colpa will return positive eigenvalues, # so we need to negate them back try: E, G = solve_via_colpa(-GDM) E = -E except ColpaFailed: return ( [np.nan for _ in range(self.M)], np.nan, [[np.nan for _ in range(2 * self.M)] for _ in range(self.M)], ) # Convert units if necessary if units != "meV": units = _validated_units(units=units, supported_units=_MAGNON_ENERGY_UNITS) tmp_factor = _MAGNON_ENERGY_UNITS["mev"] / _MAGNON_ENERGY_UNITS[units] E = E * tmp_factor # Factor of two explained in the paper (TODO: add doi after publication) energies = E[: self.M] * 2 transformation_matrices = G[: self.M] return ( energies, # energies (M) complex(0.5 * (np.sum(E[self.M :]) - np.sum(E[: self.M]))), # delta term transformation_matrices, # transformation matrix (M x 2M) )
[docs] def omega(self, k, relative=False, units="meV"): r""" Computes magnon's eigenenergies at the given ``k`` point. Parameters ---------- k : (3,) |array-like|_ Reciprocal vector. relative : bool, default False If ``relative=True``, then ``k`` is interpreted as given relative to the reciprocal unit cell. Otherwise it is interpreted as given in absolute coordinates. units : str, default "meV" .. versionadded:: 0.3.0 Units of energy. See :ref:`user-guide_usage_units_magnon-energy` for the full list of supported units. Returns ------- omegas : (M, ) :numpy:`ndarray` Array of omegas. Note, that data type is complex. If the ground state is correct, then the complex part should be zero. See Also -------- LSWT.diagonalize LSWT.delta Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> lswt.omega(k=[0, 0, 0.5], relative=True) array([2.+0.j]) """ return self.diagonalize(k=k, relative=relative, units=units)[0]
[docs] def delta(self, k, relative=False, units="meV"): r""" Computes constant delta term of the diagonalized Hamiltonian. .. math:: \sum_{\boldsymbol{k}}\Delta(\boldsymbol{k}) Parameters ---------- k : (3,) |array-like|_ Reciprocal vector. relative : bool, default False If ``relative=True``, then ``k`` is interpreted as given relative to the reciprocal unit cell. Otherwise it is interpreted as given in absolute coordinates. units : str, default "meV" .. versionadded:: 0.3.0 Units of energy. See :ref:`user-guide_usage_units_magnon-energy` for the full list of supported units. Returns ------- delta : float Constant energy term that results from diagonalization. Note, that data type is complex. If the ground state is correct, then the complex part should be zero. See Also -------- LSWT.diagonalize LSWT.omega Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> lswt.delta(k=[0, 0, 0.5], relative=True) 0j """ return self.diagonalize(k=k, relative=relative, units=units)[1]
[docs] def G(self, k, relative=False): r""" Computes transformation matrix to the new bosonic operators. .. math:: b_{\alpha}(\boldsymbol{k}) = \sum_{\beta} (\mathcal{G})_{\alpha, \beta}(\boldsymbol{k}) \mathcal{A}_{\beta}(\boldsymbol{k}) Parameters ---------- k : (3,) |array-like|_ Reciprocal vector relative : bool, default False If ``relative=True``, then ``k`` is interpreted as given relative to the reciprocal unit cell. Otherwise it is interpreted as given in absolute coordinates. Returns ------- G : (M, 2M) :numpy:`ndarray` Transformation matrix from the original boson operators. See Also -------- LSWT.diagonalize LSWT.omega LSWT.delta Examples -------- .. doctest:: >>> import magnopy >>> spinham = magnopy.examples.cubic_ferro_nn() >>> lswt = magnopy.LSWT(spinham=spinham, spin_directions=[[0, 0, 1]]) >>> lswt.G(k=[0, 0, 0.5], relative=True) # doctest: +SKIP array([[1.+0.j, 0.+0.j]]) """ return self.diagonalize(k=k, relative=relative)[2]
# Populate __all__ with objects defined in this file __all__ = list(set(dir()) - old_dir) # Remove all semi-private objects __all__ = [i for i in __all__ if not i.startswith("_")] del old_dir