npsolve


Namenpsolve JSON
Version 0.2.0 PyPI version JSON
download
home_pagehttps://github.com/pythoro/npsolve.git
SummaryEasier object-oriented calculations for numerical solvers.
upload_time2024-03-16 06:54:05
maintainer
docs_urlNone
authorReuben Rusk
requires_python
license
keywords numerical solver numpy scipy ode integration
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # npsolve

The *npsolve* package is a small, simple package built on *numpy* to make it
easy to use object-oriented classes and methods for the calculation step for
numerical solvers.

Many numerical solvers (like those in *scipy*) provide candidate solutions as
a numpy ndarray. They often also require a numpy ndarray as a return value
(e.g. an array of derivatives) during the solution. These requirements can make
it difficult to use an object oriented approach to performing the calculations.
Usually, we end up with script-like code that looses many of the benefits
of object-oriented programming.

The npsolve framework links a solver with multiple classes that handle the
calculations for each step in the algorithm. It allows different parts of 
the calculations to be encapsulated and polymorphic, and makes the code 
much easier to modify and maintain.

Advantages:
* No-fuss management of variables and their initial conditions
* Updated state automatically shared with all objects
* Calls between classes possible using dependency injection
* Optional caching methods prevent redundant calculations
* Introduces very little overhead in calculation time


## Basic usage tutorial

Let's use npsolve to do some integration through time, like you would to
solve an ODE. Instead of equations, though, we're using class methods. The
code for all the tutorials is available in the repository under 'examples'.

First, setup some classes that you want to do calculations with. We do this
by using the `add_var` method to setup variables and their initial values.

```python

import numpy as np
import npsolve

class Component1(npsolve.Partial):
    def __init__(self):
        super().__init__()  # Don't forget to call this!
        self.add_var("position1", init=0.1)
        self.add_var("velocity1", init=0.3)
    

class Component2(npsolve.Partial):
    def __init__(self):
        super().__init__()  # Don't forget to call this!
        self.add_var("component2_value", init=-0.1)

```

All the variables are made available to all Partial instances automatically
through their `state` attribute. It's a dictionary. The `add_var` method 
sets initial values into the instance's state dictionary. Later, the `Solver`
will ultimately replace the `state` attribute with a new dictionary that
contains all variables from all the Partial classes.

Next, we'll tell these classes how to do some calculations during each time
step. The `step` method is called automatically and expects a dictionary of
return values (e.g. derivatives). We'll use that one here. The state
dictionary is given again as the first argument, but we're going to use the
internal `state` attribute instead. So, we'll add some more methods:

```python

class Component1(npsolve.Partial):
    def __init__(self):
        super().__init__()  # Don't forget to call this!
        self.add_var("position1", init=0.1)
        self.add_var("velocity1", init=0.3)

    def step(self, state_dct, t, *args):
        """Called by the solver at each time step

        Calculate acceleration based on the net component2_value.
        """
        acceleration = 1.0 * self.state["component2_value"]
        derivatives = {
            "position1": self.state["velocity1"],
            "velocity1": acceleration,
        }
        return derivatives


class Component2(npsolve.Partial):
    def __init__(self):
        super().__init__()  # Don't forget to call this!
        self.add_var("component2_value", init=-0.1)

    def calculate(self, t):
        """Some arbitrary calculations based on current time t
        and the position at that time calculated in Component1.
        This returns a derivative for variable 'c'
        """
        dc = 1.0 * np.cos(2 * t) * self.state["position1"]
        derivatives = {"component2_value": dc}
        return derivatives

    def step(self, state_dct, t, *args):
        """Called by the solver at each time step"""
        return self.calculate(t)

```

Now, we'll set up the solver. For this example, we'll use the odeint solver
from Scipy (npsolve has a more convenient Solver class).
Here's what it looks like:


```python

from scipy.integrate import odeint

class Solver(npsolve.Solver):
    def solve(self, t_end=10):
        self.npsolve_init()  # Initialise
        self.t_vec = np.linspace(0, t_end, 1001)
        result = odeint(self.step, self.npsolve_initial_values, self.t_vec)
        return result

```

Let's look at what's going on in the `solve` method. By default, Solvers
have a `step` method that's ready to use. (They also have a `one_way_step`
method that doesn't expect return values from the Partials, and a `tstep` 
method that expects a time value as the first argument.) After initialisation,
the initial values set by the Partial classes are captured in the
`npsolve_initial_values` attribute. By default, the Solver's `step` method
returns a vector of all the return values, the same size as the Solver's
`npsolve_initial_values` array. So most of the work is done for us here
already. 

