# SPDX-FileCopyrightText: 2020/2021 Jonathan Pieper <ody55eus@mailbox.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""
Evaluates Hysteresis Loops.
"""
from .single import SingleM
import logging
import re
from glob import glob
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.interpolate
import scipy.constants
import scipy.stats
[docs]class Hloop(SingleM):
"""This class represents the measurement of a hysteresis loop. It evaluates
and plots the data.
It collects all files that match the following regex:
.. code-block:: python
files = glob("data/Hloop/[mM]%s_*.dat" % measurement_number)
The files should contain a file that contains the word "up" or "down"
indicating the direction of the field sweep.
If one file contains the word 'Gradio' it expects a gradiometry
measurement. If the filename contains the word 'Parallel' it expects a
parallel measurement. If the filename contains neither of these words the
variables need to be set manually.
"""
def __init__(self, measurement_number, **kwargs):
"""
Args:
measurement_number:
**kwargs:
"""
super().__init__()
self.logger = logging.getLogger('Hloop')
self.name = 'Hloop'
# Defining Constants
header_gradio = ["Time", "B", "Vx8", "Vy8", "Vr8", "Vth8", "TempRO CU",
"TempRO YOKE", "TempCX CU", "SampTemp", "Temp1K Pot",
"TempCharcoal"]
header_parallel = ["Time", "B", "Vx8", "Vy8", "Vr8", "Vth8", "Vx9",
"Vy9", "Vr9", "Vth9", "Vx13", "Vy13", "Vr13",
"Vth13", "TempRO CU", "TempRO YOKE", "TempCX CU",
"SampTemp", "Temp1K Pot", "TempCharcoal"]
self.c_list = [0, 6.8, 10, 18, 27, 39, 56, 82, 100, 150, 180, 270,
330] # Condensators in pF
self.r_list = [0, 10, 100, 200, 300, 392, 475, 562, 619, 750, 825, 909,
1e3] # Resistors in k Ohm
self.n = 1369288448319507.5 # Carrier Concentration calculated from perp. sweep
if isinstance(measurement_number, float) or isinstance(
measurement_number, int):
files = glob("data/Hloop/[mM]%s_*.dat" % measurement_number)
else:
files = glob(measurement_number)
files = kwargs.get('file_list', files)
if not (files):
raise NameError(
"No File for measurement Nr. %s found." % measurement_number)
for fname in files:
file_info = None
try:
if fname.find('Gradio') > 0:
reg = re.match(
".*[Mm]([0-9.]*)_([A-Za-z]*)_([0-9.-]*)deg_"
"([A-Za-z]*)_"
"([A-Za-z]*)_*([A-Za-z]*)_([0-9]*)_([0-9]*)_"
"I1-([0-9\-]*)_I2-([0-9\-]*)_G.*-([0-9\-]*)_"
"Vin-([0-9.]*)V_R11-([0-9]*.)O_R12-([0-9.]*.)O_"
"R13-([0-9.]*.)_R21-([0-9.]*.)O_"
"C11-([0-9]*)_C21-([0-9]*)_"
"T-([0-9]*)K_SR-([0-9.]*)-T-min",
fname)
if not reg:
raise NameError("%s not fitting Regex" % fname)
m_no, struct, deg, m_type, \
m_dir, m_type2, m_date, m_time, \
m_i1, m_i2, m_li1, \
m_vin, m_r11, m_r12, \
m_r13, m_r21, \
m_c11, m_c21, \
m_T, m_SR = reg.groups()
header = header_gradio
self.parallel = False
elif fname.find('Parallel') > 0:
reg = re.match(
".*[Mm]([0-9.]*)_([A-Za-z_]*)_([0-9.-]*)deg_([A-Za-z]*)_"
+ "([A-Za-z]*)_*([A-Za-z]*)_([0-9]*)_([0-9]*)_" \
+ "I-([0-9\-]*)_(G.*-[0-9\-]*)_" \
+ "Vin-([0-9.]*)V_R11-([0-9]*.)O" \
+ ".*_" \
+ "T-([0-9]*)K_SR-([0-9.]*)-T-min",
fname)
if not reg:
raise NameError("%s not fitting Regex" % fname)
m_no, struct, deg, m_type, \
m_dir, m_type2, m_date, m_time, \
m_i1, m_li1, \
m_vin, m_r11, \
m_T, m_SR = reg.groups()
m_i2 = m_r12 = m_r13 = m_r21 = m_c11 = m_c21 = '0'
header = header_parallel
self.parallel = True
else:
raise NameError("%s not fitting Regex" % fname)
except NameError:
file_info = self.get_info_from_name(fname)
self.parallel = file_info.get('type2') == 'Parallel'
header = header_gradio if file_info.get('type2') == 'Gradio' else header_parallel
header = kwargs.get('header', header)
kwargs.update(file_info)
if isinstance(file_info.get('deg'), str) and \
file_info.get('deg').find('neg') > -1:
file_info['deg'] = file_info.get('deg').replace('neg', '-')
deg = file_info.get('deg') if file_info.get('deg') else 0.01
m_dir = file_info.get('dir', 'down').lower()
if fname.find('b.dat') > 0:
m_dir = 'up'
### Loading File
if (m_dir.lower() == 'up'):
self.fname_up = fname
self.up = pd.read_csv(fname, sep='\t', names=header,
skiprows=3)
self.up.B = self.up.B * 1e3
else:
self.fname_down = fname
self.down = pd.read_csv(fname, sep='\t', names=header,
skiprows=3)
self.down.B = self.down.B * 1e3
if (kwargs.get('down_only')):
self.up = self.down
if (kwargs.get('up_only')):
self.down = self.up
self.measurement_number = measurement_number
self.factor = 1
# Mirror all Angles bigger than 90
if kwargs.get('force_mirror') or \
(kwargs.get('auto_mirror', True) and \
float(deg) >= 90):
self.set_factor(-1)
self.factor = 1
if file_info:
self.info = file_info
self.info.update({
'Type': "%s (%s)" % (file_info.get('type'),
file_info.get('type2')),
'Structure': file_info.get('struct'),
'Angle': file_info.get('deg'),
})
else:
m_datetime = "%s.%s.%s %s:%s" % (
m_date[6:], m_date[4:6], m_date[:4], m_time[:2], m_time[2:])
self.info = {
'Type': "%s (%s)" % (m_type, m_type2),
'Date': m_datetime,
'Structure': struct,
'Angle': deg,
'I1': m_i1,
'I2': m_i2,
'Vin': m_vin,
'R11': m_r11,
'R12': m_r12,
'R13': m_r13,
'R21': m_r21,
'C11': self.c_list[int(m_c11)],
'C21': self.c_list[int(m_c21)],
'T': m_T,
'SR': m_SR,
'Additional': ''}
# Update data from kwargs
for key, value in kwargs.items():
if key in self.info.keys():
self.info[key] = value
[docs] def set_factor(self, factor):
"""Multiplying the Voltage by a Factor
Args:
factor:
"""
self.up.Vx8 /= self.factor
self.down.Vx8 /= self.factor
if (self.parallel):
self.up.Vx9 /= self.factor
self.up.Vx13 /= self.factor
self.down.Vx9 /= self.factor
self.down.Vx13 /= self.factor
self.factor = factor
self.up.Vx8 *= self.factor
self.down.Vx8 *= self.factor
if (self.parallel):
self.up.Vx9 *= self.factor
self.up.Vx13 *= self.factor
self.down.Vx9 *= self.factor
self.down.Vx13 *= self.factor
def __repr__(self):
return self.get_default_title()
[docs] def calc_B(self, V):
"""
Args:
V:
"""
e = scipy.constants.physical_constants['electron volt'][0]
R = V / 2.5e-6
B = R * self.n * e
return B
[docs] def remove_zero(self, columns=None):
"""Removes the values 0.0 from up and down sweep where the lock-in
didn't return any values (read error will be saved as 0.0).
Args:
columns:
"""
# Set Columns to remove values from
if columns == None:
if self.parallel:
columns = ['Vx8', 'Vx9', 'Vx13']
else:
columns = ['Vx8']
# Remove zero values from columns (= measurement error)
for v in columns:
self.up = self.up[self.up[v] != 0.0]
self.down = self.down[self.down[v] != 0.0]
[docs] def fit(self, cond=pd.Series()):
"""
Fitting the Voltage Signal.
For Parallel measurement:
the empty cross Vx13 is subtracted from the signal Vx8/Vx9
For gradiometry:
A condition can be added to determine the amount of
datapoints to fit (default: B < B_min + 150mT).
.. code-block:: python
:caption: Example
cond = (self.up.B < self.up.B.min() + 150)
m.fit(cond)
Args:
cond:
"""
# Set Fitting Condition
if (cond.empty):
cond = (self.up.B < self.up.B.min() + 150)
### Fitting Graph
self.up_fitted = self.up.copy()
self.down_fitted = self.down.copy()
if (self.parallel):
self.up_fitted.Vx8 -= self.up_fitted.Vx13
self.up_fitted.Vx9 -= self.up_fitted.Vx13
self.down_fitted.Vx8 -= self.down_fitted.Vx13
self.down_fitted.Vx9 -= self.down_fitted.Vx13
else:
fit_area = self.up[cond].copy()
fit = scipy.stats.linregress(fit_area.B, fit_area.Vx8)
self.up_fitted['fit'] = (
self.up_fitted.B * fit.slope + fit.intercept)
self.down_fitted['fit'] = (
self.down_fitted.B * fit.slope + fit.intercept)
fit_area['fit'] = (fit_area.loc[:,
'B'].to_numpy() * fit.slope + fit.intercept)
self.up_fitted.Vx8 = self.up_fitted.Vx8 - self.up_fitted.fit
self.down_fitted.Vx8 = self.down_fitted.Vx8 - self.down_fitted.fit
[docs] def plot_hloop(self, ax, figtitle="", show_fitted=True,
show_rem=False, show_coer=False,
**kwargs):
"""Plotting the hysteresis loop.
:param ax: Matplotlib Axis to plot on.
:param figtitle: can be set to a manual figure title.
:param show_fitted: plot the fitted curve.
:param show_rem: show the remanent voltage.
:param show_coer: show the coercive field.
:param show_original: plots the RAW data.
:param show_linear_fit: plots the linear fitting line used for the fit
(only gradiometry).
"""
if kwargs.get('show_original'): # Drawing RAW Data
ax.plot(self.up.B, self.up.Vx8, label="up")
ax.plot(self.down.B, self.down.Vx8, label='down')
if (self.parallel):
ax.plot(self.up.B, self.up.Vx9, label="up (LI 9)")
ax.plot(self.down.B, self.down.Vx9, label='down (LI 9)')
ax.plot(self.up.B, self.up.Vx13, label="up (LI 13)")
ax.plot(self.down.B, self.down.Vx13, label='down (LI 13)')
if show_fitted: # Drawing fitted data
# Already fitted?
if not (hasattr(self, 'down_fitted')):
self.fit()
if (self.parallel):
ax.plot(self.up_fitted.B, self.up_fitted.Vx8,
label='Plusses: Up (fitted)')
ax.plot(self.down_fitted.B, self.down_fitted.Vx8,
label='Plusses: Down (fitted)')
ax.plot(self.up_fitted.B, self.up_fitted.Vx9,
label='Crosses: Up (fitted)')
ax.plot(self.down_fitted.B, self.down_fitted.Vx9,
label='Crosses: Down (fitted)')
else:
ax.plot(self.up_fitted.B, self.up_fitted.Vx8,
label='Up (fitted)')
ax.plot(self.down_fitted.B, self.down_fitted.Vx8,
label='Down (fitted)')
if kwargs.get('show_linear_fit'): # Drawing linear Fit
ax.plot(self.up_fitted.B, self.up_fitted.fit,
label='Linear Fit')
### Remanent Field ###
if show_rem:
rem1, rem2 = self.get_remanence()
ax.plot([0, 0],
[self.up_fitted.Vx8.min(), self.down_fitted.Vx8.max()],
'r-.', linewidth='.5')
ax.plot(rem1['B'], rem1['Vx8'], 'bo', markersize=12)
ax.plot(rem2['B'], rem2['Vx8'], 'bo', markersize=12)
ax.annotate("$V_{rem} = %.3f \\mu V$" % rem1['Vx8'],
xy=(rem1['B'], rem1['Vx8']),
xytext=(150, -100), textcoords='offset points',
arrowprops={'arrowstyle': '->', 'color': 'black'})
ax.annotate("$V_{rem} = %.3f \\mu V$" % rem2['Vx8'],
xy=(rem2['B'], rem2['Vx8']),
xytext=(-150, 100), textcoords='offset points',
arrowprops={'arrowstyle': '->', 'color': 'black'})
### Coercive Field
if show_coer:
mean, coer, coer2 = self.get_coercive_field()
ax.plot([self.up_fitted.B.min(), self.up_fitted.B.max()],
[mean, mean], 'r-.', linewidth='.5')
ax.plot(coer['B'], coer['Vx8'], 'go', markersize=12)
ax.plot(coer2['B'], coer2['Vx8'], 'go', markersize=12)
ax.annotate("$B_{coer} = %.4f T$" % coer.B,
xy=(coer['B'], coer['Vx8']),
xytext=(50, -30), textcoords='offset points',
arrowprops={'arrowstyle': '->', 'color': 'black'})
ax.annotate("$B_{coer} = %.4f T$" % coer2.B,
xy=(coer2['B'], coer2['Vx8']),
xytext=(-200, 20), textcoords='offset points',
arrowprops={'arrowstyle': '->', 'color': 'black'})
# Making a nice plot
ax.legend(loc='best')
bmin, bmax, vmin, vmax = self.get_minmax()
ax.set_xlim(bmin, bmax)
ax.set_xlabel("$B\\;[\\mathrm{mT}]$")
# Setting correct Y Label
yunit = '$V_x$'
if (self.factor == 1e3):
yunit += ' $[\\mathrm{mV}]$'
elif (self.factor == 1e6):
yunit += ' $[\\mathrm{\\mu V}]$'
ax.set_ylabel("%s" % yunit)
if (figtitle == ''):
figtitle = self.get_default_title()
ax.set_title(figtitle)
[docs] def calculate_strayfield(self):
"""Calculates the strayfield of the signal and stores it in up and down
sweeps.
"""
self.up['Bx8'] = self.calc_B(self.up.Vx8)
self.down['Bx8'] = self.calc_B(self.down.Vx8)
if (self.parallel):
self.up['Bx9'] = self.calc_B(self.up.Vx9)
self.down['Bx9'] = self.calc_B(self.down.Vx9)
self.up['Bx13'] = self.calc_B(self.up.Vx13)
self.down['Bx13'] = self.calc_B(self.down.Vx13)
[docs] def calculate_fitted_strayfield(self):
"""Calculates the strayfield of the fitted signal and stores it in up
and down sweeps.
"""
self.up_fitted['Bx8'] = self.calc_B(self.up_fitted.Vx8)
self.down_fitted['Bx8'] = self.calc_B(self.down_fitted.Vx8)
if (self.parallel):
self.up_fitted['Bx9'] = self.calc_B(self.up_fitted.Vx9)
self.down_fitted['Bx9'] = self.calc_B(self.down_fitted.Vx9)
self.up_fitted['Bx13'] = self.calc_B(self.up_fitted.Vx13)
self.down_fitted['Bx13'] = self.calc_B(self.down_fitted.Vx13)
[docs] def plot_strayfield(self, ax, figtitle="", **kwargs):
"""Plots the strayfield of the data. Strayfield is calculated using the
electron concentration at 0 degree measured before
(hardcoded in __init__).
Args:
ax (matplotlib.pyplot.axis): where should we plot.
figtitle (str, optional): Choose your own title above the figure.
**kwargs:
Returns:
None.:
"""
self.set_factor(kwargs.get('factor', 1e3))
self.calculate_strayfield()
if kwargs.get('show_original'): # Drawing RAW Data
ax.plot(self.up.B, self.up.Bx8, label="up")
ax.plot(self.down.B, self.down.Bx8, label='down')
if kwargs.get('show_fitted', True): # Drawing fitted data
# Already fitted?
if not (hasattr(self, 'down_fitted')):
self.fit()
self.calculate_fitted_strayfield()
if self.parallel:
if kwargs.get('show_plusses', True):
ax.plot(self.up_fitted.B, self.up_fitted.Bx8,
label='Plusses: Up (fitted)')
ax.plot(self.down_fitted.B, self.down_fitted.Bx8,
label='Plusses: Down (fitted)')
if kwargs.get('show_crosses', True):
ax.plot(self.up_fitted.B, self.up_fitted.Bx9,
label='Crosses: Up (fitted)')
ax.plot(self.down_fitted.B, self.down_fitted.Bx9,
label='Crosses: Down (fitted)')
else:
ax.plot(self.up_fitted.B, self.up_fitted.Bx8,
label='Up (fitted)')
ax.plot(self.down_fitted.B, self.down_fitted.Bx8,
label='Down (fitted)')
# Making a nice plot
if not (kwargs.get('nolegend')):
ax.legend(loc='best')
bmin, bmax, vmin, vmax = self.get_minmax()
ax.set_xlim(bmin, bmax)
ax.set_xlabel("$\\mu_0 H_{ext}$ $[\\mathrm{mT}]$")
# Setting correct Y Label
bhmin, bhmax = self.get_bhminmax()
ax.set_ylim(bhmin - .05, bhmax + .05)
ax.set_ylabel("$\\langle B_z \\rangle$ $[\\mathrm{mT}]$")
# Setting Title
if (figtitle == ''):
figtitle = self.get_default_title()
if (self.parallel):
figtitle = "M%s: $%s^\\circ$" % (self.measurement_number,
self.info['Angle'])
ax.set_title(figtitle)
[docs] def plot_downminusup(self, ax, figtitle=""):
"""Plotting the difference between the down and up sweep. ax: Matplotlib
Axis to plot on. figtitle: can be set to a manual figure title.
Args:
ax:
figtitle:
"""
B, vx = self.get_downminusup()
ax.plot(B, vx)
ax.set_xlabel("$B [\\mathrm{mT}]$")
ax.set_ylabel("$V_x [\\mathrm{\\mu V}]$")
if (figtitle == ''):
figtitle = self.get_default_title()
ax.set_title(figtitle)
[docs] def get_downminusup(self, n=1e5):
"""Returns the magnetic field and difference between down and up sweep.
.. code-block:: python
:caption: Example
B, Vx = meas.get_downminusup() plt.plot(B, Vx)
Args:
n:
"""
f_up = scipy.interpolate.interp1d(self.up.B, self.up.Vx8)
f_down = scipy.interpolate.interp1d(self.down.B, self.down.Vx8)
B = np.linspace(self.up.B.min(), self.up.B.max(), int(n))
downminusup = f_down(B) - f_up(B)
return B, downminusup
[docs] def get_downminusup_strayfield(self, n=1e5, fitted_data=False):
"""Returns the magnetic field and difference between down and up sweep.
.. code-block:: python
:caption: Example
B_ext, Bx = meas.get_downminusup_strayfield() plt.plot(B_ext, Bx)
Args:
n:
fitted_data:
"""
if fitted_data:
up = self.up_fitted
down = self.down_fitted
else:
up = self.up
down = self.down
f_up = scipy.interpolate.interp1d(up.B, up.Bx8)
f_down = scipy.interpolate.interp1d(down.B, down.Bx8)
B = np.linspace(up.B.min(), up.B.max(), int(n))
downminusup = f_down(B) - f_up(B)
return B, downminusup
[docs] def get_coercive_field(self):
"""Returns the mean and coercive fields calculated.
.. code-block:: python
:caption: Example
mean, coer1, coer2 = meas.get_coercive_field()
print('Coercive Field (up sweep): (%.3f, %.3f)' % (coer1, mean))
print('Coercive Field (down sweep): (%.3f, %.3f)' % (coer2, mean))
"""
mean = (self.up_fitted['Vx8'].min() + self.down_fitted[
'Vx8'].max()) / 2
coer = self.up_fitted.iloc[np.abs(self.up_fitted[ \
self.up_fitted.B.abs() < 350][
'Vx8'] - mean).argmin()]
coer2 = self.down_fitted.iloc[np.abs(self.down_fitted[ \
self.down_fitted.B.abs() < 350][
'Vx8'] - mean).argmin()]
return mean, coer, coer2
[docs] def plot_hysteresis(self, y="B", **kwargs):
fig, ax = plt.subplots(figsize=self.style.get('figsize'))
if y == 'B':
self.plot_strayfield(ax, **kwargs)
elif y == 'V':
self.plot_hloop(ax, **kwargs)
else:
self.plot_downminusup(ax, **kwargs)
[docs] def get_remanence(self):
"""Returns the remanent voltage calculated.
.. code-block:: python
:caption: Example
rem1, rem2 = meas.get_remanence()
print('Remanent Voltage (up sweep): (%d, %.3f)' % (0, rem1))
print('Remanent Voltage (down sweep): (%d, %.3f)' % (0, rem2))
"""
rem1 = self.up_fitted.iloc[self.up_fitted.B.abs().idxmin()]
rem2 = self.down_fitted.iloc[self.down_fitted.B.abs().idxmin()]
return rem1, rem2
[docs] def get_minmax(self):
"""Returns the minimum and maximum of the field and voltage.
.. code-block:: python
:caption: Example
bmin, bmax, vmin, vmax = meas.get_minmax()
"""
bmin = np.min([self.down.B.min(), self.up.B.min()])
bmax = np.max([self.down.B.max(), self.up.B.max()])
if (self.parallel):
max_8 = self.up.Vx8.max()
max_9 = self.up.Vx9.max()
max_13 = self.up.Vx13.max()
vmax = np.max([max_8, max_9, max_13])
min_8 = self.up.Vx8.min()
min_9 = self.up.Vx9.min()
min_13 = self.up.Vx13.min()
vmin = np.min([min_8, min_9, min_13])
else:
vmax = self.up.Vx8.max()
vmin = self.up.Vx8.min()
return bmin, bmax, vmin, vmax
[docs] def get_bhminmax(self):
"""Returns the minimum and maximum of the measured hall voltage
converted to strayfield.
.. code-block:: python
:caption: Example
bhmin, bhmax = meas.get_bhminmax()
"""
max_8 = np.max([self.up_fitted.Bx8.max(),
self.down_fitted.Bx8.max()])
min_8 = np.min([self.up_fitted.Bx8.min(),
self.down_fitted.Bx8.min()])
if (self.parallel):
max_9 = np.max([self.up_fitted.Bx9.max(),
self.down_fitted.Bx9.max()])
vmax = np.max([max_8, max_9])
min_9 = np.min([self.up_fitted.Bx9.min(),
self.down_fitted.Bx9.min()])
vmin = np.min([min_8, min_9])
else:
vmax = max_8
vmin = min_8
return vmin, vmax
[docs] def get_default_title(self):
if (self.parallel):
return "m%s: $%s^\\circ$" % (self.measurement_number,
self.info['Angle'])
return 'm%s: %s ($%s^\\circ$)' % (self.measurement_number,
self.info['Structure'],
self.info['Angle'])