# ================================== 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 os
import numpy as np
from wulfric.cell import get_params
from wulfric.crystal import get_atom_species
from magnopy._constants._si import JOULE, ELECTRON_VOLT, MILLI
from magnopy._package_info import logo
from magnopy._parameters._p22 import to_dmi, to_symm_anisotropy
from magnopy._spinham._convention import Convention
from magnopy._spinham._hamiltonian import SpinHamiltonian
# Save local scope at this moment
old_dir = set(dir())
old_dir.add("old_dir")
def _verified_materials(materials, M):
if materials is None:
materials = [i for i in range(M)]
else:
if len(materials) != M:
raise ValueError(f"Expected {M} materials, got {len(materials)}.")
materials_pool = set(materials)
higher_material = max(materials_pool)
for i in range(0, higher_material + 1):
if i not in materials_pool:
raise ValueError(
f"Materials indices should be consecutive integers between 0 and {higher_material}. Missing {i}."
)
return materials
[docs]
def dump_vampire(
spinham: SpinHamiltonian,
seedname="vampire",
anisotropic=True,
dmi=True,
custom_mask=None,
decimals=5,
materials=None,
no_logo=False,
) -> None:
"""
Saves spin Hamiltonian in the format suitable for |Vampire|_ (.UCF and .mat).
Parameters
----------
spinham : :py:class:`.SpinHamiltonian`
Spin Hamiltonian object to be saved.
seedname : str, default "vampire"
Seedname for the .UCF and .mat files. Extensions are added automatically. Input
file is independent of ``seedname`` and always has the name "input-template".
anisotropic : bool, default True
Whether to output anisotropic exchange.
dmi : bool, default True
Whether to output DMI exchange.
custom_mask : func, optional
Custom mask for the exchange parameter. Function that takes (3, 3)
:numpy:`ndarray` as an input and returns (3, 3) :numpy:`ndarray` as an output. If
given, then ``anisotropic`` and ``dmi`` parameters are ignored.
.. code-block:: python
parameter_to_output = custom_mask(parameter)
decimals : int, default 4
Number of decimals to be printed (only for the exchange values).
materials : list of int, optional
List of materials for the atoms. Length is the same as the number of magnetic
atoms in the ``spinham`` (``spinham.M``). Order is the same as in
:py:attr:`.SpinHamiltonian.magnetic_atoms`. If none given, each magnetic atom is
considered as a separate material. Material indices start from 0 and should
contain all consecutive integers between 0 and number of materials. Number of
materials cannot be higher than number of magnetic atoms.
no_logo : bool, default False
Whether to include the logo in the output files.
Notes
-----
Examples of the correct ``materials`` list for 5 magnetic atoms
.. code-block:: python
[0, 0, 0, 0, 0]
[1, 3, 2, 1, 0]
[0, 1, 2, 3, 4]
Examples of the incorrect ``materials`` list for 5 magnetic atoms
.. code-block:: python
[0, 6, 0, 0, 0]
[1, 3, 3, 1, 0]
[1, 2, 3, 4, 5]
"""
head, _ = os.path.split(seedname)
if head != "":
os.makedirs(head, exist_ok=True)
dump_vampire_ucf(
spinham,
filename=f"{seedname}.UCF",
anisotropic=anisotropic,
dmi=dmi,
custom_mask=custom_mask,
decimals=decimals,
materials=materials,
no_logo=no_logo,
)
dump_vampire_mat(
spinham,
filename=f"{seedname}.mat",
materials=materials,
no_logo=no_logo,
)
with open(os.path.join(head, "input-template"), "w", encoding="utf-8") as file:
if not no_logo:
file.write(f"{logo(comment=True, date_time=True)}\n")
file.write(
"\n".join(
[
"#------------------------------------------",
f"material:file={seedname}.mat",
f"material:unit-cell-file = {seedname}.UCF",
"#------------------------------------------",
"# TODO: simulation setup",
]
)
)
[docs]
def dump_vampire_mat(
spinham: SpinHamiltonian, filename, materials=None, no_logo=False
) -> None:
"""
Generates .mat file for |Vampire|_.
Parameters
----------
spinham : :py:class:`.SpinHamiltonian`
Spin Hamiltonian object to be saved.
filename : str
Name for the .mat file. Extension ".mat" is added if not present.
materials : list of int, optional
List of materials for the atoms. Length is the same as the number of magnetic
atoms in the ``spinham`` (``spinham.M``). Order is the same as in
:py:attr:`.SpinHamiltonian.magnetic_atoms`. If none given, each magnetic atom is
considered as a separate material. Material indices start from 0 and should
contain all consecutive integers between 0 and number of materials. Number of
materials cannot be higher than number of magnetic atoms.
no_logo : bool, default False
Whether to include the logo in the output files.
Raises
------
ValueError
If ``materials`` list is given and its length is not equal to the number of
magnetic atoms in the ``spinham``.
ValueError
If ``materials`` list does not contain all consecutive integers between 0 and
the highest material index.
Notes
-----
Examples of the correct ``materials`` list for 5 magnetic atoms
.. code-block:: python
[0, 0, 0, 0, 0]
[1, 3, 2, 1, 0]
[0, 1, 2, 3, 4]
Examples of the incorrect ``materials`` list for 5 magnetic atoms
.. code-block:: python
[0, 6, 0, 0, 0]
[1, 3, 3, 1, 0]
[1, 2, 3, 4, 5]
"""
if len(filename) < 4 or filename[-4:] != ".mat":
filename += ".mat"
materials = _verified_materials(materials, spinham.M)
if no_logo:
text = []
else:
text = [logo(comment=True, date_time=True)]
text.append(f"material:num-materials = {max(materials) + 1}")
for i, (material, name, spin, g_factor) in enumerate(
zip(
materials,
spinham.magnetic_atoms.names,
spinham.magnetic_atoms.spins,
spinham.magnetic_atoms.g_factors,
)
):
if material not in materials[:i]:
m_i = material + 1
text.append("#---------------------------------------------------")
text.append(f"# Material {m_i}")
text.append("#---------------------------------------------------")
text.append(f"material[{m_i}]:material-name = {name}")
text.append(f"material[{m_i}]:material-element = {get_atom_species(name)}")
text.append(f"material[{m_i}]:atomic-spin-moment={spin * g_factor} ! muB")
text.append(f"material[{m_i}]:initial-spin-direction = random")
text.append(f"material[{m_i}]:damping-constant = 0.1")
text.append(f"material[{m_i}]:uniaxial-anisotropy-constant = 0.0")
text.append("#---------------------------------------------------")
with open(filename, "w", encoding="utf-8") as file:
file.write("\n".join(text))
[docs]
def dump_vampire_ucf(
spinham: SpinHamiltonian,
filename,
anisotropic=True,
dmi=True,
custom_mask=None,
decimals=5,
materials=None,
no_logo=False,
) -> None:
"""
Generates .UCF file for |Vampire|_.
Parameters
----------
spinham : :py:class:`.SpinHamiltonian`
Spin Hamiltonian object to be saved.
filename : str, optional
Name for the .UCF file. Extension ".UCF" is added if not present.
anisotropic : bool, default True
Whether to output anisotropic exchange.
dmi : bool, default True
Whether to output DMI exchange.
custom_mask : func, optional
Custom mask for the exchange parameter. Function that takes (3, 3)
:numpy:`ndarray` as an input and returns (3, 3) :numpy:`ndarray` as an output. If
given, then ``anisotropic`` and ``dmi`` parameters are ignored.
.. code-block:: python
parameter_to_output = custom_mask(parameter)
decimals : int, default 4
Number of decimals to be printed (only for the exchange values).
materials : list of int, optional
List of materials for the atoms. Length is the same as the number of magnetic
atoms in the ``spinham`` (``spinham.M``). Order is the same as in
:py:attr:`.SpinHamiltonian.magnetic_atoms`. If none given, each magnetic atom is
considered as a separate material. Material indices start from 0 and should
contain all consecutive integers between 0 and number of materials. Number of
materials cannot be higher than number of magnetic atoms.
no_logo : bool, default False
Whether to include the logo in the output files.
Raises
------
ValueError
If ``materials`` list is given and its length is not equal to the number of
magnetic atoms in the ``spinham``.
ValueError
If ``materials`` list does not contain all consecutive integers between 0 and
the highest material index.
Notes
-----
Examples of the correct ``materials`` list for 5 magnetic atoms
.. code-block:: python
[0, 0, 0, 0, 0]
[1, 3, 2, 1, 0]
[0, 1, 2, 3, 4]
Examples of the incorrect ``materials`` list for 5 magnetic atoms
.. code-block:: python
[0, 6, 0, 0, 0]
[1, 3, 3, 1, 0]
[1, 2, 3, 4, 5]
"""
if len(filename) < 4 or filename[-4:] != ".UCF":
filename += ".UCF"
materials = _verified_materials(materials, spinham.M)
original_convention = spinham.convention
spinham.convention = Convention.get_predefined(name="Vampire")
if no_logo:
text = []
else:
text = [logo(comment=True, date_time=True)]
a, b, c, _, _, _ = get_params(spinham.cell)
text.append("# Unit cell size:")
text.append(f"{a:.8f} {b:.8f} {c:.8f}")
text.append("# Unit cell lattice vectors:")
text.append(
f"{spinham.cell[0][0]:15.8f} {spinham.cell[0][1]:15.8f} {spinham.cell[0][2]:15.8f}"
)
text.append(
f"{spinham.cell[1][0]:15.8f} {spinham.cell[1][1]:15.8f} {spinham.cell[1][2]:15.8f}"
)
text.append(
f"{spinham.cell[2][0]:15.8f} {spinham.cell[2][1]:15.8f} {spinham.cell[2][2]:15.8f}"
)
text.append("# Atoms")
text.append(f"{len(spinham.magnetic_atoms.names)} {len(np.unique(materials))}")
for alpha in range(spinham.M):
position = spinham.magnetic_atoms.positions[alpha]
text.append(
f"{alpha:<5} {position[0]:15.8f} {position[1]:15.8f} {position[2]:15.8f} {materials[alpha]:>5}"
)
text.append("# Interactions")
text.append(f"{len(spinham.p22)} tensorial")
IID = 0
fmt = f"{7 + decimals}.{decimals}e"
# Write (two spins & one site)
for alpha, J in spinham.p21:
alpha = spinham.map_to_magnetic[alpha]
if custom_mask is not None:
J = custom_mask(J)
else:
if not dmi:
J -= to_dmi(J, matrix_form=True)
if not anisotropic:
J -= to_symm_anisotropy(J)
J = J * (MILLI * ELECTRON_VOLT) / JOULE
text.append(
f"{IID:<5} {alpha:>3} {alpha:>3} {0:>2} {0:>2} {0:>2} "
f"{J[0][0]:{fmt}} {J[0][1]:{fmt}} {J[0][2]:{fmt}} "
f"{J[1][0]:{fmt}} {J[1][1]:{fmt}} {J[1][2]:{fmt}} "
f"{J[2][0]:{fmt}} {J[2][1]:{fmt}} {J[2][2]:{fmt}}"
)
IID += 1
# Write (two spins & two sites)
bonds = []
for alpha, beta, nu, J in spinham.p22:
alpha = spinham.map_to_magnetic[alpha]
beta = spinham.map_to_magnetic[beta]
if custom_mask is not None:
J = custom_mask(J)
else:
if not dmi:
J -= to_dmi(J, matrix_form=True)
if not anisotropic:
J -= to_symm_anisotropy(J)
# print(alpha, beta, nu)
# print(J, end="\n\n")
J = J * (MILLI * ELECTRON_VOLT) / JOULE
r_alpha = np.array(spinham.magnetic_atoms.positions[alpha])
r_beta = np.array(spinham.magnetic_atoms.positions[beta])
distance = np.linalg.norm((r_beta - r_alpha + nu) @ spinham.cell)
bonds.append([alpha, beta, nu, J, distance])
bonds = sorted(bonds, key=lambda x: x[4])
for alpha, beta, (i, j, k), J, _ in bonds:
text.append(
f"{IID:<5} {alpha:>3} {beta:>3} {i:>2} {j:>2} {k:>2} "
f"{J[0][0]:{fmt}} {J[0][1]:{fmt}} {J[0][2]:{fmt}} "
f"{J[1][0]:{fmt}} {J[1][1]:{fmt}} {J[1][2]:{fmt}} "
f"{J[2][0]:{fmt}} {J[2][1]:{fmt}} {J[2][2]:{fmt}}"
)
IID += 1
spinham.convention = original_convention
with open(filename, "w", encoding="utf-8") as file:
file.write("\n".join(text))
# 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