Source code for magnopy._spinham._hamiltonian

# ================================== LICENSE ===================================
# Magnopy - Python package for magnons.
# Copyright (C) 2023-2026 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 =================================


from copy import deepcopy
from math import ceil

import numpy as np
from wulfric import add_sugar

from magnopy._spinham._c1 import _add_1, _p1, _remove_1
from magnopy._spinham._c21 import _add_21, _p21, _remove_21
from magnopy._spinham._c22 import _add_22, _get_primary_p22, _p22, _remove_22
from magnopy._spinham._c31 import _add_31, _p31, _remove_31
from magnopy._spinham._c32 import _add_32, _p32, _remove_32
from magnopy._spinham._c33 import _add_33, _p33, _remove_33
from magnopy._spinham._c41 import _add_41, _p41, _remove_41
from magnopy._spinham._c43 import _add_43, _p43, _remove_43
from magnopy._spinham._c44 import _add_44, _p44, _remove_44
from magnopy._spinham._c421 import _add_421, _p421, _remove_421
from magnopy._spinham._c422 import _add_422, _p422, _remove_422
from magnopy._spinham._convention import Convention

from magnopy._data_validation import _validated_units

from magnopy._constants._units import _PARAMETER_UNITS, _PARAMETER_UNITS_MAKEUP
from magnopy._constants._si import BOHR_MAGNETON, ANGSTROM, VACUUM_MAGNETIC_PERMEABILITY

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


def _merge(list1: list, list2: list) -> list:
    r"""
    Merge two sorted parameter lists for any term.

    Lists of parameters have the form

    .. code-block:: python

        list = [[specs, parameter], ...]

    Comparison is based on specs.

    Parameter
    ---------
    list1 : list
        First list of parameters.
    list2 : list
        Second list of parameters.

    Returns
    -------
    merged_list : list
        Merged list of parameters.
    """

    list1 = deepcopy(list1)
    list2 = deepcopy(list2)

    merged_list = []

    i1 = 0
    i2 = 0

    while i1 < len(list1) or i2 < len(list2):
        if i1 >= len(list1):
            merged_list.append(list2[i2])
            i2 += 1
        elif i2 >= len(list2):
            merged_list.append(list1[i1])
            i1 += 1
        elif list1[i1][:-1] == list2[i2][:-1]:
            merged_list.append(list1[i1])
            merged_list[-1][-1] = merged_list[-1][-1] + list2[i2][-1]
            i1 += 1
            i2 += 1
        elif list1[i1][:-1] < list2[i2][:-1]:
            merged_list.append(list1[i1])
            i1 += 1
        else:
            merged_list.append(list2[i2])
            i2 += 1

    return merged_list


