Developer guide¶
How to contribute¶
Contributions via pull requests are very welcome. Just fork the “master” branch, make your changes, and create a pull request back to “master” with a descriptive title and an explanation of what you have done. If you decide to contribute code, please
Updating the documentation¶
The documentation is stored in the docs
directory of the repository
and is built using sphinx.
To build the documentation, first install the build requirements by running
this in the docs
directory:
pip install -r requirements.txt``
You can now build the documentation with
sphinx-build . _build
Open the file _build/index.html
in your web browser to view the
result.
Writing model functions¶
You are here because you would like to write a new model function for nanite. Note that all model functions implemented in nanite are consequently available in PyJibe as well.
Getting started¶
First, create a Python file model_unique_name.py
which will be the home of your
new model (make sure the name starts with model_
). Place the file in the
following location: nanite/model/model_unique_name.py
. You file should at least
contain the following:
import lmfit
import numpy as np
def get_parameter_defaults():
"""Return the default model parameters"""
# The order of the parameters must match the order
# of ´parameter_names´ and ´parameter_keys´.
params = lmfit.Parameters()
params.add("E", value=3e3, min=0)
params.add("contact_point", value=0)
params.add("baseline", value=0)
return params
def your_model_name(delta, E, contact_point=0, baseline=0):
r"""A brief model description
A more elaborate model description with a formula.
.. math::
F = \frac{4}{3}
E
\delta^{3/2}
Parameters
----------
delta: 1d ndarray
Indentation [m]
E: float
Young's modulus [N/m²]
contact_point: float
Indentation offset [m]
baseline: float
Force offset [N]
Returns
-------
F: float
Force [N]
Notes
-----
Here you can add more information about the model.
References
----------
Please give proper references for your model (e.g. publications or
arXiv manuscripts. You can do so by editing the "docs/nanite.bib"
file and cite it like so:
Sneddon (1965) :cite:`Sneddon1965`
"""
# this is a convention to avoid computing the root of negative values
root = contact_point - delta
pos = root > 0
# this is the model output
out = np.zeros_like(delta)
out[pos] = 4/3 * E * root[pos]**(3/2)
# add the baseline
return out + baseline
model_doc = your_model_name.__doc__
model_func = your_model_name
model_key = "unique_model_key"
model_name = "short model name"
parameter_keys = ["E", "contact_point", "baseline"]
parameter_names = ["Young's Modulus", "Contact Point", "Force Baseline"]
parameter_units = ["Pa", "m", "N"]
valid_axes_x = ["tip position"]
valid_axes_y = ["force"]
Once you have created this file, you have to register it in nanite by adding the line
from . import model_unique_name # noqa: F401
at the top in the file nanite/model/__init__.py
. That’s it!
A few things should be noted:
When designing your model parameters, always use SI units.
Always include a model formula. You can test whether it renders correctly by building the documentation (see above) and checking whether your model shows up properly in the code reference.
Fitting parameters should not contain spaces. Only use characters that are allowed in Python variable names.
Since fitting is based on lmfit, you may define mathematical constraints in
get_parameter_defaults
. However, if possible, try to solve your particular problem with ancillaries (see below), a concept that is easier to understand.
Now it is time for a quick sanity check:
from nanite import model
assert "unique_model_key" in model.models_available
Ancillary parameters¶
For more elaborate models, you might need additional parameters from the
nanite.indent.Indentation
instance. This is where ancillary
parameters come into play.
You can define an arbitrary number of ancillary parameters in your
model_unique_name.py
file:
def compute_ancillaries(idnt):
"""Compute ancillaries for my model
Parameters
----------
idnt: nanite.indent.Indentation
Indentation dataset from which to extract the ancillary
parameters.
Returns
-------
example: dict
Dictionary with ancillary parameters. In this example:
- "force_range": total force range covered by approach and retract
"""
# You have access to the initial fit parameters (including a
# good contact point estimate) with this line:
parms = idnt.get_initial_fit_parameters(model_key=model_key,
model_ancillaries=False)
# You can access individual columns...
force = idnt.data["force"]
segment = idnt.data["segment"] # `False` for approach; `True` for retract
tip_position = idnt.data["tip position"]
# ...and segments
force_approach = force[~segment] # equivalent to force[segment == False]
force_retract = force[segment]
# Initialize ancillary dictionary.
anc_dict = dict()
# This is the exemplary force parameter
anc_dict["force_range"] = np.ptp(force)
return anc_dict
# And below the other `parameter_keys` etc.:
parameter_anc_keys = ["force_range"]
parameter_anc_names = ["Overall peak-to-peak force"]
parameter_anc_units = ["N"]
You should know:
If an ancillary parameter key matches that of a fitting parameter (defined in
get_parameter_defaults
above), then the ancillary parameter can be used as an initial value for fitting (seenanite.fit.guess_initial_parameters()
).If
compute_ancillaries
does not know how to compute a certain parameter, it shoud set it tonp.nan
instead ofNone
(compatibility with PyJibe).If you would like to define an ancillary parameter that depends on a successful fit, you could first check against
idnt.fit_properties["success"]
and then compute your parameter (else set it tonp.nan
).