Source code for ana.hloop

# 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'])