import copy
import inspect
import pathlib
import numpy as np
import scipy.signal as spsig
from .fit import IndentationFitter, FitProperties
from .preproc import IndentationPreprocessor
from .rate import get_rater
[docs]class Indentation(object):
def __init__(self, idnt_data):
"""Force-indentation
Parameters
----------
idnt_data: nanite.read.IndentationData
Object holding the experimental data
"""
self.metadata = idnt_data.metadata
self.path = idnt_data.path
self.enum = idnt_data.enum
#: All data in a Pandas DataFrame
self.data = idnt_data.data
#: Default preprocessing steps steps,
#: see :func:`Indentation.apply_preprocessing`.
self.preprocessing = []
#: Fitting results, see :func:`Indentation.fit_model`)
self.fit_properties = FitProperties()
# Curve rating (see `self.rate_quality`)
self._rating = None
# Store initial parameters for reset (see `self.reset`)
frame = inspect.currentframe()
args, _, _, values = inspect.getargvalues(frame)
self._init_kwargs = {}
args.remove("self")
for arg in args:
self._init_kwargs[arg] = copy.deepcopy(values[arg])
def __contains__(self, key):
return self.data.__contains__(key)
def __getitem__(self, key):
return self.data.__getitem__(key)
def __setitem__(self, key, value):
return self.data.__setitem__(key, value)
def __repr__(self):
return "Indentation {: 6d} in '{}'".format(
self.enum,
self.path
)
[docs] def apply_preprocessing(self, preprocessing=None):
"""Perform curve preprocessing steps
Parameters
----------
preprocessing: list
A list of preprocessing method names that are
stored in the `IndentationPreprocessor` class.
If set to `None`, `self.preprocessing` will be
used.
"""
if preprocessing is None:
preprocessing = self.preprocessing
if "preprocessing" in self.fit_properties:
preproc_past = self.fit_properties["preprocessing"]
else:
preproc_past = []
if preproc_past != preprocessing:
# Remember initial fit parameters for user convenience
fp = self.fit_properties
fp["preprocessing"] = preprocessing
# Reset all data
fp.reset() # TODO: this is redundant (check FitProperties)
self.reset()
# Apply preprocessing
IndentationPreprocessor.apply(self, preprocessing)
# Check availability of axes
for ax in ["x_axis", "y_axis"]:
# make sure the fitting axes are defined
if ax in fp and not fp[ax] in self.data:
fp.pop(ax)
# Set new fit properties
self.fit_properties = fp
[docs] def compute_emodulus_mindelta(self, callback=None):
"""Compute elastic modulus vs. mindelta
Parameters
----------
callback: callable
A method that is called with the `emoduli` and
`indentations` as the computation proceeds every
five steps.
Returns
-------
emoduli, indentations: 1d ndarrays
The fitted elastic moduli and at the corresponding
maximal indentation depths.
Notes
-----
The information about emodulus and mindelta is also stored in
`self.fit_properties` with the keys "optimal_fit_E_array" and
"optimal_fit_delta_array", if `self.fit_model` is called with
the argument `search_optimal_fit` set to `True`.
"""
if "optimal_fit_E_array" in self.fit_properties:
emoduli = self.fit_properties["optimal_fit_E_array"]
indentations = self.fit_properties["optimal_fit_delta_array"]
else:
fitter = IndentationFitter(self)
emoduli, indentations = fitter.compute_emodulus_vs_mindelta(
callback=callback
)
self.fit_properties["optimal_fit_E_array"] = emoduli
self.fit_properties["optimal_fit_delta_array"] = indentations
return emoduli, indentations
[docs] def estimate_optimal_mindelta(self):
"""Estimate the optimal indentation depth
This is a convenience function that wraps around
`compute_emodulus_mindelta` and
`IndentationFitter.compute_opt_mindelta`.
"""
emoduli, indentations = self.compute_emodulus_mindelta()
dopt = IndentationFitter.compute_opt_mindelta(
emoduli=emoduli,
indentations=indentations
)
return dopt
[docs] def export(self, path):
"""Saves the current data as tab separated values"""
path = pathlib.Path(path)
with path.open("w") as fd:
self.data.to_csv(path_or_buf=fd,
sep="\t",
encoding="utf-8",
na_rep="nan",
index=False)
[docs] def fit_model(self, **kwargs):
"""Fit the approach-retract data to a model function
Parameters
----------
model_key: str
A key referring to a model in
`nanite.model.models_available`
params_initial: instance of lmfit.Parameters or dict
Parameters for fitting. If not given,
default parameters are used.
range_x: tuple of 2
The range for fitting, see `range_type` below.
range_type: str
One of:
- absolute:
Set the absolute fitting range in values
given by the `x_axis`.
- relative cp:
In some cases it is desired to be able to
fit a model only up until a certain indentation
depth (tip position) measured from the contact
point. Since the contact point is a fit parameter
as well, this requires a two-pass fitting.
preprocessing: list of str
Preprocessing
segment: str
One of "approach" or "retract".
weight_cp: float
Weight the contact point region which shows artifacts
that are difficult to model with e.g. Hertz.
optimal_fit_edelta: bool
Search for the optimal fit by varying the maximal
indentation depth and determining a plateau in the
resulting Young's modulus (fitting parameter "E").
"""
# self.fit_properties is an instance of FitProperties that
# stores previous fit kwargs. If the given kwargs are
# different than in the previous fit, the following two
# lines will reset the "hash" in the fit properties, triggering
# a new fit.
for arg in kwargs:
self.fit_properties[arg] = kwargs[arg]
if "hash" in self.fit_properties:
# There is nothing to do, because the initial fit
# properties are the same.
pass
else:
fitter = IndentationFitter(self, **kwargs)
# Perform fitting
# Note: if `fitter.fp["success"]` is `False`, then
# the `fit_residuals` and `fit_curve` are `nan`.
fitter.fit()
self["fit"] = fitter.fit_curve
self["fit residuals"] = fitter.fit_residuals
self["fit range"] = fitter.fit_range
self.fit_properties = fitter.fp
[docs] def get_initial_fit_parameters(self):
if ("params_initial" in self.fit_properties and
self.fit_properties["params_initial"] is not None):
parms = self.fit_properties["params_initial"]
else:
parms = IndentationFitter(self).fp["params_initial"]
return parms
[docs] def rate_quality(self, regressor="Extra Trees", training_set="zef18",
names=None, lda=None):
"""Compute the quality of the obtained curve
Uses heuristic approaches to rate a curve.
Parameters
----------
regressor: str
The regressor name used for rating.
training_set: str
A label for a training set shipped with nanite or a
path to a training set.
Returns
-------
rating: float
A value between 0 and 10 where 0 is the lowest rating.
If no fit has been performed, a rating of -1 is returned.
Notes
-----
The rating is cached based on the fitting hash
(see `IndentationFitter._hash`).
"""
if (self.fit_properties and "hash" in self.fit_properties):
curhash = self.fit_properties["hash"]
else:
curhash = "none"
if regressor.lower() == "none":
rt = -1
elif (self._rating is None or
self._rating[0] != curhash or
self._rating[1] != regressor or
self._rating[2] != training_set or
self._rating[3] != names or
self._rating[4] != lda):
# Perform rating
rater = get_rater(regressor=regressor,
training_set=training_set,
names=names,
lda=lda)
rt = rater.rate(datasets=self)[0]
self._rating = (curhash, regressor, training_set, names, lda, rt)
else:
# Use cached rating
rt = self._rating[-1]
return rt
[docs] def reset(self):
"""Resets all data operations"""
self.__init__(**self._init_kwargs)