"""
Van der Pauw resistivity measurement.
| Four-point measurement bypass resistance of ohmic contacts.
| To find resistivity from sheet resistance, use
:mod:`physicslab.electricity.Resistivity.from_sheet_resistance` method.
| Pay special attention to enum :class:`Geometry`.
"""
import enum
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.optimize import newton as scipy_optimize_newton
from physicslab.electricity import Resistivity, Resistance
from physicslab.ui import plot_grid
from physicslab.utility import (_ColumnsBase, permutation_sign,
squarificate, get_name)
[docs]def process(data, thickness=None):
""" Bundle method.
Parameter :attr:`data` must include geometry voltage and current.
See :class:`Columns` for details and column names.
The optional parameter allows to calculate additional quantities:
`resistivity` and `conductivity`.
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 thickness: Sample dimension perpendicular to the plane marked
by the electrical contacts, defaults to None
:type thickness: float, optional
:return: Derived quantities listed in :meth:`Columns.process` or units
:rtype: pandas.Series
"""
if data is None:
from physicslab.experiment import UNITS
import physicslab.electricity as el
name = UNITS
sheet_resistance = el.Sheet_Resistance.UNIT
ratio_resistance = '1'
sheet_conductance = el.Sheet_Conductance.UNIT
resistivity = el.Resistivity.UNIT
conductivity = el.Conductivity.UNIT
else:
name = get_name(data)
measurement = Measurement(data)
(resistivity, conductivity) = [np.nan] * 2
Rh, Rv = measurement.analyze()
sheet_resistance, ratio_resistance = Solve.analyze(Rh, Rv)
sheet_conductance = 1 / sheet_resistance
if thickness is not None:
resistivity = Resistivity.from_sheet_resistance(sheet_resistance,
thickness)
conductivity = 1 / resistivity
return pd.Series(
data=(sheet_resistance, ratio_resistance, sheet_conductance,
resistivity, conductivity),
index=Columns.process(), name=name)
[docs]class Solve:
""" Van der Pauw formula and means to solve it. """
[docs] @staticmethod
def square(Rh, Rv):
""" Compute sheet resistance from the given resistances.
Accurate only for square sample: :math:`R_h = R_v`.
:param Rh: Horizontal resistance
:type Rh: float
:param Rv: Vertical resistance
:type Rv: float
:return: Sheet resistance
:rtype: float
"""
R = (Rh + Rv) / 2
van_der_pauw_constant = np.pi / np.log(2)
return R * van_der_pauw_constant
[docs] @classmethod
def universal(cls, Rh, Rv, Rs0):
""" Compute sheet resistance from the given resistances.
Universal formula. Computation flow for square-like samples is
as follows:
.. code:: python
Rs0 = van_der_pauw.Solve.square(Rh, Rv)
Rs = van_der_pauw.Solve.universal(Rh, Rv, Rs0)
:param Rh: Horizontal resistance
:type Rh: float
:param Rv: Vertical resistance
:type Rv: float
:param Rs0: Approximate value to start with.
:type Rs0: float
:return: Sheet resistance
:rtype: float
"""
return scipy_optimize_newton(
cls.implicit_formula, Rs0, args=(Rh, Rv), fprime=None)
[docs] @classmethod
def analyze(cls, Rh, Rv):
""" Solve :meth:`Solve.implicit_formula` to find sample's
sheet resistance. Also compute resistance symmetry ratio (always
greater than one). The ratio assess how squarish the sample is,
quality of ohmic contacts (small, symmetric) etc.
:param Rh: Horizontal resistance
:type Rh: float
:param Rv: Vertical resistance
:type Rv: float
:return: Sheet resistance and symmetry ratio
:rtype: tuple(float, float)
"""
Rs0 = cls.square(Rh, Rv)
sheet_resistance = cls.universal(Rh, Rv, Rs0)
ratio_resistance = Rh / Rv
if ratio_resistance < 1:
ratio_resistance = 1 / ratio_resistance
return sheet_resistance, ratio_resistance
[docs]class Columns(_ColumnsBase):
""" Bases: :class:`physicslab.utility._ColumnsBase`
Column names.
"""
GEOMETRY = 'Geometry'
VOLTAGE = 'Voltage'
CURRENT = 'Current'
RESISTANCE = 'Hall_resistance'
SHEET_RESISTANCE = 'sheet_resistance'
RATIO_RESISTANCE = 'ratio_resistance'
SHEET_CONDUCTANCE = 'sheet_conductance'
RESISTIVITY = 'resistivity'
CONDUCTIVITY = 'conductivity'
[docs] @classmethod
def mandatory(cls):
""" Get the current mandatory column names.
:rtype: set(str)
"""
return {cls.GEOMETRY, cls.VOLTAGE, cls.CURRENT}
[docs] @classmethod
def process(cls):
""" Get the current values of the :func:`process` output column names.
:rtype: lits(str)
"""
return [cls.SHEET_RESISTANCE, cls.RATIO_RESISTANCE,
cls.SHEET_CONDUCTANCE, cls.RESISTIVITY, cls.CONDUCTIVITY]
[docs]class Measurement:
""" Van der Pauw resistances measurements.
:param pandas.DataFrame data: Voltage-current pairs with respective
geometries.
: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
[docs] def analyze(self):
""" Classify geometries into either :attr:`Geometry.Horizontal`
or :attr:`Geometry.Vertical`. Then average respective hall resistances.
Additionally save Hall resistances to :data:`data`.
:return: Horizontal and vertical sheet resistances
:rtype: tuple(float, float)
"""
self.data.loc[:, Columns.RESISTANCE] = Resistance.from_ohms_law(
self.data[Columns.VOLTAGE], self.data[Columns.CURRENT])
geometries = self.data[Columns.GEOMETRY].apply(Geometry.classify)
mask = geometries.apply(Geometry.is_horizontal)
Rh = self.data.loc[mask, Columns.RESISTANCE].mean()
Rv = self.data.loc[~mask, Columns.RESISTANCE].mean()
return Rh, Rv
[docs]class Geometry(enum.Enum):
""" Resistance measurement configuration :class:`~enum.Enum`.
Legend: ``Rijkl`` = :math:`R_{ij,kl} = V_{kl}/I_{ij}`. The contacts are
numbered from 1 to 4 in a counter-clockwise order beginning at the
top-left contact. See `Van der Pauw method
<https://en.wikipedia.org/wiki/Van_der_Pauw_method#Reversed_polarity_measurements>`_
at Wikipedia.
There are two additional group values: ``Vertical`` and ``Horizontal``.
"""
R1234 = '1234'
R3412 = '3412'
R2143 = '2143'
R4321 = '4321'
R2341 = '2341'
R4123 = '4123'
R3214 = '3214'
R1432 = '1432'
Vertical = '12'
Horizontal = '21'
[docs] @classmethod
def parse(cls, text):
""" Parse string to :class:`Geometry`.
:param text: Any valid permutation of digits 1-4, e.g. ``4123``
or ``vertical`` or ``horizontal``
:type text: str
:raises ValueError: If there is no corresponding :class:`Geometry`
:return: Enum corresponding to :attr:`text`
:rtype: Geometry
"""
if text == 'vertical':
return cls.Vertical
elif text == 'horizontal':
return cls.Horizontal
return cls(text)
[docs] def reverse_polarity(self):
""" Reverse polarity of voltage and current.
:return: Reversed geometry
:rtype: Geometry
"""
if len(self.value) == 2:
return self
# len(self.value) == 4
new_value = ''.join( # [self.value[pairs][reverse order] for ...]
[self.value[i:i+2][::-1] for i in range(0, len(self.value), 2)]
)
return Geometry(new_value)
[docs] def rotate(self, number=1, counterclockwise=True):
""" Shift measuring pins counterclockwise.
:param number: Number of pins to jump, defaults to 1
:type number: int, optional
:param counterclockwise: Direction of rotation, defaults to True
:type counterclockwise: bool, optional
:return: Rotated geometry
:rtype: Geometry
"""
number = number % len(self.value)
if not counterclockwise:
number *= -1
new_value = self.value[-number:] + self.value[:-number]
return Geometry(new_value)
[docs] def is_horizontal(self):
""" Find whether the geometry describes horizontal configuration.
:return: Is horizontal?
:rtype: bool
"""
return permutation_sign(self.value) == -1
[docs] def is_vertical(self):
""" Find whether the geometry describes vertical configuration.
:return: Is vertical?
:rtype: bool
"""
return permutation_sign(self.value) == 1
[docs] def classify(self):
""" Sort the Geometry to either vertical or horizontal group.
:return: One of the two group configurations
:rtype: Geometry
"""
if self.is_horizontal():
return self.Horizontal
else:
return self.Vertical
[docs]def plot(data_list, results):
""" Plot individual measurements and results with quality coefficients.
For plotting details, see:
https://matplotlib.org/stable/tutorials/intermediate/gridspec.html#a-complex-nested-gridspec-using-subplotspec
:param data_list:
:type data_list: list[pandas.DataFrame]
:param results: Analysis data from :func:`physicslab.experiment.process`
:type results: pandas.DataFrame
:return: Same objects as from :meth:`matplotlib.pyplot.subplots`.
Axes are: grid axis array, right plot left axis, right plot right axis
:rtype: tuple[~matplotlib.figure.Figure,
numpy.ndarray[~matplotlib.axes.Axes]]
"""
df = pd.DataFrame(data=squarificate(data_list))
title = 'Van der Pauw'
# Plotting initialization.
size = mpl.rcParams['figure.figsize']
size = (size[0] * 2, size[1]) # Double width.
fig = plt.figure(num=title, figsize=size)
outer_grid = fig.add_gridspec(1, 2)
# Grid plot.
inner_grid = outer_grid[0].subgridspec(*df.shape)
axs_grid = inner_grid.subplots()
def plot_value(ax, value: pd.DataFrame):
for ori in value['ori'].drop_duplicates():
value_ori = value.loc[value['ori'] == ori, :]
ax.plot(value_ori[Columns.VOLTAGE],
value_ori[Columns.CURRENT], 'o-')
plot_grid(df, plot_value, fig_axs=(fig, axs_grid), title=title,
ylabel='Current / A', row_labels=False, column_labels=False)
fig.text(0.30, 0.04, 'Voltage / V', ha='center')
# Single plot.
ax_plot = fig.add_subplot(outer_grid[1])
ax_plot.plot(results[Columns.SHEET_CONDUCTANCE], 'ko:')
ax_plot.set_xlabel('Measurement number')
ax_plot.set_ylabel('Sheet conductance / $(k\\Omega)^{-1}$')
ax_plot.set_ylim(bottom=0)
color = 'red'
ax_plot_2 = ax_plot.twinx()
ax_plot_2.plot(results[Columns.RATIO_RESISTANCE], 'o:', c=color)
ax_plot_2.set_ylabel('$R_{12} / R_{23}$ {>1}')
ax_plot_2.spines['right'].set_color(color)
ax_plot_2.yaxis.label.set_color(color)
ax_plot_2.tick_params(axis='y', colors=color)
ax_plot_2.set_ylim(bottom=0)
return fig, np.array((axs_grid, ax_plot, ax_plot_2), dtype=object)