Source code for mammos_mumag.hysteresis

"""Functions for evaluating and processin the hysteresis loop."""

from __future__ import annotations

import pathlib
from typing import TYPE_CHECKING

import mammos_entity as me
import mammos_units as u
import matplotlib.pyplot as plt
import numpy as np
import pandas
import pandas as pd
import pyvista as pv
from pydantic import ConfigDict
from pydantic.dataclasses import dataclass

from mammos_mumag.materials import Materials
from mammos_mumag.parameters import Parameters
from mammos_mumag.simulation import Simulation

if TYPE_CHECKING:
    import matplotlib
    import pyvista


[docs] def run( Ms: float | u.Quantity | me.Entity, A: float | u.Quantity | me.Entity, K1: float | u.Quantity | me.Entity, mesh_filepath: pathlib.Path, hstart: float | u.Quantity, hfinal: float | u.Quantity, hstep: float | u.Quantity | None = None, hnsteps: int = 20, outdir: str | pathlib.Path = "hystloop", ) -> Result: r"""Run hysteresis loop. Args: Ms: Spontaneous magnetisation in :math:`\mathrm{A}/\mathrm{m}`. A: Exchange stiffness constant in :math:`\mathrm{J}/\mathrm{m}`. K1: First magnetocrystalline anisotropy constant in :math:`\mathrm{J}/\mathrm{m}^3`. mesh_filepath: TODO hstart: Initial strength of the external field. hfinal: Final strength of the external field. hstep: Step size. hnsteps: Number of steps in the field sweep. outdir: Directory where simulation results are written to. Returns: Result object. """ if hstep is None: hstep = (hfinal - hstart) / hnsteps if not isinstance(A, u.Quantity) or A.unit != u.J / u.m: A = me.A(A, unit=u.J / u.m) if not isinstance(K1, u.Quantity) or K1.unit != u.J / u.m**3: K1 = me.Ku(K1, unit=u.J / u.m**3) if not isinstance(Ms, u.Quantity) or Ms.unit != u.A / u.m: Ms = me.Ms(Ms, unit=u.A / u.m) sim = Simulation( mesh_filepath=mesh_filepath, materials=Materials( domains=[ { "theta": 0, "phi": 0.0, "K1": K1, "K2": me.Ku(0), "Ms": Ms, "A": A, }, { "theta": 0.0, "phi": 0.0, "K1": me.Ku(0), "K2": me.Ku(0), "Ms": me.Ms(0), "A": me.A(0), }, { "theta": 0.0, "phi": 0.0, "K1": me.Ku(0), "K2": me.Ku(0), "Ms": me.Ms(0), "A": me.A(0), }, ], ), parameters=Parameters( size=1.0e-9, scale=0, m_vect=[0, 0, 1], hstart=hstart.to(u.T, equivalencies=u.magnetic_flux_field()).value, hfinal=hfinal.to(u.T, equivalencies=u.magnetic_flux_field()).value, hstep=hstep.to(u.T, equivalencies=u.magnetic_flux_field()).value, h_vect=[0.01745, 0, 0.99984], mstep=0.4, mfinal=-1.2, tol_fun=1e-10, tol_hmag_factor=1, precond_iter=10, ), ) sim.run_loop(outdir=outdir, name="hystloop") df = pd.read_csv( f"{outdir}/hystloop.dat", delimiter=" ", names=["configuration_type", "mu0_Hext", "polarisation", "energy_density"], ) return Result( H=me.Entity( "ExternalMagneticField", value=(df["mu0_Hext"].to_numpy() * u.T).to( u.A / u.m, equivalencies=u.magnetic_flux_field() ), unit=u.A / u.m, ), M=me.Ms( (df["polarisation"].to_numpy() * u.T).to( u.A / u.m, equivalencies=u.magnetic_flux_field() ), unit=u.A / u.m, ), energy_density=me.Entity( "EnergyDensity", value=df["energy_density"], unit=u.J / u.m**3 ), configurations={ i + 1: fname for i, fname in enumerate( sorted(pathlib.Path(outdir).resolve().glob("*.vtu")) ) }, configuration_type=df["configuration_type"].to_numpy(), )
[docs] @dataclass(config=ConfigDict(arbitrary_types_allowed=True, frozen=True)) class Result: """Hysteresis loop Result.""" H: me.Entity """Array of external field strengths.""" M: me.Entity """Array of spontaneous magnetization values for the field strengths.""" energy_density: me.Entity | None = None """Array of energy densities for the field strengths.""" configuration_type: np.ndarray | None = None """Array of indices of representative configurations for the field strengths.""" configurations: dict[int, pathlib.Path] | None = None """Mapping of configuration indices to file paths.""" @property def dataframe(self) -> pandas.DataFrame: """Dataframe containing the result data of the hysteresis loop.""" return pd.DataFrame( { "configuration_type": self.configuration_type, "H": self.H.q, "M": self.M.q, "energy_density": self.energy_density.q, } )
[docs] def plot( self, duplicate: bool = True, duplicate_change_color: bool = True, configuration_marks: bool = False, ax: matplotlib.axes.Axes | None = None, label: str | None = None, **kwargs, ) -> matplotlib.axes.Axes: """Plot hysteresis loop. Args: duplicate: Also plot loop with -M and -H to simulate full hysteresis. configuration_marks: Show markers where a configuration has been saved. ax: Matplotlib axes object to which the plot is added. A new one is create if not passed. kwargs: Additional keyword arguments passed to `ax.plot` when plotting the hysteresis lines. Returns: The `matplotlib.axes.Axes` object which was used to plot the hysteresis loop """ if ax: ax = ax else: _, ax = plt.subplots() if label: (line,) = ax.plot(self.dataframe.H, self.dataframe.M, label=label, **kwargs) else: (line,) = ax.plot(self.dataframe.H, self.dataframe.M, **kwargs) j = 0 if configuration_marks: for _, row in self.dataframe.iterrows(): idx = int(row.configuration_type) if idx != j: plt.plot(row.H, row.M, "rx") j = idx ax.annotate( j, xy=(row.H, row.M), xytext=(-2, -10), textcoords="offset points", ) ax.set_title("Hysteresis Loop") ax.set_xlabel(self.H.axis_label) ax.set_ylabel(self.M.axis_label) if label: ax.legend() if duplicate: if not duplicate_change_color: kwargs.setdefault("color", line.get_color()) ax.plot(-self.dataframe.H, -self.dataframe.M, **kwargs) return ax
[docs] def plot_configuration( self, idx: int, jupyter_backend: str = "trame", plotter: pyvista.Plotter | None = None, ) -> None: """Plot configuration with index `idx`. This method does only directly show the plot if no plotter is passed in. Otherwise, the caller must call ``plotter.show()`` separately. This behavior is based on the assumption that the user will want to further modify the plot before displaying/saving it when passing a plotter. Args: idx: Index of the configuration. jupyter_backend: Plotting backend. plotter: Pyvista plotter to which glyphs will be added. A new plotter is created if no plotter is passed. """ config = pv.read(self.configurations[idx]) config["m_norm"] = np.linalg.norm(config["m"], axis=1) glyphs = config.glyph( orient="m", scale="m_norm", ) pl = plotter or pv.Plotter() pl.add_mesh( glyphs, scalars=glyphs["GlyphVector"][:, 2], lighting=False, cmap="coolwarm", clim=[-1, 1], scalar_bar_args={"title": "m_z"}, ) pl.show_axes() if plotter is None: pl.show(jupyter_backend=jupyter_backend)