Source code for magnopy.experimental

# ================================== 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 =================================
import random
import numpy as np
from magnopy._plotly_engine import PlotlyEngine
from wulfric.crystal import get_vector
from magnopy._parameters._p22 import to_dmi, to_iso
from magnopy._spinham._hamiltonian import SpinHamiltonian

try:
    import matplotlib.pyplot as plt

    MATPLOTLIB_AVAILABLE = True
    MATPLOTLIB_ERROR_MESSAGE = "If you see this message, please contact developers of the code (see magnopy.org)."
except ImportError:
    MATPLOTLIB_AVAILABLE = False
    MATPLOTLIB_ERROR_MESSAGE = "\n".join(
        [
            "Installation of matplotlib is not found, can not produce .png files",
            "Please install matplotlib with",
            "",
            "    pip install matplotlib",
            "",
        ]
    )

from magnopy._constants._units import _MAGNON_ENERGY_UNITS


[docs] def plot_spinham( spinham: SpinHamiltonian, distance_digits=5, plot_dmi=True, dmi_vectors_scale=1, _sphinx_gallery_fix=False, ): r""" Visualizes spin Hamiltonian. .. warning:: Experimental feature. Only 1, 21, 22 parameters are implemented. .. versionadded:: 0.2.0 Parameters ---------- spinham : :py:class:`.SpinHamiltonian` Spin Hamiltonian distance_digits : int, default 5 Precision for comparing two linear distances. plot_dmi : bool, default True Whether to plot DMI vectors dmi_scale : float, default 1.0 Scale for the maximum dmi vector length. Returns ------- pe1 : :py:class:`.PlotlyEngine` Instance of the magnopy's plot engine, with plotted spin Hamiltonian. Ready to be saved or showed. Only on-site parameters are plotted pe2 : :py:class:`.PlotlyEngine` Instance of the magnopy's plot engine, with plotted spin Hamiltonian. Ready to be saved or showed. Only two-spins/two-sites parameters are plotted. """ pe1 = PlotlyEngine(_sphinx_gallery_fix=_sphinx_gallery_fix) pe1.plot_cell( cell=spinham.cell, color="LightGrey", legend_label="(0, 0, 0) unit cell", legend_group="(0, 0, 0) unit cell", ) points = spinham.magnetic_atoms.positions @ spinham.cell hoverinfo = [ "<br>".join( [ f'Magnetic center "{name}", located at', f" Relative: ({pos[0]:.8f}, {pos[1]:.8f}, {pos[2]:.8f})", f" Absolute: ({(pos @ spinham.cell)[0]:.8f}, {(pos @ spinham.cell)[1]:.8f}, {(pos @ spinham.cell)[2]:.8f})", "", f"With spin value {spin:.3f}", "", f"and g-factor {g_factor:.3f}", ] ) for name, pos, spin, g_factor in zip( spinham.magnetic_atoms.names, spinham.magnetic_atoms.positions, spinham.magnetic_atoms.spins, spinham.magnetic_atoms.g_factors, ) ] for alpha, parameter in spinham.p1: alpha = spinham.map_to_magnetic[alpha] hoverinfo[alpha] += "<br>".join( [ "", "One-spin parameter:", f" {parameter[0]:10.5f} {parameter[0]:10.5f} {parameter[0]:10.5f}", "", ] ) for alpha, parameter in spinham.p21: alpha = spinham.map_to_magnetic[alpha] hoverinfo[alpha] += "<br>".join( [ "", "Two-spin parameter:", f" {parameter[0][0]:10.5f} {parameter[1][0]:10.5f} {parameter[2][0]:10.5f}", f" {parameter[0][1]:10.5f} {parameter[1][1]:10.5f} {parameter[2][1]:10.5f}", f" {parameter[0][2]:10.5f} {parameter[1][2]:10.5f} {parameter[2][2]:10.5f}", "", ] ) with_parameters = [] without_parameters = [] for i, text in enumerate(hoverinfo): if text == "": without_parameters.append(i) else: with_parameters.append(i) xyz = points[with_parameters].T pe1.fig.add_traces( data=dict( type="scatter3d", mode="markers", legendgroup="with parameters", name="Magnetic cites with parameters\n(hover to see the values)", showlegend=True, x=xyz[0], y=xyz[1], z=xyz[2], marker=dict(size=10, color="Black"), text=[hoverinfo[i] for i in with_parameters], hoverinfo="text", ), ) if len(without_parameters) != 0: xyz = points[without_parameters].T pe1.fig.add_traces( data=dict( type="scatter3d", mode="markers", legendgroup="without parameters", name="Magnetic cites without parameters", showlegend=True, x=xyz[0], y=xyz[1], z=xyz[2], marker=dict(size=5, color="Grey"), hoverinfo="none", ), ) # Fix for plotly #7143 points = points.T pe1._update_range( x_min=points[0].min(), x_max=points[0].max(), y_min=points[1].min(), y_max=points[1].max(), z_min=points[2].min(), z_max=points[2].max(), ) pe2 = PlotlyEngine(_sphinx_gallery_fix=_sphinx_gallery_fix) start_points = [] end_points = [] parameters = [] distances = [] dmi_vectors = [] dmi_positions = [] alphas = [] betas = [] nus = [] cells_to_plot = [] for alpha, beta, nu, parameter in spinham.p22: vector = get_vector( cell=spinham.cell, atoms=spinham.atoms, atom1=alpha, atom2=beta, R=nu ) start_points.append(spinham.atoms.positions[alpha] @ spinham.cell) end_points.append(start_points[-1] + vector) distances.append(round(np.linalg.norm(vector), ndigits=distance_digits)) parameters.append(parameter) alphas.append(alpha) betas.append(beta) nus.append(nu) dmi_vectors.append(to_dmi(parameter=parameter)) dmi_positions.append(start_points[-1] + vector / 2) if nu != (0, 0, 0): cells_to_plot.append(nu) indices = np.argsort(distances) start_points = np.array(start_points)[indices] end_points = np.array(end_points)[indices] parameters = np.array(parameters)[indices] distances = np.array(distances)[indices] dmi_positions = np.array(dmi_positions)[indices] dmi_vectors = np.array(dmi_vectors)[indices] alphas = np.array(alphas, dtype=int)[indices] betas = np.array(betas, dtype=int)[indices] nus = np.array(nus, dtype=int)[indices] dmi_max_length = np.array([np.linalg.norm(_) for _ in dmi_vectors]).max() if dmi_max_length != 0: dmi_vectors = dmi_vectors / dmi_max_length * dmi_vectors_scale unique_distances = np.unique(distances) chars = "0123456789ABCDEF" random_colors = [ "#" + "".join(random.sample(chars, 6)) for _ in range(len(unique_distances)) ] colors = dict(zip(unique_distances, random_colors)) colors = [colors[_] for _ in distances] legend_labels = [f"d = {_:.{distance_digits}f}" for _ in unique_distances] legend_groups = dict(zip(unique_distances, legend_labels)) legend_labels = dict(zip(unique_distances, legend_labels)) hoverinfo = [ "<br>".join( [ "Bond", f' from magnetic center "{spinham.atoms.names[alpha]}" in (0, 0, 0) unit cell', f' to magnetic center "{spinham.atoms.names[beta]}" in ({nu[0]}, {nu[1]}, {nu[2]}) unit cell', "", "Full two-spins/two-sites parameter:", f" {param[0][0]:10.5f} {param[0][1]:10.5f} {param[0][2]:10.5f}", f" {param[1][0]:10.5f} {param[1][1]:10.5f} {param[1][2]:10.5f}", f" {param[2][0]:10.5f} {param[2][1]:10.5f} {param[2][2]:10.5f}", "", f"Isotropic: {to_iso(param):10.5f}", "", f"DMI: {to_dmi(param)[0]:10.5f} {to_dmi(param)[1]:10.5f} {to_dmi(param)[2]:10.5f}", ] ) for alpha, beta, nu, param in zip(alphas, betas, nus, parameters) ] legend_label = "Other_cells" magnetic_sites = [] for i, j, k in cells_to_plot: shift = [i, j, k] @ spinham.cell pe2.plot_cell( cell=spinham.cell, legend_group="other cells", legend_label=legend_label, color="LightGrey", shift=shift, plot_vectors=False, ) if legend_label is not None: legend_label = None magnetic_sites.extend( spinham.magnetic_atoms.positions @ spinham.cell + shift[np.newaxis, :] ) pe2.plot_cell( cell=spinham.cell, legend_label="(0, 0, 0) unit cell", color="Grey", plot_vectors=True, ) pe2.plot_points( points=magnetic_sites, colors="LightGrey", legend_group="Magnetic sites", scale=2, ) pe2.plot_points( points=spinham.magnetic_atoms.positions @ spinham.cell, colors="Grey", legend_label="Magnetic sites", legend_group="Magnetic sites", scale=2, ) for i in range(len(parameters)): x, y, z = np.array([start_points[i], end_points[i]]).T pe2.fig.add_traces( data=dict( type="scatter3d", mode="lines", legendgroup=legend_groups[distances[i]], name=legend_labels[distances[i]], showlegend=legend_labels[distances[i]] is not None, x=x, y=y, z=z, line=dict(color=colors[i], width=4), hoverinfo="text", text=hoverinfo[i], ), ) if plot_dmi: pe2.plot_vector( start_point=dmi_positions[i] - dmi_vectors[i] / 2, end_point=dmi_positions[i] + dmi_vectors[i] / 2, color=colors[i], legend_group=legend_groups[distances[i]], ) if legend_labels[distances[i]] is not None: legend_labels[distances[i]] = None return pe1, pe2
[docs] def change_cell(spinham, new_cell, new_atoms_specs): r""" Changes the unit cell of a spin Hamiltonian. .. warning:: Experimental feature. Not tested well. Parameters ---------- spinham : :py:class:`.SpinHamiltonian` new_cell : (3, 3) |array-like|_ new_atoms_specs : list of tuple .. code-block:: python new_atoms_specs = [(index, nu), ...] where ``index`` is an index of an atom in ``spinham.atoms`` and ``nu = (i,j,k)`` is the unit cell to which an atom belongs. Returns ------- new_spinham : :py:class:`.SpinHamiltonian` """ new_cell = np.array(new_cell) new_atoms = {} for key in spinham.atoms: new_atoms[key] = [] for atom_index, (i, j, k) in new_atoms_specs: for key in spinham.atoms: if key == "positions": position = spinham.atoms.positions[atom_index] new_position = ( np.array([(position[0] + i), (position[1] + j), (position[2] + k)]) @ spinham.cell @ np.linalg.inv(new_cell) ) new_atoms["positions"].append(new_position) else: new_atoms[key].append(spinham.atoms[key][atom_index]) map_to_indices = { tuple(np.around(new_atoms["positions"][i], decimals=2)): i for i in range(len(new_atoms["positions"])) } new_spinham = SpinHamiltonian( cell=new_cell, atoms=new_atoms, convention=spinham.convention ) def get_new_indices(atom_index, ijk): i, j, k = ijk position = spinham.atoms.positions[atom_index] new_position = ( np.array([(position[0] + i), (position[1] + j), (position[2] + k)])
[docs] @ spinham.cell @ np.linalg.inv(new_cell) ) new_ijk = new_position // 1 new_ijk = tuple(map(int, new_ijk)) new_index = map_to_indices[tuple(np.around(new_position % 1, decimals=2))] return new_index, new_ijk # Re-populate parameters for new_alpha, (atom_index, (i, j, k)) in enumerate(new_atoms_specs): # One spin for alpha, parameter in spinham._1: if alpha == atom_index: new_spinham.add_1(alpha=new_alpha, parameter=parameter) # Two spins for alpha, parameter in spinham._21: if alpha == atom_index: new_spinham.add_21(alpha=new_alpha, parameter=parameter) for alpha, beta, nu, parameter in spinham._22: if alpha == atom_index: new_beta, new_nu = get_new_indices( atom_index=beta, ijk=np.array(nu) + (i, j, k) ) new_spinham.add_22( alpha=new_alpha, beta=new_beta, nu=new_nu, parameter=parameter, when_present="replace", ) # Three spins for alpha, parameter in spinham._31: if alpha == atom_index: new_spinham.add_31(alpha=new_alpha, parameter=parameter) for alpha, beta, nu, parameter in spinham._32: if alpha == atom_index: new_beta, new_nu = get_new_indices( atom_index=beta, ijk=np.array(nu) + (i, j, k) ) new_spinham.add_32( alpha=new_alpha, beta=new_beta, nu=new_nu, parameter=parameter, when_present="replace", ) for alpha, beta, gamma, nu, _lambda, parameter in spinham._33: if alpha == atom_index: new_beta, new_nu = get_new_indices( atom_index=beta, ijk=np.array(nu) + (i, j, k) ) new_gamma, new_lambda = get_new_indices( atom_index=gamma, ijk=np.array(_lambda) + (i, j, k) ) new_spinham.add_33( alpha=new_alpha, beta=new_beta, gamma=new_gamma, nu=new_nu, _lambda=new_lambda, parameter=parameter, when_present="replace", ) # Four spins for alpha, parameter in spinham._41: if alpha == atom_index: new_spinham.add_41(alpha=new_alpha, parameter=parameter) for alpha, beta, nu, parameter in spinham._421: if alpha == atom_index: new_beta, new_nu = get_new_indices( atom_index=beta, ijk=np.array(nu) + (i, j, k) ) new_spinham.add_421( alpha=new_alpha, beta=new_beta, nu=new_nu, parameter=parameter, when_present="replace", ) for alpha, beta, nu, parameter in spinham._422: if alpha == atom_index: new_beta, new_nu = get_new_indices( atom_index=beta, ijk=np.array(nu) + (i, j, k) ) new_spinham.add_422( alpha=new_alpha, beta=new_beta, nu=new_nu, parameter=parameter, when_present="replace", ) for alpha, beta, gamma, nu, _lambda, parameter in spinham._43: new_beta, new_nu = get_new_indices( atom_index=beta, ijk=np.array(nu) + (i, j, k) ) new_gamma, new_lambda = get_new_indices( atom_index=gamma, ijk=np.array(_lambda) + (i, j, k) ) new_spinham.add_43( alpha=new_alpha, beta=new_beta, gamma=new_gamma, nu=new_nu, _lambda=new_lambda, parameter=parameter, when_present="replace", ) for ( alpha, beta, gamma, epsilon, nu, _lambda, rho, parameter, ) in spinham._44: new_beta, new_nu = get_new_indices( atom_index=beta, ijk=np.array(nu) + (i, j, k) ) new_gamma, new_lambda = get_new_indices( atom_index=gamma, ijk=np.array(_lambda) + (i, j, k) ) new_epsilon, new_rho = get_new_indices( atom_index=epsilon, ijk=np.array(rho) + (i, j, k) ) new_spinham.add_44( alpha=new_alpha, beta=new_beta, gamma=new_gamma, epsilon=new_epsilon, nu=new_nu, _lambda=new_lambda, rho=new_rho, parameter=parameter, when_present="replace", ) return new_spinham
# DEPRECATED in v0.4.0 # Remove in May 2026 def plot_dispersion(data, kp=None, output_filename=None, ylabel=None): r""" Plots some k-resolved data. .. deprecated:: 0.4.0 Function deprecated in 0.4.0 and will be removed in May 2026. Use :py:func:`magnopy.io.plot_dispersion()` instead. If only the ``data`` are given, then an index of the omegas is used for abscissa (x axis). Parameters ---------- data : (N, M) |array-like|_ Some k-resolved data. N (:math:`\ge 1`) is the amount of kpoints. M is the number of data modes/entries. Expected to be given in meV. kp : :py:class:`wulfric.Kpoints`, optional. Instance of the :py:class:`wulfric.Kpoints` class. It should be the same instance that were used in the preparation of the ``data``. output_filename : str, optional Name of the file for saving the image. If ``None``, then the graph would be opened in the interactive matplotlib window. ylabel : str, optional Label for the ordinate (y axis). Do not include units, units are included automatically. """ import warnings warnings.warn( "Function magnopy.experimental.plot_dispersion() was deprecated in 0.4.0 and will be removed in May 2026. Use magnopy.io.plot_dispersion() instead", DeprecationWarning, stacklevel=2, ) if not MATPLOTLIB_AVAILABLE: import warnings warnings.warn(MATPLOTLIB_ERROR_MESSAGE, RuntimeWarning, stacklevel=2) return data = np.array(data).T fig, ax = plt.subplots() if len(data.shape) == 2: for entry in data: if kp is not None: ax.plot(kp.flat_points(), entry, lw=1, color="#A47864") else: ax.plot(entry, lw=1, color="#A47864") ax.set_xlim(0, len(entry)) else: if kp is not None: ax.plot(kp.flat_points(), data, lw=1, color="#A47864") else: ax.plot(data, lw=1, color="#A47864") ax.set_xlim(0, len(data)) if kp is not None: ax.set_xticks(kp.ticks(), kp.labels, fontsize=13) ax.set_xlim(kp.ticks()[0], kp.ticks()[-1]) ax.vlines( kp.ticks(), 0, 1, lw=0.5, color="grey", ls="dashed", zorder=0, transform=ax.get_xaxis_transform(), ) if ylabel is not None: ax.set_ylabel(f"{ylabel}, meV", fontsize=15) ax.hlines( 0, 0, 1, lw=0.5, color="grey", linestyle="dashed", transform=ax.get_yaxis_transform(), ) # Add twin axis ylims = ax.get_ylim() meV_to_THz = _MAGNON_ENERGY_UNITS["mev"] / _MAGNON_ENERGY_UNITS["thz"] twinax = ax.twinx() twinax.set_ylim(meV_to_THz * ylims[0], meV_to_THz * ylims[1]) twinax.set_ylabel(f"{ylabel}, THz", fontsize=15) if output_filename is not None: plt.savefig(output_filename, dpi=400, bbox_inches="tight") else: plt.show() plt.close()