Skip to content

Commit

Permalink
feat: allow to load fitting models from external Python files
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmueller committed Jan 6, 2022
1 parent 66ec9ba commit dd7f92d
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 23 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
3.4.0
- feat: allow to load fitting models from external Python files
- enh: add method to deregister NaniteFitModels
3.3.1
- docs: added some clarifications in the model docs
- enh: check for leading/trailing spaces in models
Expand Down
18 changes: 15 additions & 3 deletions docs/sec_develop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,21 @@ 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_``).
You have two options (**1 or 2**) to make your model available in nanite:
You have three options (**1, 2 or 3**) to make your model available in nanite:

1. Place the file in the following location: ``nanite/model/model_unique_name.py``.
1. Place the file anywhere in your file system (e.g.
``/home/peter/model_unique_name.py``) and run:

.. code:: python
from nanite.model import load_model_from_file
load_model_from_file("/home/peter/model_unique_name.py", register=True)
This is probably the most convenient method when prototyping. Note that
you can also import model scritps in PyJibe (via the Preferences menu).

2. Place the file in the following location: ``nanite/model/model_unique_name.py``.
Once you have created this file, you have to register it in nanite by
adding the line

Expand All @@ -74,7 +86,7 @@ You have two options (**1 or 2**) to make your model available in nanite:
at the top in the file ``nanite/model/__init__.py``. This is the procedure
when you create a pull request.

2. Or place the file in another location from where you can import it. This can
3. Or place the file in another location from where you can import it. This can
be a submodule in a different package, or just the script in your ``PATH``.
The only thing you need is to import the script and register it.

Expand Down
18 changes: 3 additions & 15 deletions nanite/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from . import model_sneddon_spherical # noqa: F401
from . import model_sneddon_spherical_approximation # noqa: F401

from .core import NaniteFitModel
from .core import NaniteFitModel # noqa: F401
from . import residuals # noqa: F401
from .logic import models_available, register_model
from .logic import deregister_model, load_model_from_file # noqa: F401


def compute_anc_parms(idnt, model_key):
Expand Down Expand Up @@ -119,20 +121,6 @@ def get_parm_unit(model_key, parm_key):
return md.get_parm_unit(parm_key)


def register_model(module, *args):
"""Register a fitting model"""
if args:
warnings.warn("Please only pass the module to `register_model`!",
DeprecationWarning)
global models_available # this is not necessary, but clarifies things
# add model
md = NaniteFitModel(module)
models_available[module.model_key] = md
return md


models_available = {}

# Populate list of available fit models
_loc = locals().copy()
for _item in _loc:
Expand Down
12 changes: 10 additions & 2 deletions nanite/model/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@
from . import residuals


class ModelIncompleteError(BaseException):
class ModelError(BaseException):
pass


class ModelImplementationError(BaseException):
class ModelIncompleteError(ModelError):
pass


class ModelImplementationError(ModelError):
pass


class ModelImportError(ModelError):
pass


Expand Down
87 changes: 87 additions & 0 deletions nanite/model/logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import importlib
import pathlib
import sys
import warnings

from .core import ModelImportError, NaniteFitModel

#: currently available models
models_available = {}


def load_model_from_file(path, register=False):
"""Import a fit model file and return the module
This is intended for loading custom models or for model
development.
Parameters
----------
path: str or Path
pathname to a Python script conaining a fit model
register: bool
whether to register the model after import
Returns
-------
model: NaniteFitModel
nanite fit model object
Raises
------
ModelImportError
If the model cannot be imported
"""
path = pathlib.Path(path)
try:
# insert the plugin directory to sys.path so we can import it
sys.path.insert(-1, str(path.parent))
sys.dont_write_bytecode = True
module = importlib.import_module(path.stem)
except ModuleNotFoundError:
raise ModelImportError(f"Could not import '{path}'!")
finally:
# undo our path insertion
sys.path.pop(0)
sys.dont_write_bytecode = False

mod = NaniteFitModel(module)

if register:
register_model(module)

return mod


def register_model(module, *args):
"""Register a fitting model
Parameters
----------
module: Python module or NaniteFitModel
the model to register
Returns
-------
model: NaniteFitModel
the corresponding NaniteFitModel instance
"""
if args:
warnings.warn("Please only pass the module to `register_model`!",
DeprecationWarning)
global models_available # this is not necessary, but clarifies things
# add model
if isinstance(module, NaniteFitModel):
# we already have a fit model
md = module
else:
md = NaniteFitModel(module)
# the actual registration
models_available[module.model_key] = md
return md


def deregister_model(model):
"""Deregister a NaniteFitModel"""
global models_available # this is not necessary, but clarifies things
models_available.pop(model.model_key)
7 changes: 4 additions & 3 deletions tests/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import lmfit
import nanite
import nanite.model.logic
from nanite.model import residuals
import numpy as np

Expand All @@ -16,7 +17,7 @@ def __init__(self, model_key, **kwargs):
self.model_key = model_key

def __enter__(self):
return nanite.model.register_model(self)
return nanite.model.logic.register_model(self)

def __exit__(self, a, b, c):
nanite.model.models_available.pop(self.model_key)
Expand All @@ -40,11 +41,11 @@ def __init__(self, **kwargs):
self.valid_axes_y = ["force"]

def __enter__(self):
nanite.model.register_model(self)
nanite.model.logic.register_model(self)
return self

def __exit__(self, a, b, c):
nanite.model.models_available.pop(self.model_key)
nanite.model.logic.deregister_model(self)

@staticmethod
def get_parameter_defaults():
Expand Down
37 changes: 37 additions & 0 deletions tests/data/model_external_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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("R", value=10e-6, min=0, vary=False)
params.add("nu", value=.5, min=0, max=0.5, vary=False)
params.add("contact_point", value=0)
params.add("baseline", value=0)
return params


def hertz_paraboloidal(delta, E, R, nu, contact_point=0, baseline=0):
"""This is identical to the Hertz parabolic indenter model"""
aa = 4/3 * E/(1-nu**2)*np.sqrt(R)
root = contact_point-delta
pos = root > 0
bb = np.zeros_like(delta)
bb[pos] = (root[pos])**(3/2)
return aa*bb + baseline


model_doc = hertz_paraboloidal.__doc__
model_func = hertz_paraboloidal
model_key = "hans_peter"
model_name = "Hans Peter's model"
parameter_keys = ["E", "R", "nu", "contact_point", "baseline"]
parameter_names = ["Young's Modulus", "Tip Radius",
"Poisson's Ratio", "Contact Point", "Force Baseline"]
parameter_units = ["Pa", "m", "", "m", "N"]
valid_axes_x = ["tip position"]
valid_axes_y = ["force"]
27 changes: 27 additions & 0 deletions tests/test_model_logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import pathlib

import nanite


data_dir = pathlib.Path(__file__).parent / "data"


def test_load_model_from_file():
mpath = data_dir / "model_external_basic.py"
md = nanite.model.load_model_from_file(mpath, register=True)
assert md.model_key == "hans_peter"
assert md.model_key in nanite.model.models_available
nanite.model.deregister_model(md)
assert md.model_key not in nanite.model.models_available


def test_load_model_from_model():
mpath = data_dir / "model_external_basic.py"
md = nanite.model.load_model_from_file(mpath, register=False)
assert md.model_key == "hans_peter"
assert md.model_key not in nanite.model.models_available
md2 = nanite.model.register_model(md)
assert md is md2
assert md.model_key in nanite.model.models_available
nanite.model.deregister_model(md)
assert md.model_key not in nanite.model.models_available

0 comments on commit dd7f92d

Please sign in to comment.