import math
import copy
import numpy as np
import matplotlib.pyplot as plt
from typing import Union, Dict, Tuple, List
from cohesivm.analysis import Analysis, result_buffer
from cohesivm.database import Dataset, Dimensions
from cohesivm.plots import XYPlot
class FPPAnalysis(Analysis):
"""Implements the functions and plots to analyse the data of a four-point-probe measurement
(``FPPMeasurement``).
:param dataset: A tuple of (i) data arrays which are mapped to contact IDs and (ii) the corresponding metadata
of the dataset. Or, optionally, just (i).
:param interface_dimensions: The :class:`~cohesivm.database.Dimensions.Shape` of the interface. Required if the
``dataset`` contains no :class:`~cohesivm.database.Metadata`.
:param contact_position_dict: A dictionary of contact IDs and the corresponding coordinates on the sample.
Required if the ``dataset`` contains no :class:`~cohesivm.database.Metadata`.
:param pixel_dimension_dict: A dictionary of contact IDs and the corresponding
:class:`~cohesivm.database.Dimensions.Generic` shape of the pixels. Required if the ``dataset`` contains no
:class:`~cohesivm.database.Metadata`.
:param temperature: The temperature of the sample during the measurement in K. Required if the ``dataset``
contains no :class:`~cohesivm.database.Metadata`.
:param film_thickness: The thickness of the conductive film in mm. Required if the ``dataset`` contains no
:class:`~cohesivm.database.Metadata`.
"""
def __init__(self, dataset: Union[Dataset, Dict[str, np.ndarray]],
interface_dimensions: Dimensions.Shape = None,
contact_position_dict: Dict[str, Tuple[float, float]] = None,
pixel_dimension_dict: Dict[str, Dimensions.Generic] = None,
temperature: float = None,
film_thickness: float = None,
) -> None:
functions = {
'Temperature (K)': self.temperature,
'Film Thickness (mm)': self.film_thickness,
'Linear Fit Resistance (Ohm)': self.linear,
'Sheet Resistance (Ohm)': self.sheet,
'Film Resistivity (Ohm mm)': self.rho_film,
'Bulk Resistivity (Ohm mm)': self.rho_bulk,
'Edge Distance i0 (mm)': self.edge_dist_i0,
'Edge Distance i3 (mm)': self.edge_dist_i3,
'Edge Sheet Resistance (Ohm)': self.edge_sheet,
'Edge Film Resistivity (Ohm mm)': self.edge_rho_film,
'Edge Bulk Resistivity (Ohm mm)': self.edge_rho_bulk,
}
plots = {
'Measurement': self.measurement,
'Film Resistivity': self.resistance_plot
}
super().__init__(functions, plots, dataset, contact_position_dict)
if self.metadata is not None:
self._interface_dimensions = Dimensions.object_from_string(self.metadata.interface_dimensions)
self._contact_position_dict = self.metadata.contact_position_dict
self._pixel_dimension_dict = {
k: Dimensions.object_from_string(v) for k, v in self.metadata.pixel_dimension_dict.items()}
self._temperature = self.metadata.measurement_settings['temperature']
self._film_thickness = self.metadata.measurement_settings['film_thickness']
else:
self._interface_dimensions = interface_dimensions
self._contact_position_dict = contact_position_dict
self._pixel_dimension_dict = pixel_dimension_dict
self._temperature = temperature
self._film_thickness = film_thickness
self.il = 'Current (A)'
self.vl = 'Voltage (V)'
def temperature(self, contact_id: str = '') -> float:
"""Retrieves the sample temperature from the measurement settings.
:param contact_id: Does nothing.
:returns: The temperature of the sample during the measurement in K.
"""
return self._temperature
def film_thickness(self, contact_id: str = '') -> float:
"""Retrieves the sample film thickness from the measurement settings.
:param contact_id: Does nothing.
:returns: The thickness of the measured conductive film in mm.
"""
return self._film_thickness
@result_buffer
def probe_coordinates(self, contact_id: str) -> List[Tuple[float, float]]:
"""Calculates the absolute coordinates of the four point probe with respect to the interface.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: A list of coordinate tuples for the four contacts of the probe.
"""
d = self._pixel_dimension_dict[contact_id]
x_offset, y_offset = self._contact_position_dict[contact_id]
x_abs = [x_offset + x for x in d.x_coords]
y_abs = [y_offset + y for y in d.y_coords]
return list(zip(x_abs, y_abs))
@staticmethod
def line_distance(x1y1: Tuple[float, float], x2y2: Tuple[float, float], xpyp: Tuple[float, float]
) -> Tuple[float, float, float]:
"""Calculates the 2D distance between a line (given by two points) and another point.
:param x1y1: The coordinates of the first point on the line.
:param x2y2: The coordinates of the second point on the line.
:param xpyp: The coordinates of the point for which the distance should be calculated.
:returns: A tuple of the signed distance and the x- and y-component of the normal unit vector.
"""
x1, y1 = x1y1
x2, y2 = x2y2
xp, yp = xpyp
a = y2 - y1
b = - (x2 - x1)
c = -a * x1 - b * y1
m = math.sqrt(a * a + b * b)
a_prim, b_prim, c_prim = a/m, b/m, c/m
dist = a_prim * xp + b_prim * yp + c_prim
return dist, a_prim, b_prim
@result_buffer
def edge_distances(self, contact_id: str) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]:
"""Calculates the line distances of the closest edge to a four point probe.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: A tuple of line distance tuples (see :meth:`line_distance`).
"""
xy = self.probe_coordinates(contact_id)
if_d = self._interface_dimensions
best_dist = (0., 0., 0.), (0., 0., 0.)
# only works with a rectangular interface
if not isinstance(if_d, Dimensions.Rectangle):
return best_dist
edge_vectors = [(0., 0.), (if_d.width, 0.), (if_d.width, if_d.height), (0., if_d.height), (0., 0.)]
min_dist = float('inf')
for x1y1, x2y2 in zip(edge_vectors[:4], edge_vectors[1:]):
dist = self.line_distance(x1y1, x2y2, xy[0]), self.line_distance(x1y1, x2y2, xy[3])
dist_avg = (abs(dist[0][0]) + abs(dist[1][0])) / 2
if dist_avg < min_dist:
min_dist = dist_avg
best_dist = dist
return best_dist
@result_buffer
def mirrored_coordinates(self, contact_id: str) -> List[Tuple[float, float]]:
"""Calculates coordinates for the current source contacts of a four point probe mirrored along the closest
edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: A list of coordinate tuples for the two mirrored contacts of the probe.
"""
xy_m = self.probe_coordinates(contact_id)[0], self.probe_coordinates(contact_id)[3]
dist = self.edge_distances(contact_id)
return [(xy_m[i][0] - 2 * dist[i][1] * dist[i][0], xy_m[i][1] - 2 * dist[i][2] * dist[i][0])
for i in range(2)]
@staticmethod
def l_mn(xy: List[Tuple[float, float]], m: int, n: int) -> float:
return math.log((xy[m][0] - xy[n][0])**2 + (xy[m][1] - xy[n][1])**2)
@staticmethod
def s_mn(xy: List[Tuple[float, float]], m: int, n: int) -> float:
return math.sqrt((xy[m][0] - xy[n][0])**2 + (xy[m][1] - xy[n][1])**2)
@result_buffer
def l0(self, contact_id: str) -> float:
xy = self.probe_coordinates(contact_id)
return self.l_mn(xy, 1, 0) + self.l_mn(xy, 2, 3) - self.l_mn(xy, 1, 3) - self.l_mn(xy, 2, 0)
@result_buffer
def s0(self, contact_id: str) -> float:
xy = self.probe_coordinates(contact_id)
return -1/self.s_mn(xy, 1, 0) - 1/self.s_mn(xy, 2, 3) + 1/self.s_mn(xy, 1, 3) + 1/self.s_mn(xy, 2, 0)
@result_buffer
def lm(self, contact_id: str) -> float:
xy = self.probe_coordinates(contact_id) + self.mirrored_coordinates(contact_id)
return (self.l0(contact_id)
+ self.l_mn(xy, 1, 4) + self.l_mn(xy, 2, 5) - self.l_mn(xy, 1, 5) - self.l_mn(xy, 2, 4))
@result_buffer
def sm(self, contact_id: str) -> float:
xy = self.probe_coordinates(contact_id) + self.mirrored_coordinates(contact_id)
return (self.s0(contact_id) -
1/self.s_mn(xy, 1, 4) - 1/self.s_mn(xy, 2, 5) + 1/self.s_mn(xy, 1, 5) + 1/self.s_mn(xy, 2, 4))
@result_buffer
def linear(self, contact_id: str) -> float:
"""Performs a linear regression between the measured current and voltage values to obtain the slope, i.e.,
the fitted resistance after Ohm's Law.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The fraction between the measured voltage and sourced current fitted over all data points in Ohm.
"""
return float(np.polyfit(self.data[contact_id][self.il], self.data[contact_id][self.vl], deg=1)[0])
@result_buffer
def sheet(self, contact_id: str) -> float:
"""Calculates the sheet resistance for a probe far from the edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The sheet resistance in Ohm.
"""
return abs(4 * math.pi * self.linear(contact_id) * 1 / self.l0(contact_id))
@result_buffer
def rho_film(self, contact_id: str) -> float:
"""Calculates the resistivity of a film for a probe far from the edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The film resistivity in Ohm mm.
"""
t = self.film_thickness()
return abs(self.sheet(contact_id) * t) if t is not None else None
@result_buffer
def rho_bulk(self, contact_id: str) -> float:
"""Calculates the resistivity of a bulk material for a probe far from the edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The bulk resistivity in Ohm mm.
"""
return abs(2 * math.pi * self.linear(contact_id) * 1 / self.s0(contact_id))
@result_buffer
def edge_dist_i0(self, contact_id: str) -> float:
"""Retrieves the distance of the i0 contact of the four point probe to the closest edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The edge distance of the i0 contact in mm.
"""
return self.edge_distances(contact_id)[0][0]
@result_buffer
def edge_dist_i3(self, contact_id: str) -> float:
"""Retrieves the distance of the i3 contact of the four point probe to the closest edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The edge distance of the i3 contact in mm.
"""
return self.edge_distances(contact_id)[1][0]
@result_buffer
def edge_sheet(self, contact_id: str) -> float:
"""Calculates the sheet resistance for a probe close to an edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The edge sheet resistance in Ohm.
"""
return abs(4 * math.pi * self.linear(contact_id) * 1 / self.lm(contact_id))
@result_buffer
def edge_rho_film(self, contact_id: str) -> float:
"""Calculates the resistivity of a film for a probe close to an edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The edge film resistivity in Ohm mm.
"""
t = self.film_thickness()
return abs(self.edge_sheet(contact_id) * t) if t is not None else None
@result_buffer
def edge_rho_bulk(self, contact_id: str) -> float:
"""Calculates the resistivity of a bulk material for a probe close to an edge.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The edge bulk resistivity in Ohm mm.
"""
return abs(2 * math.pi * self.linear(contact_id) * 1 / self.sm(contact_id))
def measurement(self, contact_id: str) -> plt.Figure:
"""Creates a basic x-y plot of the data.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The figure object which may be displayed by the :class:`~cohesivm.gui.AnalysisGUI`.
"""
plot = XYPlot()
plot.make_plot()
data = copy.deepcopy(self.data[contact_id])
plot.update_plot(data)
return plot.figure
def resistance_plot(self, contact_id: str) -> plt.Figure:
"""Creates a plot of the resistance calculated after Ohm's Law. Should be a horizontal line for the ideal
case.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: A figure object which may be displayed by the :class:`~cohesivm.gui.AnalysisGUI`.
"""
plot = XYPlot(origin=False)
plot.make_plot()
data = copy.deepcopy(self.data[contact_id])
data[self.vl] = data[self.vl] / data[self.il]
data.dtype = copy.deepcopy(data.dtype)
data.dtype.names = (self.il, 'Resistance (Ohm)')
plot.update_plot(data)
plot.ax.axhline(y=self.linear(contact_id), color='r', ls='--')
return plot.figure