Note here that we don't need to know anything about the model or
the elements in the model. This allows us to decouple the model and Partials
from the solver. We can pass in different models, or pass models to different
solvers. We can make models with different components. It's flexible and easy
to maintain!

To run, we just have to instantiate the Solver and Partial instances,
then pass a list or dictionary of the Partial instances to the
`connect_partials` method of the Solver. They'll link up automatically.
Or, you can link them individually using the `connect_partial` method.


```python
    
def run():
    solver = Solver()
    partials = [Component1(), Component2()]
    solver.connect_partials(partials)
    res = solver.solve()
    return res, solver
```

Let's set up a plot to see the results. Use the `npsolve_slices` attribute of
the Solver to get the right columns. (The npsolve.Solver class makes accessing
results more convenient by splitting them into a dictionary.)

```python

import matplotlib.pyplot as plt

def plot(res, solver):
    s = solver
    slices = s.npsolve_slices
    plt.figure()
    plt.plot(s.t_vec, res[:, slices["position1"]], label="position1")
    plt.plot(s.t_vec, res[:, slices["velocity1"]], label="velocity1")
    plt.plot(
        s.t_vec, res[:, slices["component2_value"]], label="component2_value"
    )
    plt.legend()

```

Run it and see what happens!

```python

res, s = run()
plot(res, s)

```

### Calls between partials
To facilitate calls between components, use dependency injection. Let's 
illustrate by using methods instead of instead of using the values in the
state dictionary like we did above. So, let's modify our two classes like 
this:

```python

class Component1(npsolve.Partial):
    def __init__(self):
        super().__init__()  # Don't forget to call this!
        self.add_var("position1", init=0.1)
        self.add_var("velocity1", init=0.3)

    def get_position(self):
        """Returns a value
        
        In this example, it is just a state variable, but it could be much
        more complex.
        """
        return self.state['position1']

    def connect(self, component2, reverse=True):
        """Connect with a Component2 instance"""
        self._component2 = component2
        if reverse:
            component2.connect(self, reverse=False)

    def step(self, state_dct, t, *args):
        """Called by the solver at each time step

        Calculate acceleration based on the net component2_value.
        """
        acceleration = 1.0 * self._component2.get_value()
        derivatives = {
            "position1": self.state["velocity1"],
            "velocity1": acceleration,
        }
        return derivatives


class Component2(npsolve.Partial):
    def __init__(self):
        super().__init__()  # Don't forget to call this!
        self.add_var("component2_value", init=-0.1)

    def get_value(self):
        """Returns a value
        
        In this example, it is just a state variable, but it could be much
        more complex.
        """
        return self.state['component2_value']

    def connect(self, component1, reverse=True):
        """Connect with a Component1 instance"""
        self._component1 = component1
        if reverse:
            component1.connect(self, reverse=False)

    def calculate(self, t):
        """Some arbitrary calculations based on current time t
        and the position at that time calculated in Component1.
        This returns a derivative for variable 'c'
        """
        dc = 1.0 * np.cos(2 * t) * self._component1.get_position()
        derivatives = {"component2_value": dc}
        return derivatives

    def step(self, state_dct, t, *args):
        """Called by the solver at each time step"""
        return self.calculate(t)

```

Before we run the solver, we just need to inject the dependency by calling
the 'connect' methods we've created. So, now our run function becomes:

```python

def run():
    solver = Solver()
    component1 = Component1()
    component2 = Component2()
    component1.connect(component2)  # Inject the dependency
    component2.connect(component1)  # Inject the dependency
    partials = [component1, component2]
    solver.connect_partials(partials)
    res = solver.solve()
    return res, solver

```


### Nested Partial instances
You can also nest Partial instances. Under the hood, `connect_partials` passes
the Solver to the `connect_solver` method of each Partial instance. Just
overwrite the parent Partial instance's `connect_solver` method to pass
the solver instance on to the `connect_solver` method on the children.


## Tutorials

