From 4940fa98a8fd45932ba6fe8aaf51bfb463b43fe5 Mon Sep 17 00:00:00 2001 From: "tobias.elmoe" Date: Fri, 9 Dec 2022 15:08:23 +0100 Subject: [PATCH 1/4] feat(interface.Model): new class method "interface", which is to be used on interface class instead of Model.with_interface, which is currently used on the model class. Makes it more clear which one is the model and which one is the interface. An interface class can belong to just one type of model. --- setup.py | 3 +- .../solver/examples/models/bouncing_ball.py | 8 ++--- .../solver/examples/models/controller.py | 22 ++++++------- .../solver/examples/models/coupled_tanks.py | 33 +++++++++---------- .../examples/models/exponential_approach.py | 7 ++-- .../exponential_approach_no_derivatives.py | 12 +++---- src/numerous/solver/interface.py | 24 ++++++++++++++ 7 files changed, 66 insertions(+), 43 deletions(-) diff --git a/setup.py b/setup.py index 9f9d701..b4df1d5 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,8 @@ setuptools.setup( ], install_requires=[ "numba>=0.56.4", - "scipy==1.9.1" + "scipy==1.9.1", + "Deprecated>=1.2.13" ], packages=setuptools.find_namespace_packages(where='src'), package_dir={'': 'src'}, diff --git a/src/numerous/solver/examples/models/bouncing_ball.py b/src/numerous/solver/examples/models/bouncing_ball.py index 5290f0c..e359f72 100644 --- a/src/numerous/solver/examples/models/bouncing_ball.py +++ b/src/numerous/solver/examples/models/bouncing_ball.py @@ -131,7 +131,7 @@ import plotly.graph_objects as go import plotly -@Model.with_interface() +@Model class BouncingBall: def __init__(self, x0=1, v0=0, f_loss=0.05, g=9.81): @@ -157,9 +157,9 @@ class BouncingBall: return np.array([dxdt, dvdt], dtype='float') +@Model.interface(model=BouncingBall) class BouncingBallInterface(Interface): - def __init__(self, model): - self.model: BouncingBall = model + model: BouncingBall def get_deriv(self, t: float, y: np.array) -> np.ascontiguousarray: return self.model.diff(t, y) @@ -185,7 +185,7 @@ class BouncingBallInterface(Interface): if __name__ == "__main__": model = BouncingBall() - numsol = NumerousSolver(model=model, use_jit=False) + numsol = NumerousSolver(model=model, use_jit=True) time = np.append(np.arange(0, 10, 0.1), 10) numsol.solve(time) t = np.array(numsol.solution.results).T[0, :] diff --git a/src/numerous/solver/examples/models/controller.py b/src/numerous/solver/examples/models/controller.py index ed63ce8..eeb3c53 100644 --- a/src/numerous/solver/examples/models/controller.py +++ b/src/numerous/solver/examples/models/controller.py @@ -156,13 +156,13 @@ A note about jitting and the :meth:`Model.component` decorator The model of the CSTR has been implemented below in the :class:`Tank` model, while the controllers have been implemented as :class:`InletFlowController`, and :class:`OutletFlowController`, respectively. The :class:`Tank` model is decorated -by the :meth:`Model.with_interface` decorator, while the :class:`In/OutletFlowController` models are decorated with the -:meth:`Model.component` decorator. The reason being there can only be one interface per model, and that is the -one specified by the :class:`Tank` class, i.e. the :class:`TankInterface`. Since we want to allow the model to be -jitted by `numba`, we used the :meth:`Model.component` decorator on the :class:`In/OutletFlowController` classes. -Without this decorator, `numerous solver` cannot be jitted. Allowing jitting is also the reason why the instantiated -:class:`InletFlowController` and :class:`OutletFlowController` are fed to the :class:`Tank` as an input, since `numba` -currently cannot infer the type custom objects. Attempting to use jitting would result in an error. +by the :class:`~solver.interface.Model` decorator, while the :class:`In/OutletFlowController` models are decorated with +the :meth:`~solver.interface.Model.component` decorator. The reason being there can only be one interface per model, +and that is the one specified by the :class:`Tank` class, i.e. the :class:`TankInterface`. Since we want to allow the +model to be jitted by `numba`, we used the :meth:`Model.component` decorator on the :class:`In/OutletFlowController` +classes. Without this decorator, `numerous solver` cannot be jitted. Allowing jitting is also the reason why the +instantiated :class:`InletFlowController` and :class:`OutletFlowController` are fed to the :class:`Tank` as an input, +since `numba` currently cannot infer the type custom objects. Attempting to use jitting would result in an error. Examples -------------- @@ -263,7 +263,7 @@ class InletFlowController: self.actual_flow = self.nominal_flow -@Model.with_interface() +@Model class Tank: def __init__(self, v0: float = 1.0, a: float = 1.0, inlet_flow_controller: InletFlowController = None, outlet_flow_controller: OutletFlowController = None, @@ -305,11 +305,9 @@ class Tank: self.outlet_flow_controller.reset() self.initialize() - +@Model.interface(model=Tank) class TankInterface(Interface): - - def __init__(self, model: Tank): - self.model = model + model: Tank def set_states(self, y: np.array) -> None: y_cstr = y[:4] diff --git a/src/numerous/solver/examples/models/coupled_tanks.py b/src/numerous/solver/examples/models/coupled_tanks.py index 0e2a09f..03e72b3 100644 --- a/src/numerous/solver/examples/models/coupled_tanks.py +++ b/src/numerous/solver/examples/models/coupled_tanks.py @@ -66,23 +66,7 @@ from numerous.solver.interface import Interface, Model from numerous.solver.numerous_solver import NumerousSolver - -class CoupledTanksInterface(Interface): - - def __init__(self, model): - self.model: CoupledTanks = model - - def get_deriv(self, t: float, y: np.array) -> np.array: - return self.model.diff(y) - - def set_states(self, y): - self.model.y = y - - def get_states(self): - return self.model.y - - -@Model.with_interface(interface=CoupledTanksInterface) +@Model class CoupledTanks: def __init__(self, n_tanks=10, start_volume=10.0, k=1.0): @@ -120,6 +104,21 @@ class CoupledTanks: self.y = np.zeros(self.n_tanks, dtype='float') self.y[0] = self.start_volume + +@Model.interface(model=CoupledTanks) +class CoupledTanksInterface(Interface): + model: CoupledTanks + + def get_deriv(self, t: float, y: np.array) -> np.array: + return self.model.diff(y) + + def set_states(self, y): + self.model.y = y + + def get_states(self): + return self.model.y + + if __name__ == "__main__": n_tanks = 5 model = CoupledTanks(n_tanks=n_tanks) diff --git a/src/numerous/solver/examples/models/exponential_approach.py b/src/numerous/solver/examples/models/exponential_approach.py index 80c1a53..26205c3 100644 --- a/src/numerous/solver/examples/models/exponential_approach.py +++ b/src/numerous/solver/examples/models/exponential_approach.py @@ -60,7 +60,8 @@ from numerous.solver.numerous_solver import NumerousSolver import plotly.graph_objects as go import plotly -@Model.with_interface() + +@Model class ExponentialApproach: def __init__(self, k=1.0): self.y = np.array([0.0], dtype='float') @@ -73,10 +74,10 @@ class ExponentialApproach: self.y = np.array([0.0], dtype='float') +@Model.interface(model=ExponentialApproach) class ExponentialApproachInterface(Interface): - def __init__(self, model: ExponentialApproach): - self.model = model + model: ExponentialApproach def get_states(self) -> np.array: return self.model.y diff --git a/src/numerous/solver/examples/models/exponential_approach_no_derivatives.py b/src/numerous/solver/examples/models/exponential_approach_no_derivatives.py index 1d5b019..958bd8a 100644 --- a/src/numerous/solver/examples/models/exponential_approach_no_derivatives.py +++ b/src/numerous/solver/examples/models/exponential_approach_no_derivatives.py @@ -18,7 +18,8 @@ but for now this is not possible with numerous solver. Implementation in numerous solver --------------------------------------------------- -The :class:`ExponentialApproach` model is wrapped by the :meth:`~solver.interface.Model.with_interface` decorator, +The :class:`ExponentialApproach` model is wrapped by the :class:`~solver.interface.Model` decorator, while it's +interface is decorated by the :meth:`~solver.interface.Model.interface` method, which links the model to it's interface :class:`ExponentialApproachInterface`. It takes just one parameters as input: ============= ============================================================================================== @@ -47,7 +48,6 @@ by calling the :meth:`update_step` and then :meth:`set_states`. Examples -------------- Below is an example code that runs the model, and creates a plot for multiple values of :math:`k`. - """ import numpy as np @@ -59,7 +59,7 @@ import plotly.graph_objects as go import plotly -@Model.with_interface() +@Model class ExponentialApproach(): def __init__(self, k=1.0): self.y = np.array([0.0]) @@ -72,10 +72,9 @@ class ExponentialApproach(): self.y = np.array([0.0]) +@Model.interface(model=ExponentialApproach) class ExponentialApproachInterface(Interface): - - def __init__(self, model: ExponentialApproach): - self.model = model + model: ExponentialApproach def set_states(self, states): self.model.y = states @@ -88,6 +87,7 @@ class ExponentialApproachInterface(Interface): self.set_states(y) return SolveEvent.NoneEvent + if __name__ == "__main__": model = ExponentialApproach(k=1.0) # Generate the model numsol = NumerousSolver(model=model, method='RK45', use_jit=True) # pass model to numerous solver diff --git a/src/numerous/solver/interface.py b/src/numerous/solver/interface.py index 4f9b53e..40fe8cf 100644 --- a/src/numerous/solver/interface.py +++ b/src/numerous/solver/interface.py @@ -8,6 +8,7 @@ from numba.experimental import jitclass from numpy.typing import NDArray from typing import Optional from abc import ABC +from deprecated.sphinx import deprecated from .solve_states import SolveEvent @@ -489,6 +490,8 @@ class Model: """ new_class = self.__new__(self.__class__) + if hasattr(self.model_class, '_get_interface'): + self.interface = self.model_class._get_interface(self.model_class) new_class.__init__(self.model_class, self.interface, self.iscomponent) new_class._JitHelper = _JitHelper(new_class.model_class, *args, **kwargs) new_class.python_model = new_class._JitHelper.python_model @@ -497,7 +500,28 @@ class Model: return new_class + + @classmethod + def interface(cls, model): + """ + class method decorator to decorate a model Interface and link the model class to its interface. The model class + is monkey patched with the _get_interface() method. Overriding this method can lead to unexpected results. + :param model: The model class to link to. + :return: wrapped interface as a Model class. + """ + def inner(interface): + def get_interface(self): + return interface + model.model_class._get_interface = get_interface + + return cls(model.model_class, interface) + + return inner + + @classmethod + @deprecated(version="1.4.0", reason="This method has been deprecated. Please use decorate model with @Model " + "decorator, and mark interface with @Model.interface(model=model)") def with_interface(cls, interface: type(Interface) | str | None = None): """decorator to link model to interface. Classes inside the model class must be passed as arguments, if using numba compiled solver (jit=True). This is due to limitations of numba. -- GitLab From 1d226beae4beb556bc98ac0fca9708f6a172956d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 9 Dec 2022 15:10:12 +0000 Subject: [PATCH 2/4] chore(release): 1.4.0-alpha.1 [skip ci] # [1.4.0-alpha.1](https://gitlab.com/numerous/numerous-solver/compare/v1.3.2...v1.4.0-alpha.1) (2022-12-09) ### Features * **interface.Model:** new class method "interface", which is to be used on interface class instead of Model.with_interface, which is currently used on the model class. Makes it more clear which one is the model and which one is the interface. An interface class can belong to just one type of model. ([4940fa9](https://gitlab.com/numerous/numerous-solver/commit/4940fa98a8fd45932ba6fe8aaf51bfb463b43fe5)) --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 1892b92..2a6fa03 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.3.2 +1.4.0-alpha.1 -- GitLab From 101bf358b83b1f03141137c8f17041fa695d9b2a Mon Sep 17 00:00:00 2001 From: "tobias.elmoe" Date: Mon, 12 Dec 2022 11:04:48 +0100 Subject: [PATCH 3/4] feat: added possibility of warning when overwriting model interface. Added test `test_interface_overwrite`. Updated documentation --- docs/source/tutorial.rst | 35 +++++++++++++------ src/numerous/solver/interface.py | 5 ++- .../exponential_approach_args_and_kwargs.py | 9 ++--- tests/src/test_model_and_interface.py | 23 ++++++++++-- 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 7fd7db2..134e2c3 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -83,9 +83,8 @@ In python this could be implemented like this :class:`~solver.examples.models.ex | Since :class:`~solver.numerous_solver.NumerousSolver` handles systems of the kind :math:`dy/dt=f(t, y)`, the model above has implemented the method `diff` which should be called from :class:`~solver.numerous_solver.NumerousSolver` by creating an :class:`~solver.interface.Interface`. Notice how the model is decorated by the - :meth:`~solver.interface.Model.with_interface` decorator. If no arguments are provided to the decorator, the interface - is assumed to be named ${ModelName}Interface. This is how the model connects to the interface, and it's - explained in the next section. + :class:`~solver.interface.Model` decorator. Next, we need to create an interface and connect it to this model, which + is explained in the next section. ------------------------------- Example: Writing an interface @@ -100,26 +99,42 @@ Example: Writing an interface If you wish to solve an ODE, you need to implement the :meth:`~solver.interface.Interface.get_deriv` method as well. -| With the example above for :class:`~solver.examples.models.exponential_approach.ExponentialApproach` the correspoding +| With the example above for :class:`~solver.examples.models.exponential_approach.ExponentialApproach` the corresponding interface would be: .. literalinclude:: ../../src/numerous/solver/examples/models/exponential_approach.py :pyobject: ExponentialApproachInterface :language: python +Here, we have decorated the interface using the :meth:`~solver.interface.Model.interface` decorator, with arguments +`model=ExponentialApproach`, where model refers to the +:class:`~solver.examples.model.exponential_approach.ExponentialApproach` model class. Decorating this way links the +interface and its model. A model can only have one interface. If we create another interface somewhere in our code, and +decorate with the same method: + +.. Code-Block:: + @Model.interface(model=ExponentialApproach) + class NewInterface(Interface): + ... + +then the original interface will be overwritten, depending on where in the script/program this is done. A warning will +be given if a model already has an interface. The warning can be overwritten using the argument `warn=False` in the +:meth:`~solver.interface.Model.interface` method. + Lets go through the methods of the :class:`~solver.examples.models.exponential_approach.ExponentialApproachInterface`: -* | :meth:`~solver.examples.models.exponential_approach.ExponentialApproachInterface.get_states` returns the states from the - model. This is used to save the states in the :class:`~solver.interface.Solution` object. -* | :meth:`~solver.examples.models.exponential_approach.ExponentialApproachInterface.set_states` takes the states from the - solver and transfers them to the model. The solver operates only with :class:`numpy.ndarray` arrays, but the model - may use any format to store the states, using dicts, dataframes etc, though jitting always requires that the +* | :meth:`~solver.examples.models.exponential_approach.ExponentialApproachInterface.get_states` returns the states from + the model. This is used to save the states in the :class:`~solver.interface.Solution` object. +* | :meth:`~solver.examples.models.exponential_approach.ExponentialApproachInterface.set_states` takes the states from + the solver and transfers them to the model. The solver operates only with :class:`numpy.ndarray` arrays, but the + model may use any format to store the states, using dicts, dataframes etc, though jitting always requires that the objects can be understood by `numba `_. In the example the model states are identical to the solver states, so the transfer is straight-forward. * | :meth:`~solver.examples.models.exponential_approach.ExponentialApproachInterface.get_deriv` returns the calculated derivatives. In the example, the :meth:`~solver.examples.models.exponential_approach.ExponentialApproachInterface.get_deriv` calls the model - :meth:`~solver.examples.models.exponential_approach.ExponentialApproach.diff` method, and returns the value of this method. + :meth:`~solver.examples.models.exponential_approach.ExponentialApproach.diff` method, and returns the value of this + method. | That's it. It may seem more cumbersome than simply specifying the diff function to the solver, such as it is standard practise in many ODE solver suites, however the approach allows for much more advanced model to be implemented, and diff --git a/src/numerous/solver/interface.py b/src/numerous/solver/interface.py index 40fe8cf..cb954d3 100644 --- a/src/numerous/solver/interface.py +++ b/src/numerous/solver/interface.py @@ -1,5 +1,6 @@ import inspect import logging +import warnings import numba as nb import numpy as np @@ -502,7 +503,7 @@ class Model: @classmethod - def interface(cls, model): + def interface(cls, model, warn=True): """ class method decorator to decorate a model Interface and link the model class to its interface. The model class is monkey patched with the _get_interface() method. Overriding this method can lead to unexpected results. @@ -512,6 +513,8 @@ class Model: def inner(interface): def get_interface(self): return interface + if hasattr(model.model_class, '_get_interface') and warn: + logger.warning(f"model {model.model_class} already has a defined interface, which will be overwritten") model.model_class._get_interface = get_interface return cls(model.model_class, interface) diff --git a/tests/src/models/exponential_approach_args_and_kwargs.py b/tests/src/models/exponential_approach_args_and_kwargs.py index 52311a3..a49f492 100644 --- a/tests/src/models/exponential_approach_args_and_kwargs.py +++ b/tests/src/models/exponential_approach_args_and_kwargs.py @@ -1,7 +1,7 @@ import numpy as np from numerous.solver.interface import Model, Interface -@Model.with_interface() +@Model class ExponentialApproach: def __init__(self, k, y0=0.0): self.y = np.array([y0], dtype='float') @@ -10,12 +10,9 @@ class ExponentialApproach: def diff(self, t, y) -> np.array: return np.array([self.k * np.exp(-self.k * t)]) - +@Model.interface(model=ExponentialApproach) class ExponentialApproachInterface(Interface): - - def __init__(self, model: ExponentialApproach): - self.model = model - + model: ExponentialApproach def get_states(self) -> np.array: return self.model.y diff --git a/tests/src/test_model_and_interface.py b/tests/src/test_model_and_interface.py index 133cefa..c76190a 100644 --- a/tests/src/test_model_and_interface.py +++ b/tests/src/test_model_and_interface.py @@ -1,13 +1,17 @@ +import logging + import pytest import numpy as np from conftest import normal_solver, numerous_solver, ABSTOL, RELTOL, get_timerange, RUNMAX from numerous.solver.interface import Model as NumerousModel from numerous.solver.numerous_solver import NumerousSolver -from models.exponential_approach_args_and_kwargs import ExponentialApproach as ExponentialApproachArgsAndKwargs +from models.exponential_approach_args_and_kwargs import ExponentialApproach as ExponentialApproachArgsAndKwargs, \ + ExponentialApproachInterface from models.exponential_approach_numpy_input import ExponentialApproach as ExponentialApproachNumpyInput from models.exponential_approach_time_events_list import ExponentialApproach as ExponentialApproachTimeListEvents + @pytest.mark.parametrize("solve", [normal_solver]) @pytest.mark.parametrize("jit", [False, True]) @pytest.mark.parametrize("Model", [ExponentialApproachArgsAndKwargs]) @@ -55,4 +59,19 @@ def test_init_solver(jit: bool, numerous_solver: numerous_solver, get_timerange: solver.reset() solver.solve(timerange) - assert len(solver.solution.time_event_results) == len(time_events[-1]), "expected time events to match solution" \ No newline at end of file + assert len(solver.solution.time_event_results) == len(time_events[-1]), "expected time events to match solution" + +@pytest.mark.parametrize("warn", [True, False]) +def test_interface_overwrite(warn: bool, caplog): + + with caplog.at_level(logging.WARNING): + model = ExponentialApproachArgsAndKwargs(k=1.0) + @NumerousModel.interface(model=ExponentialApproachArgsAndKwargs, warn=warn) + class NewInterface(ExponentialApproachInterface): + pass + if warn: + assert "overwritten" in caplog.text, "expected warning" + else: + assert not "overwritten" in caplog.text, "expected no warning" + + -- GitLab From 0d2e2eb45e3f11285e41ed59efd3e98ddb73e708 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 12 Dec 2022 11:06:28 +0000 Subject: [PATCH 4/4] chore(release): 1.4.0-alpha.2 [skip ci] # [1.4.0-alpha.2](https://gitlab.com/numerous/numerous-solver/compare/v1.4.0-alpha.1...v1.4.0-alpha.2) (2022-12-12) ### Features * added possibility of warning when overwriting model interface. Added test `test_interface_overwrite`. Updated documentation ([101bf35](https://gitlab.com/numerous/numerous-solver/commit/101bf358b83b1f03141137c8f17041fa695d9b2a)) --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 2a6fa03..54dd11e 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.4.0-alpha.1 +1.4.0-alpha.2 -- GitLab