from __future__ import annotations
import functools
import copy
import matplotlib.pyplot as plt
import numpy as np
from typing import Dict, Tuple, Union, Callable
from cohesivm.analysis import Analysis, result_buffer
from cohesivm.database import Dataset, Dimensions
from cohesivm.plots import XYPlot
[docs]
def handle_hysteresis(func: Callable) -> Callable:
"""Decorator for functions which directly evaluate the measurement data and need to separate it into two individual
curves (``hysteresis`` of the :attr:`~cohesivm.measurements.iv.CurrentVoltageCharacteristic` is set
``True``). The function result will then be a tuple of two floats.
:param func: The function to be decorated.
:returns: The decorated function.
"""
@functools.wraps(func)
def wrapper(self, iv_array, *args, **kwargs):
if self.hysteresis:
midpoint = len(iv_array) // 2
args0 = [arg[0] for arg in args]
args1 = [arg[1] for arg in args]
return np.array([
func(self, iv_array[:midpoint], *args0, **kwargs),
func(self, iv_array[midpoint:], *args1, **kwargs)
])
return func(self, iv_array, *args, **kwargs)
return wrapper
[docs]
def current_density(func: Callable) -> Callable:
"""Decorator for functions which should return the result normalized by the
:attr:`~cohesivm.analysis.iv.CurrentVoltageCharacteristic.areas`.
:param func: The function to be decorated.
:returns: The decorated function.
"""
@functools.wraps(func)
def wrapper(self, contact_id):
if self.hysteresis:
return (
func(self, contact_id)[0] / self.areas[contact_id],
func(self, contact_id)[1] / self.areas[contact_id]
)
return func(self, contact_id) / self.areas[contact_id]
return wrapper
[docs]
class CurrentVoltageCharacteristic(Analysis):
"""Implements the functions and plots to analyse the data of a current-voltage-characteristic measurement
(:class:`~cohesivm.measurements.iv.CurrentVoltageCharacteristic`).
: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 contact_position_dict: A dictionary of contact IDs and the corresponding positions/coordinates on the sample.
Required if the ``dataset`` contains no :class:`~cohesivm.database.Metadata`.
:param areas: A mapping of the contact IDs with the pixel area. Required if the ``dataset`` contains no
:class:`~cohesivm.database.Metadata`.
:param hysteresis: Flags if the voltage range of the measurement was swept a second time in reverse order. Required
if the ``dataset`` contains no :class:`~cohesivm.database.Metadata`.
:param illuminated: Flags if the sample was illuminated during measurement. Required if the ``dataset`` contains no
:class:`~cohesivm.database.Metadata`.
:param power_in: The power of the input radiation source in W/mm^2. Required if the ``dataset`` contains no
:class:`~cohesivm.database.Metadata`.
"""
def __init__(self, dataset: Union[Dataset, Dict[str, np.ndarray]],
contact_position_dict: Dict[str, Tuple[float, float]] = None,
areas: Dict[str, float] = None, hysteresis: bool = None,
illuminated: bool = None, power_in: float = None
) -> None:
functions = {
'Open Circuit Voltage (V)': self.voc,
'Short Circuit Current (A)': self.isc,
'Short Circuit Current (mA)': self.isc_ma,
'Short Circuit Current Density (A/mm^2)': self.jsc,
'Short Circuit Current Density (mA/cm^2)': self.jsc_ma,
'MPP Voltage (V)': self.mpp_v,
'MPP Current (A)': self.mpp_i,
'MPP Current (mA)': self.mpp_i_ma,
'MPP Current Density (A/mm^2)': self.mpp_j,
'MPP Current Density (mA/cm^2)': self.mpp_j_ma,
'Fill Factor': self.ff,
'Efficiency': self.eff,
'Series Resistance (Ohm)': self.rs,
'Shunt Resistance (Ohm)': self.rsh
}
plots = {
'Measurement': self.measurement,
'Semi-Log': self.semilog
}
super().__init__(functions, plots, dataset, contact_position_dict)
if self.metadata is not None:
self._areas = {contact: Dimensions.object_from_string(dimension).area() for contact, dimension
in self.metadata.pixel_dimension_dict.items()} if areas is None else areas
ms = self.metadata.measurement_settings
self._hysteresis = ms['hysteresis'] if hysteresis is None else hysteresis
self._illuminated = ms['illuminated'] if illuminated is None else illuminated
self._power_in = ms['power_in'] if power_in is None else power_in
else:
self._areas = {contact_id: 1. for contact_id in self.data.keys()} if areas is None else areas
self._hysteresis = False if hysteresis is None else hysteresis
self._illuminated = True if illuminated is None else illuminated
self._power_in = 1. if power_in is None else power_in
self.vl = 'Voltage (V)'
self.il = 'Current (A)'
@property
def areas(self) -> Dict[str, float]:
"""A mapping of the contact IDs with the pixel area."""
return self._areas
@property
def hysteresis(self) -> bool:
"""Flags if the voltage range of the measurement was swept a second time in reverse order."""
return self._hysteresis
@property
def illuminated(self) -> bool:
"""Flags if the sample was illuminated during measurement."""
return self._illuminated
@property
def power_in(self) -> float:
"""The power of the input radiation source in W/mm^2."""
return self._illuminated
@handle_hysteresis
def _find_intercept(self, iv_array: np.ndarray, transpose: bool) -> Union[float, Tuple[float, float]]:
"""Finds the y-intercept of the provided data, i.e., the y-value at x=0.
:param iv_array: The data curve.
:param transpose: Flags if the x- and y-axis should be swapped.
:returns: The y-intercept(s).
"""
x = iv_array[self.vl]
y = iv_array[self.il]
if transpose:
x, y = y, x
(x_low, x_high), (y_low, y_high) = [[v[x <= 0], v[x >= 0]] for v in [x, y]]
lower_bound, upper_bound = x_low.argmax(), x_high.argmin()
return np.interp(0, [x_low[lower_bound], x_high[upper_bound]], [y_low[lower_bound], y_high[upper_bound]])
[docs]
@result_buffer
def voc(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the x-intercept of the data curve.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Open Circuit Voltage in V.
"""
return CurrentVoltageCharacteristic._find_intercept(self, self.data[contact_id], transpose=True)
[docs]
@result_buffer
def isc(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the y-intercept of the data curve.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Short Circuit Current in A.
"""
return CurrentVoltageCharacteristic._find_intercept(self, self.data[contact_id], transpose=False)
[docs]
def isc_ma(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the y-intercept of the data curve and multiplies by 1000.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Short Circuit Current in mA.
"""
return self.isc(contact_id) * 1000
[docs]
@result_buffer
@current_density
def jsc(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the y-intercept of the data curve and normalizes by the pixel area.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Short Circuit Current Density in A/mm^2.
"""
return CurrentVoltageCharacteristic.isc(self, contact_id)
[docs]
def jsc_ma(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the y-intercept of the data curve, normalizes by the pixel area and multiplies by 100000.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Short Circuit Current Density in mA/cm^2.
"""
return self.jsc(contact_id) * 100000
@handle_hysteresis
def _mpp_v(self, iv_array: np.ndarray) -> Union[float, Tuple[float, float]]:
voltage = iv_array[self.vl]
current = iv_array[self.il]
valid_range = (voltage >= 0) & (current <= 0)
power = voltage[valid_range] * current[valid_range]
return voltage[valid_range][power.argmin()]
[docs]
@result_buffer
def mpp_v(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the voltage where the product of the voltage and the current is maximal.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The MPP Voltage in V.
"""
return CurrentVoltageCharacteristic._mpp_v(self, self.data[contact_id])
@handle_hysteresis
def _mpp_i(self, iv_array: np.ndarray, mpp: float) -> Union[float, Tuple[float, float]]:
voltage = iv_array[self.vl]
current = iv_array[self.il]
return current[voltage == mpp][0]
[docs]
@result_buffer
def mpp_i(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the current where the product of the voltage and the current is maximal.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The MPP Current in A.
"""
return CurrentVoltageCharacteristic._mpp_i(self, self.data[contact_id], self.mpp_v(contact_id))
[docs]
def mpp_i_ma(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the current where the product of the voltage and the current is maximal, multiplied by 1000.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The MPP Current in mA.
"""
return self.mpp_i(contact_id) * 1000
[docs]
@result_buffer
@current_density
def mpp_j(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the current where the product of the voltage and the current is maximal, normalized by the pixel area.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The MPP Current Density in A/mm^2.
"""
return CurrentVoltageCharacteristic.mpp_i(self, contact_id)
[docs]
def mpp_j_ma(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the current where the product of the voltage and the current is maximal, normalizes by the pixel area,
and multiplies by 100000.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The MPP Current Density in mA/cm^2.
"""
return self.mpp_j(contact_id) * 100000
@handle_hysteresis
def _ff(self, iv_array: np.ndarray, mpp: float, mpp_i: float, voc: float, isc: float
) -> Union[float, Tuple[float, float]]:
return (mpp * mpp_i) / (voc * isc)
[docs]
@result_buffer
def ff(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the ratio between the area which is span by the MPP voltage and current and the area which is span by
the Voc and Isc.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Fill Factor as a unit-less fraction.
"""
return CurrentVoltageCharacteristic._ff(self, self.data[contact_id], self.mpp_i(contact_id),
self.mpp_v(contact_id), self.voc(contact_id), self.isc(contact_id))
[docs]
@result_buffer
def eff(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the ratio between the product of Voc, Jsc and FF and the power of the input radiation source.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Efficiency as a unit-less fraction.
"""
return abs(self.voc(contact_id) * self.jsc(contact_id) * self.ff(contact_id) / self.power_in)
@handle_hysteresis
def _find_slope_of_intercept(self, iv_array: np.ndarray, transpose: bool) -> Union[float, Tuple[float, float]]:
if not transpose:
x = iv_array[self.vl].round(6)
else:
x = iv_array[self.il].round(6)
regression_points = np.hstack([iv_array[x < 0][-1], iv_array[x == 0], iv_array[x > 0][0]])
return 1 / np.polyfit(regression_points[self.vl], regression_points[self.il], deg=1)[0]
[docs]
@result_buffer
def rs(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the slope of the curve at the x-intercept.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Series Resistance in Ohm.
"""
return CurrentVoltageCharacteristic._find_slope_of_intercept(self, self.data[contact_id], transpose=True)
[docs]
@result_buffer
def rsh(self, contact_id: str) -> Union[float, Tuple[float, float]]:
"""Finds the slope of the curve at the y-intercept.
:param contact_id: The ID of the contact from the :class:`~cohesivm.interfaces.Interface`.
:returns: The Shunt (Parallel) Resistance in Ohm.
"""
return CurrentVoltageCharacteristic._find_slope_of_intercept(self, self.data[contact_id], transpose=False)
[docs]
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
[docs]
def semilog(self, contact_id: str) -> plt.Figure:
"""Creates a semilog plot of the data curve where the scale of the y-axis is logarithmic.
: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()
plot.make_plot()
data = copy.deepcopy(self.data[contact_id])
data[self.il] = np.abs(data[self.il])
plot.update_plot(data)
plot.ax.set_yscale('log')
return plot.figure