Check out the tutorials in the examples folder to learn the basics and 
learn about some more advanced features like the Solver class, the Timeseries
class, caching, and logging extra values.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/pythoro/npsolve.git",
    "name": "npsolve",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "NUMERICAL SOLVER,NUMPY,SCIPY,ODE,INTEGRATION",
    "author": "Reuben Rusk",
    "author_email": "pythoro@mindquip.com",
    "download_url": "https://files.pythonhosted.org/packages/80/62/737835398abda4846e4922f02571d1461c496022ce0eed99719e97987ae8/npsolve-0.2.0.tar.gz",
    "platform": null,
    "description": "# npsolve\r\n\r\nThe *npsolve* package is a small, simple package built on *numpy* to make it\r\neasy to use object-oriented classes and methods for the calculation step for\r\nnumerical solvers.\r\n\r\nMany numerical solvers (like those in *scipy*) provide candidate solutions as\r\na numpy ndarray. They often also require a numpy ndarray as a return value\r\n(e.g. an array of derivatives) during the solution. These requirements can make\r\nit difficult to use an object oriented approach to performing the calculations.\r\nUsually, we end up with script-like code that looses many of the benefits\r\nof object-oriented programming.\r\n\r\nThe npsolve framework links a solver with multiple classes that handle the\r\ncalculations for each step in the algorithm. It allows different parts of \r\nthe calculations to be encapsulated and polymorphic, and makes the code \r\nmuch easier to modify and maintain.\r\n\r\nAdvantages:\r\n* No-fuss management of variables and their initial conditions\r\n* Updated state automatically shared with all objects\r\n* Calls between classes possible using dependency injection\r\n* Optional caching methods prevent redundant calculations\r\n* Introduces very little overhead in calculation time\r\n\r\n\r\n## Basic usage tutorial\r\n\r\nLet's use npsolve to do some integration through time, like you would to\r\nsolve an ODE. Instead of equations, though, we're using class methods. The\r\ncode for all the tutorials is available in the repository under 'examples'.\r\n\r\nFirst, setup some classes that you want to do calculations with. We do this\r\nby using the `add_var` method to setup variables and their initial values.\r\n\r\n```python\r\n\r\nimport numpy as np\r\nimport npsolve\r\n\r\nclass Component1(npsolve.Partial):\r\n    def __init__(self):\r\n        super().__init__()  # Don't forget to call this!\r\n        self.add_var(\"position1\", init=0.1)\r\n        self.add_var(\"velocity1\", init=0.3)\r\n    \r\n\r\nclass Component2(npsolve.Partial):\r\n    def __init__(self):\r\n        super().__init__()  # Don't forget to call this!\r\n        self.add_var(\"component2_value\", init=-0.1)\r\n\r\n```\r\n\r\nAll the variables are made available to all Partial instances automatically\r\nthrough their `state` attribute. It's a dictionary. The `add_var` method \r\nsets initial values into the instance's state dictionary. Later, the `Solver`\r\nwill ultimately replace the `state` attribute with a new dictionary that\r\ncontains all variables from all the Partial classes.\r\n\r\nNext, we'll tell these classes how to do some calculations during each time\r\nstep. The `step` method is called automatically and expects a dictionary of\r\nreturn values (e.g. derivatives). We'll use that one here. The state\r\ndictionary is given again as the first argument, but we're going to use the\r\ninternal `state` attribute instead. So, we'll add some more methods:\r\n\r\n```python\r\n\r\nclass Component1(npsolve.Partial):\r\n    def __init__(self):\r\n        super().__init__()  # Don't forget to call this!\r\n        self.add_var(\"position1\", init=0.1)\r\n        self.add_var(\"velocity1\", init=0.3)\r\n\r\n    def step(self, state_dct, t, *args):\r\n        \"\"\"Called by the solver at each time step\r\n\r\n        Calculate acceleration based on the net component2_value.\r\n        \"\"\"\r\n        acceleration = 1.0 * self.state[\"component2_value\"]\r\n        derivatives = {\r\n            \"position1\": self.state[\"velocity1\"],\r\n            \"velocity1\": acceleration,\r\n        }\r\n        return derivatives\r\n\r\n\r\nclass Component2(npsolve.Partial):\r\n    def __init__(self):\r\n        super().__init__()  # Don't forget to call this!\r\n        self.add_var(\"component2_value\", init=-0.1)\r\n\r\n    def calculate(self, t):\r\n        \"\"\"Some arbitrary calculations based on current time t\r\n        and the position at that time calculated in Component1.\r\n        This returns a derivative for variable 'c'\r\n        \"\"\"\r\n        dc = 1.0 * np.cos(2 * t) * self.state[\"position1\"]\r\n        derivatives = {\"component2_value\": dc}\r\n        return derivatives\r\n\r\n    def step(self, state_dct, t, *args):\r\n        \"\"\"Called by the solver at each time step\"\"\"\r\n        return self.calculate(t)\r\n\r\n```\r\n\r\nNow, we'll set up the solver. For this example, we'll use the odeint solver\r\nfrom Scipy (npsolve has a more convenient Solver class).\r\nHere's what it looks like:\r\n\r\n\r\n```python\r\n\r\nfrom scipy.integrate import odeint\r\n\r\nclass Solver(npsolve.Solver):\r\n    def solve(self, t_end=10):\r\n        self.npsolve_init()  # Initialise\r\n        self.t_vec = np.linspace(0, t_end, 1001)\r\n        result = odeint(self.step, self.npsolve_initial_values, self.t_vec)\r\n        return result\r\n\r\n```\r\n\r\nLet's look at what's going on in the `solve` method. By default, Solvers\r\nhave a `step` method that's ready to use. (They also have a `one_way_step`\r\nmethod that doesn't expect return values from the Partials, and a `tstep` \r\nmethod that expects a time value as the first argument.) After initialisation,\r\nthe initial values set by the Partial classes are captured in the\r\n`npsolve_initial_values` attribute. By default, the Solver's `step` method\r\nreturns a vector of all the return values, the same size as the Solver's\r\n`npsolve_initial_values` array. So most of the work is done for us here\r\nalready. \r\n\r\nNote here that we don't need to know anything about the model or\r\nthe elements in the model. This allows us to decouple the model and Partials\r\nfrom the solver. We can pass in different models, or pass models to different\r\nsolvers. We can make models with different components. It's flexible and easy\r\nto maintain!\r\n\r\nTo run, we just have to instantiate the Solver and Partial instances,\r\nthen pass a list or dictionary of the Partial instances to the\r\n`connect_partials` method of the Solver. They'll link up automatically.\r\nOr, you can link them individually using the `connect_partial` method.\r\n\r\n\r\n```python\r\n    \r\ndef run():\r\n    solver = Solver()\r\n    partials = [Component1(), Component2()]\r\n    solver.connect_partials(partials)\r\n    res = solver.solve()\r\n    return res, solver\r\n```\r\n\r\nLet's set up a plot to see the results. Use the `npsolve_slices` attribute of\r\nthe Solver to get the right columns. (The npsolve.Solver class makes accessing\r\nresults more convenient by splitting them into a dictionary.)\r\n\r\n```python\r\n\r\nimport matplotlib.pyplot as plt\r\n\r\ndef plot(res, solver):\r\n    s = solver\r\n    slices = s.npsolve_slices\r\n    plt.figure()\r\n    plt.plot(s.t_vec, res[:, slices[\"position1\"]], label=\"position1\")\r\n    plt.plot(s.t_vec, res[:, slices[\"velocity1\"]], label=\"velocity1\")\r\n    plt.plot(\r\n        s.t_vec, res[:, slices[\"component2_value\"]], label=\"component2_value\"\r\n    )\r\n    plt.legend()\r\n\r\n```\r\n\r\nRun it and see what happens!\r\n\r\n```python\r\n\r\nres, s = run()\r\nplot(res, s)\r\n\r\n```\r\n\r\n### Calls between partials\r\nTo facilitate calls between components, use dependency injection. Let's \r\nillustrate by using methods instead of instead of using the values in the\r\nstate dictionary like we did above. So, let's modify our two classes like \r\nthis:\r\n\r\n```python\r\n\r\nclass Component1(npsolve.Partial):\r\n    def __init__(self):\r\n        super().__init__()  # Don't forget to call this!\r\n        self.add_var(\"position1\", init=0.1)\r\n        self.add_var(\"velocity1\", init=0.3)\r\n\r\n    def get_position(self):\r\n        \"\"\"Returns a value\r\n        \r\n        In this example, it is just a state variable, but it could be much\r\n        more complex.\r\n        \"\"\"\r\n        return self.state['position1']\r\n\r\n    def connect(self, component2, reverse=True):\r\n        \"\"\"Connect with a Component2 instance\"\"\"\r\n        self._component2 = component2\r\n        if reverse:\r\n            component2.connect(self, reverse=False)\r\n\r\n    def step(self, state_dct, t, *args):\r\n        \"\"\"Called by the solver at each time step\r\n\r\n        Calculate acceleration based on the net component2_value.\r\n        \"\"\"\r\n        acceleration = 1.0 * self._component2.get_value()\r\n        derivatives = {\r\n            \"position1\": self.state[\"velocity1\"],\r\n            \"velocity1\": acceleration,\r\n        }\r\n        return derivatives\r\n\r\n\r\nclass Component2(npsolve.Partial):\r\n    def __init__(self):\r\n        super().__init__()  # Don't forget to call this!\r\n        self.add_var(\"component2_value\", init=-0.1)\r\n\r\n    def get_value(self):\r\n        \"\"\"Returns a value\r\n        \r\n        In this example, it is just a state variable, but it could be much\r\n        more complex.\r\n        \"\"\"\r\n        return self.state['component2_value']\r\n\r\n    def connect(self, component1, reverse=True):\r\n        \"\"\"Connect with a Component1 instance\"\"\"\r\n        self._component1 = component1\r\n        if reverse:\r\n            component1.connect(self, reverse=False)\r\n\r\n    def calculate(self, t):\r\n        \"\"\"Some arbitrary calculations based on current time t\r\n        and the position at that time calculated in Component1.\r\n        This returns a derivative for variable 'c'\r\n        \"\"\"\r\n        dc = 1.0 * np.cos(2 * t) * self._component1.get_position()\r\n        derivatives = {\"component2_value\": dc}\r\n        return derivatives\r\n\r\n    def step(self, state_dct, t, *args):\r\n        \"\"\"Called by the solver at each time step\"\"\"\r\n        return self.calculate(t)\r\n\r\n```\r\n\r\nBefore we run the solver, we just need to inject the dependency by calling\r\nthe 'connect' methods we've created. So, now our run function becomes:\r\n\r\n```python\r\n\r\ndef run():\r\n    solver = Solver()\r\n    component1 = Component1()\r\n    component2 = Component2()\r\n    component1.connect(component2)  # Inject the dependency\r\n    component2.connect(component1)  # Inject the dependency\r\n    partials = [component1, component2]\r\n    solver.connect_partials(partials)\r\n    res = solver.solve()\r\n    return res, solver\r\n\r\n```\r\n\r\n\r\n### Nested Partial instances\r\nYou can also nest Partial instances. Under the hood, `connect_partials` passes\r\nthe Solver to the `connect_solver` method of each Partial instance. Just\r\noverwrite the parent Partial instance's `connect_solver` method to pass\r\nthe solver instance on to the `connect_solver` method on the children.\r\n\r\n\r\n## Tutorials\r\n\r\nCheck out the tutorials in the examples folder to learn the basics and \r\nlearn about some more advanced features like the Solver class, the Timeseries\r\nclass, caching, and logging extra values.\r\n",
    "bugtrack_url": null,
    "license": "",
    "summary": "Easier object-oriented calculations for numerical solvers.",
    "version": "0.2.0",
    "project_urls": {
        "Documentation": "https://npsolve.readthedocs.io/en/latest/",
        "Download": "https://github.com/pythoro/npsolve/archive/v0.2.0.zip",
        "Homepage": "https://github.com/pythoro/npsolve.git",
        "Source": "https://github.com/pythoro/npsolve.git",
        "Tracker": "https://github.com/pythoro/npsolve/issues"
    },
    "split_keywords": [
        "numerical solver",
        "numpy",
        "scipy",
        "ode",
        "integration"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "8062737835398abda4846e4922f02571d1461c496022ce0eed99719e97987ae8",
                "md5": "c6d698700c133aa5e16d0fcca74ace63",
                "sha256": "c7042d0fe446e8bd4c77eb347e474fc6077a326b1178c0781ee81648873f497c"
            },
            "downloads": -1,
            "filename": "npsolve-0.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "c6d698700c133aa5e16d0fcca74ace63",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 23383,
            "upload_time": "2024-03-16T06:54:05",
            "upload_time_iso_8601": "2024-03-16T06:54:05.890835Z",
            "url": "https://files.pythonhosted.org/packages/80/62/737835398abda4846e4922f02571d1461c496022ce0eed99719e97987ae8/npsolve-0.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-16 06:54:05",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "pythoro",
    "github_project": "npsolve",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "requirements": [],
    "lcname": "npsolve"
}
        
Elapsed time: 0.21116s