[docs] class SpinHamiltonian: r""" Spin Hamiltonian. Parameters ---------- convention : :py:class:`.Convention` or str A convention of the spin Hamiltonian. cell : (3, 3) |array-like|_ Matrix of a cell, rows are interpreted as vectors. atoms : dict Dictionary with atoms. units : str, default "meV" .. versionadded:: 0.3.0 Units of the Hamiltonian's parameters. See :py:attr:`.SpinHamiltonian.units` for more details. Case-insensitive. Examples -------- For example of usage see the page in the user guide - :ref:`user-guide_usage_spin-hamiltonian`. """ def __init__(self, cell, atoms, convention, units="meV") -> None: self._cell = np.array(cell) self._atoms = add_sugar(atoms) # Only the magnetic sites self._magnetic_atoms = None self._map_to_magnetic = None self._map_to_all = None self._convention = convention if units.lower() not in _PARAMETER_UNITS: raise ValueError( f'Given units ("{units}") are not supported. Please use one of\n * ' + "\n * ".join(list(_PARAMETER_UNITS)) ) self._units = units.lower() # [[alpha, parameter], ...] self._1 = [] # [[alpha, parameter], ...] self._21 = [] # [[alpha, beta, nu, parameter], ...] self._22 = [] # [[alpha, parameter], ...] self._31 = [] # [[alpha, beta, nu, parameter], ...] self._32 = [] # [[alpha, beta, gamma, nu, lambda, parameter], ...] self._33 = [] # [[alpha, parameter], ...] self._41 = [] # [[alpha, beta, nu, parameter], ...] self._421 = [] # [[alpha, beta, nu, parameter], ...] self._422 = [] # [[alpha, beta, gamma, nu, lambda, parameter], ...] self._43 = [] # [[alpha, beta, gamma, epsilon, nu, lambda, rho, parameter], ...] self._44 = [] ############################################################################ # Cell and Atoms # ############################################################################ @property def cell(self): r""" Cell of the crystal on which the Hamiltonian is build. Returns ------- cell : (3, 3) :numpy:`ndarray` Matrix of the cell, rows are lattice vectors. Notes ----- If is not recommended to change the ``cell`` property after the creation of :py:class:`.SpinHamiltonian`. In fact an attempt to do so will raise an ``AttributeError``: .. doctest:: >>> import numpy as np >>> import magnopy >>> convention = magnopy.Convention() >>> spinham = magnopy.SpinHamiltonian( ... cell=np.eye(3), atoms={}, convention=convention ... ) >>> spinham.cell = 2 * np.eye(3) Traceback (most recent call last): ... AttributeError: Change of the cell attribute is not supported after the creation of SpinHamiltonian instance. If you need to modify cell, then use pre-defined methods of SpinHamiltonian or create a new one. Use pre-defined methods of the :py:class:`.SpinHamiltonian` class to safely modify the cell. If you need to change the cell attribute, then use .. doctest:: >>> import numpy as np >>> import magnopy >>> convention = magnopy.Convention() >>> spinham = magnopy.SpinHamiltonian( ... cell=np.eye(3), atoms={}, convention=convention ... ) >>> spinham.cell array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) >>> spinham._cell = 2 * np.eye(3) >>> spinham.cell array([[2., 0., 0.], [0., 2., 0.], [0., 0., 2.]]) In the latter case correct behavior of magnopy **is not** guaranteed. Use only if you have a deep understanding of the magnopy source code. """ return self._cell @cell.setter def cell(self, new_value): raise AttributeError( "Change of the cell attribute is not allowed after the creation of SpinHamiltonian instance. SpinHamiltonian.cell is immutable." ) @property def atoms(self) -> dict: r""" Atoms of the crystal on which the Hamiltonian is build. Returns ------- atoms : dict (with added sugar) Dictionary with the atoms. Notes ----- If is not recommended to change the atoms property after the creation of :py:class:`.SpinHamiltonian`. In fact an attempt to do so will raise an ``AttributeError``: .. doctest:: >>> import numpy as np >>> import magnopy >>> convention = magnopy.Convention() >>> spinham = magnopy.SpinHamiltonian( ... cell=np.eye(3), atoms={}, convention=convention ... ) >>> spinham.atoms = {"names": ["Cr"]} Traceback (most recent call last): ... AttributeError: Change of the atoms dictionary is not supported after the creation of SpinHamiltonian instance. If you need to modify atoms, then use pre-defined methods of SpinHamiltonian or create a new one. Use pre-defined methods of the :py:class:`.SpinHamiltonian` class to safely modify atoms. If you need to change the whole dictionary at once, then use .. doctest:: >>> import numpy as np >>> import magnopy >>> convention = magnopy.Convention() >>> spinham = magnopy.SpinHamiltonian( ... cell=np.eye(3), atoms={}, convention=convention ... ) >>> spinham.atoms {} >>> spinham._atoms = {"names": ["Cr"]} >>> spinham.atoms {'names': ['Cr']} In the latter case correct behavior of magnopy **is not** guaranteed. Use only if you have a deep understanding of the magnopy source code. """ return self._atoms @atoms.setter def atoms(self, new_value): raise AttributeError( "Change of the atoms dictionary is not supported after the creation of SpinHamiltonian instance. SpinHamiltonian.atoms is immutable." ) def _reset_internals(self): self._map_to_magnetic = None self._map_to_all = None self._magnetic_atoms = None def _update_internals(self): # Identify magnetic sites indices = set() for alpha, _ in self._1: indices.add(alpha) for alpha, _ in self._21: indices.add(alpha) for alpha, beta, _, _ in self._22: indices.add(alpha) indices.add(beta) for alpha, _ in self._31: indices.add(alpha) for alpha, beta, _, _ in self._32: indices.add(alpha) indices.add(beta) for alpha, beta, gamma, _, _, _ in self._33: indices.add(alpha) indices.add(beta) indices.add(gamma) for alpha, _ in self._41: indices.add(alpha) for alpha, beta, _, _ in self._421: indices.add(alpha) indices.add(beta) for alpha, beta, _, _ in self._422: indices.add(alpha) indices.add(beta) for alpha, beta, gamma, _, _, _ in self._43: indices.add(alpha) indices.add(beta) indices.add(gamma) for alpha, beta, gamma, epsilon, _, _, _, _ in self._44: indices.add(alpha) indices.add(beta) indices.add(gamma) indices.add(epsilon) indices = sorted(list(indices)) # Create index map from all to magnetic self._map_to_magnetic = [None for _ in range(len(self.atoms.names))] for i in range(len(indices)): self._map_to_magnetic[indices[i]] = i # Create index map from magnetic to all self._map_to_all = indices # Create magnetic atoms dictionary self._magnetic_atoms = add_sugar({}) for key in self.atoms: self._magnetic_atoms[key] = [] for full_index in indices: self._magnetic_atoms[key].append(self.atoms[key][full_index]) @property def map_to_magnetic(self): r""" Index map from all atoms to the magnetic ones. Returns ------- map_to_magnetic (L, ) list of int Index map. Integers. ``L = len(spinham.atoms.names)`` """ if self._map_to_magnetic is None: self._update_internals() return self._map_to_magnetic @property def map_to_all(self): r""" Index map from magnetic atoms to all atoms. Returns ------- map_to_all (M, ) list of int Index map. Integers. ``M = len(spinham.magnetic_atoms.names)`` """ if self._map_to_all is None: self._update_internals() return self._map_to_all @property def magnetic_atoms(self): r""" Magnetic atoms of the spin Hamiltonian. Magnetic atom is defined as an atom with at least one parameter associated with it. This property is dynamically computed at every call. Returns ------- magnetic_atoms : list of int Indices of magnetic atoms in the ``spinham.atoms``. Sorted. See Also -------- M """ if self._magnetic_atoms is None: self._update_internals() return self._magnetic_atoms @property def M(self): r""" Number of spins (magnetic atoms) in the unit cell. Returns ------- M : int Number of spins (magnetic atoms) in the unit cell. See Also -------- magnetic_atoms """ return len(self.magnetic_atoms.names) ############################################################################ # Convention # ############################################################################ @property def convention(self) -> Convention: r""" Convention of the spin Hamiltonian. Returns ------- convention : :py:class:`.Convention` See Also -------- Convention """ return self._convention @convention.setter def convention(self, new_convention: Convention): self._set_multiple_counting(new_convention._multiple_counting) self._set_spin_normalization(new_convention._spin_normalized) self._set_c1(new_convention._c1) self._set_c21(new_convention._c21) self._set_c22(new_convention._c22) self._set_c31(new_convention._c31) self._set_c32(new_convention._c32) self._set_c33(new_convention._c33) self._set_c41(new_convention._c41) self._set_c421(new_convention._c421) self._set_c422(new_convention._c422) self._set_c43(new_convention._c43) self._set_c44(new_convention._c44) self._convention = new_convention def _set_multiple_counting(self, multiple_counting: bool) -> None: if multiple_counting is None or self.convention._multiple_counting is None: return multiple_counting = bool(multiple_counting) if self.convention.multiple_counting == multiple_counting: return # It was absent before if multiple_counting: factor = 0.5 # It was present before else: factor = 2.0 # For (two spins & two sites) for index in range(len(self._22)): self._22[index][3] = self._22[index][3] * factor # For (three spins & two sites) for index in range(len(self._32)): self._32[index][3] = self._32[index][3] * factor # For (four spins & two sites (3+1)) for index in range(len(self._421)): self._421[index][3] = self._421[index][3] * factor # For (four spins & two sites (2+2)) for index in range(len(self._422)): self._422[index][3] = self._422[index][3] * factor # It was absent before if multiple_counting: factor = 1 / 6 # It was present before else: factor = 6 # For (three spins & three sites) for index in range(len(self._33)): self._33[index][5] = self._33[index][5] * factor # For (four spins & three sites) for index in range(len(self._43)): self._43[index][5] = self._43[index][5] * factor # It was absent before if multiple_counting: factor = 1 / 24 # It was present before else: factor = 24 # For (four spins & four sites) for index in range(len(self._44)): self._44[index][7] = self._44[index][7] * factor def _set_spin_normalization(self, spin_normalized: bool) -> None: if spin_normalized is None or self.convention._spin_normalized is None: return spin_normalized = bool(spin_normalized) if self.convention.spin_normalized == spin_normalized: return # Before it was not normalized if spin_normalized: # For (one spin & one site) for index in range(len(self._1)): alpha = self._1[index][0] self._1[index][1] = self._1[index][1] * self.atoms.spins[alpha] # For (two spins & one site) for index in range(len(self._21)): alpha = self._21[index][0] self._21[index][1] = self._21[index][1] * self.atoms.spins[alpha] ** 2 # For (two spins & two sites) for index in range(len(self._22)): alpha = self._22[index][0] beta = self._22[index][1] self._22[index][3] = self._22[index][3] * ( self.atoms.spins[alpha] * self.atoms.spins[beta] ) # For (three spins & one site) for index in range(len(self._31)): alpha = self._31[index][0] self._31[index][1] = self._31[index][1] * self.atoms.spins[alpha] ** 3 # For (three spins & two sites) for index in range(len(self._32)): alpha = self._32[index][0] beta = self._32[index][1] self._32[index][3] = self._32[index][3] * ( self.atoms.spins[alpha] ** 2 * self.atoms.spins[beta] ) # For (three spins & three sites) for index in range(len(self._33)): alpha = self._33[index][0] beta = self._33[index][1] gamma = self._33[index][2] self._33[index][5] = self._33[index][5] * ( self.atoms.spins[alpha] * self.atoms.spins[beta] * self.atoms.spins[gamma] ) # For (four spins & one site) for index in range(len(self._41)): alpha = self._41[index][0] self._41[index][1] = self._41[index][1] * self.atoms.spins[alpha] ** 4 # For (four spins & two sites (3+1)) for index in range(len(self._421)): alpha = self._421[index][0] beta = self._421[index][1] self._421[index][3] = self._421[index][3] * ( self.atoms.spins[alpha] ** 3 * self.atoms.spins[beta] ) # For (four spins & two sites (2+2)) for index in range(len(self._422)): alpha = self._422[index][0] beta = self._422[index][1] self._422[index][3] = self._422[index][3] * ( self.atoms.spins[alpha] ** 2 * self.atoms.spins[beta] ** 2 ) # For (four spins & three sites) for index in range(len(self._43)): alpha = self._43[index][0] beta = self._43[index][1] gamma = self._43[index][2] self._43[index][5] = self._43[index][5] * ( self.atoms.spins[alpha] ** 2 * self.atoms.spins[beta] * self.atoms.spins[gamma] ) # For (four spins & four sites) for index in range(len(self._44)): alpha = self._44[index][0] beta = self._44[index][1] gamma = self._44[index][2] epsilon = self._44[index][3] self._44[index][7] = self._44[index][7] * ( self.atoms.spins[alpha] * self.atoms.spins[beta] * self.atoms.spins[gamma] * self.atoms.spins[epsilon] ) # Before it was normalized else: # For (one spin & one site) for index in range(len(self._1)): alpha = self._1[index][0] self._1[index][1] = self._1[index][1] / self.atoms.spins[alpha] # For (two spins & one site) for index in range(len(self._21)): alpha = self._21[index][0] self._21[index][1] = self._21[index][1] / self.atoms.spins[alpha] ** 2 # For (two spins & two sites) for index in range(len(self._22)): alpha = self._22[index][0] beta = self._22[index][1] self._22[index][3] = self._22[index][3] / ( self.atoms.spins[alpha] * self.atoms.spins[beta] ) # For (three spins & one site) for index in range(len(self._31)): alpha = self._31[index][0] self._31[index][1] = self._31[index][1] / self.atoms.spins[alpha] ** 3 # For (three spins & two sites) for index in range(len(self._32)): alpha = self._32[index][0] beta = self._32[index][1] self._32[index][3] = self._32[index][3] / ( self.atoms.spins[alpha] ** 2 * self.atoms.spins[beta] ) # For (three spins & three sites) for index in range(len(self._33)): alpha = self._33[index][0] beta = self._33[index][1] gamma = self._33[index][2] self._33[index][5] = self._33[index][5] / ( self.atoms.spins[alpha] * self.atoms.spins[beta] * self.atoms.spins[gamma] ) # For (four spins & one site) for index in range(len(self._41)): alpha = self._41[index][0] self._41[index][1] = self._41[index][1] / self.atoms.spins[alpha] ** 4 # For (four spins & two sites (3+1)) for index in range(len(self._421)): alpha = self._421[index][0] beta = self._421[index][1] self._421[index][3] = self._421[index][3] / ( self.atoms.spins[alpha] ** 3 * self.atoms.spins[beta] ) # For (four spins & two sites (2+2)) for index in range(len(self._422)): alpha = self._422[index][0] beta = self._422[index][1] self._422[index][3] = self._422[index][3] / ( self.atoms.spins[alpha] ** 2 * self.atoms.spins[beta] ** 2 ) # For (four spins & three sites) for index in range(len(self._43)): alpha = self._43[index][0] beta = self._43[index][1] gamma = self._43[index][2] self._43[index][5] = self._43[index][5] / ( self.atoms.spins[alpha] ** 2 * self.atoms.spins[beta] * self.atoms.spins[gamma] ) # For (four spins & four sites) for index in range(len(self._44)): alpha = self._44[index][0] beta = self._44[index][1] gamma = self._44[index][2] epsilon = self._44[index][3] self._44[index][7] = self._44[index][7] / ( self.atoms.spins[alpha] * self.atoms.spins[beta] * self.atoms.spins[gamma] * self.atoms.spins[epsilon] ) def _set_c1(self, new_c1: float) -> None: if new_c1 is None or self.convention._c1 is None: return new_c1 = float(new_c1) if self.convention.c1 == new_c1: return # If factor is changing one has to scale parameters. for index in range(len(self._1)): self._1[index][1] = self._1[index][1] * self.convention.c1 / new_c1 def _set_c21(self, new_c21: float) -> None: if new_c21 is None or self.convention._c21 is None: return new_c21 = float(new_c21) if self.convention.c21 == new_c21: return # If factor is changing one has to scale parameters. for index in range(len(self._21)): self._21[index][1] = self._21[index][1] * self.convention.c21 / new_c21 def _set_c22(self, new_c22: float) -> None: if new_c22 is None or self.convention._c22 is None: return new_c22 = float(new_c22) if self.convention.c22 == new_c22: return # If factor is changing one has to scale parameters. for index in range(len(self._22)): self._22[index][3] = self._22[index][3] * self.convention.c22 / new_c22 def _set_c31(self, new_c31: float) -> None: if new_c31 is None or self.convention._c31 is None: return new_c31 = float(new_c31) if self.convention.c31 == new_c31: return # If factor is changing one has to scale parameters. for index in range(len(self._31)): self._31[index][1] = self._31[index][1] * self.convention.c31 / new_c31 def _set_c32(self, new_c32: float) -> None: if new_c32 is None or self.convention._c32 is None: return new_c32 = float(new_c32) if self.convention.c32 == new_c32: return # If factor is changing one has to scale parameters. for index in range(len(self._32)): self._32[index][3] = self._32[index][3] * self.convention.c32 / new_c32 def _set_c33(self, new_c33: float) -> None: if new_c33 is None or self.convention._c33 is None: return new_c33 = float(new_c33) if self.convention.c33 == new_c33: return # If factor is changing one has to scale parameters. for index in range(len(self._33)): self._33[index][5] = self._33[index][5] * self.convention.c33 / new_c33 def _set_c41(self, new_c41: float) -> None: if new_c41 is None or self.convention._c41 is None: return new_c41 = float(new_c41) if self.convention.c41 == new_c41: return # If factor is changing one has to scale parameters. for index in range(len(self._41)): self._41[index][1] = self._41[index][1] * self.convention.c41 / new_c41 def _set_c421(self, new_c421: float) -> None: if new_c421 is None or self.convention._c421 is None: return new_c421 = float(new_c421) if self.convention.c421 == new_c421: return # If factor is changing one has to scale parameters. for index in range(len(self._421)): self._421[index][3] = self._421[index][3] * self.convention.c421 / new_c421 def _set_c422(self, new_c422: float) -> None: if new_c422 is None or self.convention._c422 is None: return new_c422 = float(new_c422) if self.convention.c422 == new_c422: return # If factor is changing one has to scale parameters. for index in range(len(self._422)): self._422[index][3] = self._422[index][3] * self.convention.c422 / new_c422 def _set_c43(self, new_c43: float) -> None: if new_c43 is None or self.convention._c43 is None: return new_c43 = float(new_c43) if self.convention.c43 == new_c43: return # If factor is changing one has to scale parameters. for index in range(len(self._43)): self._43[index][5] = self._43[index][5] * self.convention.c43 / new_c43 def _set_c44(self, new_c44: float) -> None: if new_c44 is None or self.convention._c44 is None: return new_c44 = float(new_c44) if self.convention.c44 == new_c44: return # If factor is changing one has to scale parameters. for index in range(len(self._44)): self._44[index][7] = self._44[index][7] * self.convention.c44 / new_c44 ############################################################################ # Units # ############################################################################ @property def units(self) -> str: r""" Units of the Hamiltonian's parameters. .. versionadded:: 0.3.0 The parameters of the Hamiltonian are stored in some units of energy (or energy-like). When user adds a parameters to the Hamiltonian (i. e. :py:meth:`.SpinHamiltonian.add_21`, ...) the ``parameter`` argument is understood to be given in the units of :py:attr:`.SpinHamiltonian.units`. By default the Hamiltonian stores and expects parameters in "meV", but the user can choose out of the list of the supported units. See :ref:`user-guide_usage_units_parameter-units` for the full list of supported units. When Hamiltonian already has some parameters in it, then the change of :py:attr:`.SpinHamiltonian.units` will convert all parameter to the new units. The parameters that the user tries to add afterwards are expected to be in the new units already. Returns ------- units : str See Also -------- :ref:`user-guide_usage_units` """ return _PARAMETER_UNITS_MAKEUP[self._units] @units.setter def units(self, new_units: str): new_units = _validated_units(units=new_units, supported_units=_PARAMETER_UNITS) conversion_factor = _PARAMETER_UNITS[self._units] / _PARAMETER_UNITS[new_units] # One-site parameters for index in range(len(self._1)): self._1[index][1] = self._1[index][1] * conversion_factor for index in range(len(self._21)): self._21[index][1] = self._21[index][1] * conversion_factor for index in range(len(self._31)): self._31[index][1] = self._31[index][1] * conversion_factor for index in range(len(self._41)): self._41[index][1] = self._41[index][1] * conversion_factor # Two-sites parameters for index in range(len(self._22)): self._22[index][3] = self._22[index][3] * conversion_factor for index in range(len(self._32)): self._32[index][3] = self._32[index][3] * conversion_factor for index in range(len(self._421)): self._421[index][3] = self._421[index][3] * conversion_factor for index in range(len(self._422)): self._422[index][3] = self._422[index][3] * conversion_factor # Three-sites parameters for index in range(len(self._33)): self._33[index][5] = self._33[index][5] * conversion_factor for index in range(len(self._43)): self._43[index][5] = self._43[index][5] * conversion_factor # Four-sites parameters for index in range(len(self._44)): self._44[index][7] = self._44[index][7] * conversion_factor self._units = new_units.lower() ############################################################################ # External magnetic field # ############################################################################ # ARGUMENT "h" DEPRECATED since 0.4.0 # Remove in May of 2026
[docs] def add_magnetic_field(self, B=None, alphas=None, h=None) -> None: r""" Adds external magnetic field to the Hamiltonian in the form of one spin parameters. .. math:: \mu_B g_{\alpha} \boldsymbol{B}\cdot\boldsymbol{S}_{\mu,\alpha} = C_1 \boldsymbol{S}_{\mu,\alpha} \cdot \boldsymbol{J}_{Zeeman}(\boldsymbol{r}_{\alpha}) where :math:`\boldsymbol{J}_{Zeeman}(\boldsymbol{r}_{\alpha})` is defined as .. math:: \boldsymbol{J}_{Zeeman}(\boldsymbol{r}_{\alpha}) = \dfrac{\mu_B g_{\alpha}}{C_1}\boldsymbol{B} Parameters ---------- B : (3, ) |array-like|_ Vector of magnetic field (magnetic flux density, B) given in the units of Tesla. alphas : list of int, optional Indices of atoms, to which the magnetic field effect should be added. h : (3, ) |array-like|_ Vector of magnetic field given in the units of Tesla. .. deprecated:: 0.4.0 The argument will be removed in May of 2026. Use ``B`` instead. Notes ----- To minimize the energy the magnetic moment will be aligned with the direction of the external field. But spin vector will be directed opposite to the direction of the magnetic field. * If ``alphas is None``, then parameters of the magnetic field added only to the magnetic atoms. In other words only to atoms that already have at least one other parameter (any) associated with it. * If ``alpha is not None``, then parameters of magnetic field are added to the atoms with the provided indices (based on the order in :py:attr:`.SpinHamiltonian.atoms`) """ if h is not None: import warnings warnings.warn( 'Argument "h" is deprecated as of 0.4.0, use "B" instead. "h" will be removed in May of 2026.', DeprecationWarning, stacklevel=2, ) B = h if B is None: raise TypeError( "SpinHamiltonian.add_magnetic_field() missing 1 required argument: 'B'" ) if self.convention._c1 is None: self.convention._c1 = 1.0 B = np.array(B, dtype=float) mu_B = BOHR_MAGNETON / _PARAMETER_UNITS[self._units] # spinham.units / Tesla if alphas is None: alphas = self.map_to_all zeeman_parameters = [ mu_B * self.atoms.g_factors[alpha] * B / self.convention.c1 for alpha in alphas ] i = 0 j = 0 new_p1 = [] while i < len(alphas) or j < len(self._1): if i >= len(alphas) or (j < len(self._1) and alphas[i] > self._1[j][0]): new_p1.append(self._1[j]) j += 1 elif j >= len(self._1) or (i < len(alphas) and alphas[i] < self._1[j][0]): new_p1.append([alphas[i], zeeman_parameters[i]]) i += 1 elif alphas[i] == self._1[j][0]: new_p1.append( [ alphas[i], zeeman_parameters[i] + self._1[j][1], ] ) i += 1 j += 1 self._1 = new_p1 self._reset_internals()
############################################################################ # Magnetic dipole-dipole interaction # ############################################################################
[docs] def add_dipole_dipole(self, R_cut=None, E_cut=None, alphas=None): r""" Adds magnetic dipole dipole interaction to the Hamiltonian. Magnetic dipole dipole interaction is added in the form of two-spin & two-sites parameter .. math:: C_{2,2} \sum_{\mu,\nu,\alpha,\beta,i,j} J_{dd}(\boldsymbol{r}_{\nu,\alpha\beta})^{ij} S_{\mu,\alpha}^i S_{\mu+\nu,\beta}^j where the parameter is defined as .. math:: J_{dd}(\boldsymbol{r}_{\nu,\alpha\beta})^{ij} = \dfrac{\mu_0\mu_B^2}{8\pi C_{2,2}} \dfrac{g_{\alpha}g_{\beta}}{\vert\boldsymbol{r}_{\nu,\alpha\beta}\vert^3} (\delta_{k,l} - 3\hat{r}_{\nu,\alpha\beta}^i\hat{r}_{\nu,\alpha\beta}^j) if :py:attr:`.SpinHamiltonian.convention.multiple_counting` is ``True`` and as .. math:: J_{dd}(\boldsymbol{r}_{\nu,\alpha\beta})^{ij} = \dfrac{\mu_0\mu_B^2}{4\pi C_{2,2}} \dfrac{g_{\alpha}g_{\beta}}{\vert\boldsymbol{r}_{\nu,\alpha\beta}\vert^3} (\delta_{k,l} - 3\hat{r}_{\nu,\alpha\beta}^i\hat{r}_{\nu,\alpha\beta}^j) if :py:attr:`.SpinHamiltonian.convention.multiple_counting` is ``False``. where :math:`g_{\alpha}` is a g-factor, :math:`\boldsymbol{\hat{r}}_{\nu,\alpha\beta}` is a unit vector. Parameters ---------- R_cut : float, optional Cut off radius for the distance between a pair of atoms. :math:`R_{cut} \ge 0`. E_cut : float, optional Cut off value for the maximum value of the parameter. :math:`E_{cut} > 0`. alphas : list of int, optional Indices of atoms, to which the magnetic field effect should be added. Raises ------ ValueError * If none of the ``R_cut`` or ``E_cut`` are provided. * If ``R_cut < 0`` * If ``E_cut <= 0`` Notes ----- * If only ``R_cut`` is given, then the dipole dipole term between the pair of spins :math:`S_{\mu,\alpha}^i` and :math:`S_{\mu+\nu,\beta}^j` is added if :math:`\vert\boldsymbol{r}_{\nu,\alpha\beta}\vert <= R_{cut}`. * If only ``E_cut`` is given, then the ``R_cut`` is estimated as .. math:: R_{cut} = \left( 3\sqrt{2} \dfrac{\mu_0\mu_B^2g_{max}^2}{8\pi C_{2,2}E_{cut}} \right)^{\dfrac{1}{3}} if :py:attr:`.SpinHamiltonian.convention.multiple_counting` is ``True`` and as .. math:: R_{cut} = \left( 3\sqrt{2} \dfrac{\mu_0\mu_B^2g_{max}^2}{4\pi C_{2,2}E_{cut}} \right)^{\dfrac{1}{3}} if :py:attr:`.SpinHamiltonian.convention.multiple_counting` is ``False``. The dipole dipole term between the pair of spins :math:`S_{\mu,\alpha}^i` and :math:`S_{\mu+\nu,\beta}^j` is added if :math:`\vert\boldsymbol{r}_{\nu,\alpha\beta}\vert \le R_{cut}` and :math:`\vert J_{dd}(\boldsymbol{r}_{\nu,\alpha\beta})^{ij}\vert\ge E_{cut}` for some :math:`i, j`. * If both ``R_cut`` and ``E_cut`` are provided, then the dipole dipole term between the pair of spins :math:`S_{\mu,\alpha}^i` and :math:`S_{\mu+\nu,\beta}^j` is added if :math:`\vert\boldsymbol{r}_{\nu,\alpha\beta}\vert \le R_{cut}` and :math:`\vert J_{dd}(\boldsymbol{r}_{\nu,\alpha\beta})^{ij}\vert \ge E_{cut}` for some :math:`i, j`. Magnetic dipole-dipole interaction is added either to magnetic atoms or to the list of the atoms provided by user. * If ``alphas is None``, then parameters of the magnetic field added only to the magnetic atoms. In other words only to atoms that already have at least one other parameter (any) associated with it. * If ``alpha is not None``, then parameters of magnetic field are added to the atoms with the provided indices (based on the order in :py:attr:`.SpinHamiltonian.atoms`) """ # Constants MU_0_MU_B = ( VACUUM_MAGNETIC_PERMEABILITY * BOHR_MAGNETON**2 / ANGSTROM**3 / _PARAMETER_UNITS[self._units] ) # spinham.units * Angstrom^3 if E_cut is None and R_cut is None: raise ValueError("Expected either E_cut or R_cut, got neither.") if E_cut is not None: if E_cut <= 0: raise ValueError(f"Expected positive cut-off energy, got {E_cut}.") R_cut = ( 3 * np.sqrt(2) * MU_0_MU_B * max(self.atoms.g_factors) ** 2 / 4 / np.pi / self.convention.c22 / E_cut ) if self.convention.multiple_counting: R_cut = R_cut / 2 R_cut = R_cut ** (1 / 3) else: R_cut = float(R_cut) if R_cut < 0: raise ValueError(f"Expected positive cut-off radius, got {R_cut}.") # Get indices for unit cells of interest a1, a2, a3 = self.cell a_3_perp = abs(np.cross(a1, a2) @ a3 / np.linalg.norm(np.cross(a1, a2))) m_3_max = ceil(R_cut / a_3_perp) a_2_perp = np.cross(a2, a3) @ a1 / np.linalg.norm(np.cross(a2, a3)) m_2_max = ceil(R_cut / a_2_perp) m_1_max = ceil(R_cut / np.linalg.norm(a1)) # Run over all pairs of atoms between (0, 0, 0) and all unit cells of # interest tmp_parameters = [] if alphas is None: alphas = self.map_to_all for alpha in alphas: for k in range(-m_3_max, m_3_max + 1): for j in range(-m_2_max, m_2_max + 1): for i in range(-m_1_max, m_1_max + 1): for beta in alphas: if k == 0 and j == 0 and i == 0 and alpha == beta: continue if _get_primary_p22( alpha=alpha, beta=beta, nu=(i, j, k) ) != (alpha, beta, (i, j, k)): continue vector = ( np.array([i, j, k]) + self.atoms.positions[beta] - self.atoms.positions[alpha] ) @ self.cell distance = np.linalg.norm(vector) if distance <= R_cut: parameter = ( MU_0_MU_B / 4 / np.pi / self.convention.c22 * self.atoms.g_factors[alpha] * self.atoms.g_factors[beta] / distance**3 ) * ( np.eye(3, dtype=float) - 3 * np.outer(vector, vector) / distance**2 ) if self.convention.multiple_counting: parameter = parameter / 2 if E_cut is None or (np.abs(parameter) >= E_cut).any(): tmp_parameters.append( [alpha, beta, (i, j, k), parameter] ) if len(tmp_parameters) > 0: tmp_parameters.sort(key=lambda x: x[:-1]) self._22 = _merge(list1=self._22, list2=tmp_parameters)
############################################################################ # Copy getter # ############################################################################
[docs] def copy(self): R""" Returns a new, independent copy of the same Hamiltonian. Returns ------- spinham : :py:class:`.SpinHamiltonian` A new instance of the same Hamiltonian. """ return deepcopy(self)
[docs] def get_empty(self): r""" Returns the Hamiltonian with the same cell, atoms, units and convention, but with no parameters present. Returns ------- spinham : py:class:`.SpinHamiltonian` New instance of the spin Hamiltonian. Notes ----- Note that in the new Hamiltonian ``spinham.M == 0`` - as there is no parameters present, then no atoms are considered to be magnetic. """ return SpinHamiltonian( cell=self.cell, atoms=self.atoms, convention=self.convention, units=self.units, )
############################################################################ # Arithmetic operations # ############################################################################ def __mul__(self, number): if not isinstance(number, int) and not isinstance(number, float): raise TypeError( f"unsupported operand type(s) for *: '{type(number)}' and 'SpinHamiltonian'" ) spinham = self.copy() # One spin for i in range(len(spinham._1)): spinham._1[i][1] *= number # Two spins for i in range(len(spinham._21)): spinham._21[i][1] *= number for i in range(len(spinham._22)): spinham._22[i][3] *= number # Three spins for i in range(len(spinham._31)): spinham._31[i][1] *= number for i in range(len(spinham._32)): spinham._32[i][3] *= number for i in range(len(spinham._33)): spinham._33[i][5] *= number # Four spins for i in range(len(spinham._41)): spinham._41[i][1] *= number for i in range(len(spinham._421)): spinham._421[i][3] *= number for i in range(len(spinham._422)): spinham._422[i][3] *= number for i in range(len(spinham._43)): spinham._43[i][5] *= number for i in range(len(spinham._44)): spinham._44[i][7] *= number return spinham def __rmul__(self, number): return self.__mul__(number=number) def __add__(self, other): if not isinstance(other, SpinHamiltonian): raise NotImplementedError # Check that unit cells are the same if not np.allclose(self.cell, other.cell): raise ValueError( "Unit cells of two Hamiltonians are different, " "summation is not supported" ) # Check that atoms are the same same_atoms = True if len(self.atoms.names) != len(other.atoms.names): same_atoms = False else: for i in range(len(self.atoms.names)): if ( self.atoms.names[i] != other.atoms.names[i] or not np.allclose( self.atoms.positions[i], other.atoms.positions[i] ) or abs(self.atoms.spins[i] - other.atoms.spins[i]) > 1e-8 or abs(self.atoms.g_factors[i] - other.atoms.g_factors[i]) > 1e-8 ): same_atoms = False if not same_atoms: raise ValueError( "Atoms of two spin Hamiltonians are different, " "summation is not supported." ) # Make sure that units are the same other_units = other.units other.units = self.units # Make sure that conventions are the same other_convention = other.convention other.convention = self.convention result = self.get_empty() # One spin terms result._1 = _merge(list1=self._1, list2=other._1) # Two spin terms result._21 = _merge(list1=self._21, list2=other._21) result._22 = _merge(list1=self._22, list2=other._22) # Three spin terms result._31 = _merge(list1=self._31, list2=other._31) result._32 = _merge(list1=self._32, list2=other._32) result._33 = _merge(list1=self._33, list2=other._33) # Four spin terms result._41 = _merge(list1=self._41, list2=other._41) result._421 = _merge(list1=self._421, list2=other._421) result._422 = _merge(list1=self._422, list2=other._422) result._43 = _merge(list1=self._43, list2=other._43) result._44 = _merge(list1=self._44, list2=other._44) # Restore units of other Hamiltonian other.units = other_units # Restore convention of other Hamiltonian other.convention = other_convention return result def __sub__(self, other): return self + (-1) * other ############################################################################ # One spin & one site # ############################################################################ p1 = _p1 add_1 = _add_1 remove_1 = _remove_1 ############################################################################ # Two spins & one site # ############################################################################ p21 = _p21 add_21 = _add_21 remove_21 = _remove_21 ############################################################################ # Two spins & two sites # ############################################################################ p22 = _p22 add_22 = _add_22 remove_22 = _remove_22 ############################################################################ # Three spins & one site # ############################################################################ p31 = _p31 add_31 = _add_31 remove_31 = _remove_31 ############################################################################ # Three spins & two sites # ############################################################################ p32 = _p32 add_32 = _add_32 remove_32 = _remove_32 ############################################################################ # Three spins & three sites # ############################################################################ p33 = _p33 add_33 = _add_33 remove_33 = _remove_33 ############################################################################ # Four spins & one site # ############################################################################ p41 = _p41 add_41 = _add_41 remove_41 = _remove_41 ############################################################################ # Four spins & two sites (3+1) # ############################################################################ p421 = _p421 add_421 = _add_421 remove_421 = _remove_421 ############################################################################ # Four spins & two sites (2+2) # ############################################################################ p422 = _p422 add_422 = _add_422 remove_422 = _remove_422 ############################################################################ # Four spins & three sites # ############################################################################ p43 = _p43 add_43 = _add_43 remove_43 = _remove_43 ############################################################################ # Four spins & four sites # ############################################################################ p44 = _p44 add_44 = _add_44 remove_44 = _remove_44
# 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