"""
Magnetization measurement.
Separate diamagnetic and ferromagnetic contributions.
"""
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.optimize import curve_fit as scipy_optimize_curve_fit
from physicslab.curves import magnetic_hysteresis_loop
from physicslab.utility import _ColumnsBase, get_name
[docs]def process(data, diamagnetism=True, ferromagnetism=True):
""" Bundle method.
Parameter :attr:`data` must include magnetic field and magnetization.
See :class:`Columns` for details and column names.
Output :attr:`ratio_DM_FM` compares max values - probably for the
strongest magnetic field.
Supplying `None` for :attr:`data` returns :class:`pandas.Series` of
the same columns with values being units.
:param data: Measured data. If None, return units instead
:type data: pandas.DataFrame or None
:param diamagnetism: Look for diamagnetism contribution, defaults to True
:type diamagnetism: bool, optional
:param ferromagnetism: Look for ferromagnetism contribution,
defaults to True
:type ferromagnetism: bool, optional
:return: Derived quantities listed in :meth:`Columns.process` or units
:rtype: pandas.Series
"""
if data is None:
from physicslab.experiment import UNITS
name = UNITS
# [B] = Oe; [M] = emu
magnetic_susceptibility = 'emu/Oe'
offset = 'emu'
saturation = 'emu'
remanence = 'emu'
coercivity = 'Oe'
ratio_DM_FM = '1'
else:
name = get_name(data)
measurement = Measurement(data)
(magnetic_susceptibility, offset, saturation, remanence,
coercivity, ratio_DM_FM) = [np.nan] * 6
if diamagnetism:
magnetic_susceptibility, offset = measurement.diamagnetism(
from_residual=True)
if ferromagnetism:
saturation, remanence, coercivity = measurement.ferromagnetism(
from_residual=True)
if diamagnetism and ferromagnetism:
ratio_DM_FM = abs(
measurement.data[Columns.DIAMAGNETISM].iloc[-1]
/ measurement.data[Columns.FERROMAGNETISM].iloc[-1])
return pd.Series(
data=(magnetic_susceptibility, offset, saturation, remanence,
coercivity, ratio_DM_FM),
index=Columns.process(), name=name)
[docs]class Columns(_ColumnsBase):
""" Bases: :class:`physicslab.utility._ColumnsBase`
Column names.
"""
MAGNETICFIELD = 'B'
MAGNETIZATION = 'M'
# :data:`data` residue after DM/FM component subtraction.
RESIDUAL_MAGNETIZATION = 'M_residual'
FERROMAGNETISM = 'Ferromagnetism'
DIAMAGNETISM = 'Diamagnetism'
MAGNETIC_SUSCEPTIBILITY = 'magnetic_susceptibility'
OFFSET = 'offset'
SATURATION = 'saturation'
REMANENCE = 'remanence'
COERCIVITY = 'coercivity'
RATIO_DM_FM = 'ratio_DM_FM'
[docs] @classmethod
def mandatory(cls):
""" Get the current mandatory column names.
:rtype: set(str)
"""
return {cls.MAGNETICFIELD, cls.MAGNETIZATION}
[docs] @classmethod
def process(cls):
""" Get the current values of the :func:`process` output column names.
:rtype: lits(str)
"""
return [cls.MAGNETIC_SUSCEPTIBILITY, cls.OFFSET, cls.SATURATION,
cls.REMANENCE, cls.COERCIVITY, cls.RATIO_DM_FM]
[docs]class Measurement():
""" Magnetization vs magnetic field measurement.
Copy magnetization column as :data:`Columns.RESIDUAL_MAGNETIZATION`,
so individual magnetic effects can be subtracted.
:param pandas.DataFrame data: Magnetic field and magnetization data.
:raises ValueError: If :attr:`data` is missing a mandatory column
"""
def __init__(self, data):
if not Columns.mandatory().issubset(data.columns):
raise ValueError('Missing mandatory column. See Columns class.')
self.data = data
self.data[Columns.RESIDUAL_MAGNETIZATION] = \
self.data[Columns.MAGNETIZATION].copy()
def _magnetization_label(self, from_residual):
if from_residual:
return Columns.RESIDUAL_MAGNETIZATION
else:
return Columns.MAGNETIZATION
[docs] def diamagnetism(self, from_residual=False):
""" Find diamagnetic component of overall magnetization.
Simulated data are subtracted from residue column (making it centred).
:param from_residual: Use residual data instead of the original data,
defaults to False
:type from_residual: bool, optional
:return: Magnetic susceptibility and magnetization offset
:rtype: tuple
"""
coef = self._lateral_linear_fit(
self.data[Columns.MAGNETICFIELD],
self.data[self._magnetization_label(from_residual)]
)
fit = np.polynomial.polynomial.polyval(
self.data[Columns.MAGNETICFIELD], coef)
self.data[Columns.DIAMAGNETISM] = fit
self.data.loc[:, Columns.RESIDUAL_MAGNETIZATION] -= fit
offset, magnetic_susceptibility = coef
return magnetic_susceptibility, offset
@staticmethod
def _lateral_linear_fit(x, y, percentage=10):
""" Linear fit bypassing central region (there can be hysteresis loop).
Separate fit of top and bottom part. Then average.
:param numpy.ndarray x: Free variable
:param numpy.ndarray y: Function value
:param percentage: How far from either side should the fitting go.
Using value, because center can be measured with higher accuracy,
defaults to 10
:type percentage: int, optional
:return: Array of fitting parameters sorted in ascending order.
:rtype: numpy.ndarray
"""
lateral_interval = (max(x) - min(x)) * percentage / 100
mask = x >= max(x) - lateral_interval
popt_top = np.polynomial.polynomial.polyfit(x[mask], y[mask], 1)
mask = x <= min(x) + lateral_interval
popt_bottom = np.polynomial.polynomial.polyfit(x[mask], y[mask], 1)
# Two-element array (const, slope).
return (popt_bottom + popt_top) / 2
[docs] def ferromagnetism(self, from_residual=False, p0=None):
""" Find ferromagnetic component of overall magnetization.
| Simulated data are subtracted from residue column.
| Hysteresis loop shape can be found in
:meth:`~physicslab.curves.magnetic_hysteresis_loop`.
:param from_residual: Use residual data instead of the original data,
defaults to False
:type from_residual: bool, optional
:param p0: Initial guess of hysteresis loop parameters. If None, the
parameters will be estimated automatically, defaults to None
:type p0: tuple, optional
:return: Saturation, remanence and coercivity
:rtype: tuple
"""
magnetization = self.data[self._magnetization_label(from_residual)]
if p0 is None:
p0 = self._ferromagnetism_parameter_guess(
B=self.data[Columns.MAGNETICFIELD], M=magnetization)
popt, pcov = scipy_optimize_curve_fit(
f=magnetic_hysteresis_loop,
xdata=self.data[Columns.MAGNETICFIELD],
ydata=magnetization,
p0=p0
)
saturation, remanence, coercivity = popt
fit = magnetic_hysteresis_loop(
self.data[Columns.MAGNETICFIELD], *popt)
self.data[Columns.FERROMAGNETISM] = fit
self.data.loc[:, Columns.RESIDUAL_MAGNETIZATION] -= fit
return saturation, remanence, coercivity
@staticmethod
def _ferromagnetism_parameter_guess(B, M):
""" Try to guess ferromagnetic hysteresis loop parameters.
:param float B: Magnetic field
:param float M: Magnetization
:return: Saturation, remanence, coercivity
:rtype: tuple
"""
saturation = abs(max(M) - min(M)) * 0.5 # 50 %
remanence = saturation * 0.5 # 25 %
coercivity = abs(max(B) - min(B)) * 0.1 # 10 %
return saturation, remanence, coercivity
[docs]def plot(data):
""" Plot single magnetization measurement separated data
:param data:
:type data: list[pandas.DataFrame]
:return: Same objects as from :meth:`matplotlib.pyplot.subplots`
:rtype: tuple[~matplotlib.figure.Figure, ~matplotlib.axes.Axes]
"""
fig, ax = plt.subplots(num='Magnetism type')
B = data[Columns.MAGNETICFIELD]
ax.plot(B, data[Columns.MAGNETIZATION], 'ko', label='Data')
ax.plot(B, data[Columns.DIAMAGNETISM], 'r-', label='Diamagnetism')
ax.plot(B, data[Columns.FERROMAGNETISM], 'b-', label='Ferromagnetism')
ax.plot(B, data[Columns.RESIDUAL_MAGNETIZATION], 'g-', label='Residue')
ax.set_xlabel('Magnetic field / Oe')
ax.set_ylabel('Magnetization / emu')
ax.legend()
return fig, ax