Welcome to npsolve’s documentation!¶
The npsolve package lets you use object-oriented classes and methods for calculations with numerical solvers.
Instead of defining equations, you can write methods with any combination of logic and equations. It decouples the calculations from the machinery to numerically iterate with them, giving you all the benefits you’d expect from objects.
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.
Tutorials¶
The fastest way to learn npsolve is to work through some simple examples. These short tutorials walk through some simple examples, to help learn how to use npsolve.
Tutorial 1 - Basics¶
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.
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.
import numpy as np
import npsolve
class Component1(npsolve.Partial):
def __init__(self):
super().__init__() # Don't forget to call this!
self.add_var('position', init=0.1)
self.add_var('velocity', init=0.3)
class Component2(npsolve.Partial):
def __init__(self):
super().__init__() # Don't forget to call this!
self.add_var('force', 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:
class Component1(npsolve.Partial):
def __init__(self):
super().__init__() # Don't forget to call this!
self.add_var('position', init=0.1)
self.add_var('velocity', init=0.3)
def step(self, state_dct, t, *args):
""" Called by the solver at each time step
Calculate acceleration based on the net force.
"""
acceleration = 1.0 * self.state['force']
derivatives = {'position': self.state['velocity'],
'velocity': acceleration}
return derivatives
class Component2(npsolve.Partial):
def __init__(self):
super().__init__() # Don't forget to call this!
self.add_var('force', 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['position']
derivatives = {'force': 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. Here’s what it looks like:
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()
method of the Solver. They’ll link up
automatically through fastwire.
def run():
solver = Solver()
partials = [Component1(), Component2()]
solver.connect(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 us the right columns.
import matplotlib.pyplot as plt
def plot(res, s):
slices = s.npsolve_slices
plt.plot(s.t_vec, res[:,slices['position']], label='position')
plt.plot(s.t_vec, res[:,slices['velocity']], label='velocity')
plt.plot(s.t_vec, res[:,slices['force']], label='force')
plt.legend()
Now let’s run it!
res, s = run()
plot(res, s)

Tutorial 2 - Handling discontinuities¶
Often, real-world integration problems in engineering have discontinuities. That means that the physics cannot be described adequately by a single set of equations.
Fortunately, npsolve provides a soft_functions module to make it easy to handle discontinuities, by preventing them entirely. These functions work by providing a differential that changes smoothly over a very, very small time, distance, or whatever value it’s applied to. Variable time step solvers, such many in in scipy.integrate, handle these very small but smooth transitions easily. The approximation of these functions inevitably introduces small errors, but for many real-world problems these errors are negligible. It’s far more important to get a 99.999% accurate result than none at all.
We’ll illustrate the use of two soft_functions to model a bouncing ball. It’s going to start rolling along a ledge, before falling off onto a surface that it bounces on. Let’s start with some setup:
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
import npsolve
from npsolve.soft_functions import negdiff, below
G = -9.80665
Y_SURFACE = 1.5
X_LEDGE = 2.0
Y_LEDGE = 5.0
Now we’ll start writing our Ball class. We’ll give it some mass, an initial x-velocity, and some parameters to control how it bounces. We’ll create the bouncing by a combination of spring-like behaviour and velocity-dependent damping. Here’s the constructor:
class Ball(npsolve.Partial):
def __init__(self, mass=1.0,
k_bounce=1e7,
c_bounce=3e3,
initial_vel=5.0):
super().__init__() # Don't forget to call this!
self.mass = mass
self.k_bounce = k_bounce
self.c_bounce = c_bounce
self.add_var('position', init=np.array([0.0, Y_LEDGE]))
self.add_var('velocity', init=np.array([initial_vel, 0.0]))
Notice here that the position and velocity variables are 1D numpy ndarrays of size 2, to reflect x and y axes. npsolve handles variables of 1D arrays!
Let’s create a method in the Ball class to calculate the force of gravity on the ball.
class Ball():
# ...
def F_gravity(self):
""" Force of gravity """
return np.array([0, self.mass * G])
Pretty simple so far. Now let’s make our ledge react the force of gravity until the ball reaches the edge of the ledge with another method:
class Ball():
# ...
def F_ledge(self):
x_pos = self.state['position'][0]
return -self.F_gravity() * below(x_pos, limit=X_LEDGE)
Here, we’re using the below soft function. It’s 1 below a limit (here X_LEDGE) and 0 above it, with a very small but smooth transition around the limit. So, this force will only apply until the ball reaches the edge of the ledge. We do this so the differentials are continuously smooth, so the scipy integrator won’t have any trouble with discontinuities.
Now, let’s make the method to generate a bouncing force when it hits the surface. This one is a bit more complex:
class Ball():
# ...
def F_bounce(self):
""" Force bouncing on the surface """
y_pos = self.state['position'][1]
y_vel = self.state['velocity'][1]
F_spring = -self.k_bounce * negdiff(y_pos, limit=Y_SURFACE)
c_damping = -self.c_bounce * below(y_pos, Y_SURFACE)
F_damping = c_damping * negdiff(y_vel, limit=0)
return np.array([0, F_spring + F_damping])
We’re using the negdiff soft function here. It gives the difference between a value and some limit when the value is below the limit and 0 when it’s above. Again, it’s a smooth function with a very small transition region around the limit. We apply that to the spring force so it only provides that force when the ball is (slightly) below the surface.
For the damping, we’ve used the below function to set up a damping coefficient, c_damping, that will only apply when the ball is (slightly) below the surface. We also want the damping force to only push the ball - we don’t want it to ‘pull’ the ball like a sticky surface might. So, we’ve used the negdiff function on the velocity. When the velocity is positive, the negdiff function goes to 0 so the damping force will be 0 too. We add the two forces to return a bounce force.
Now we can set up the step method that gets called by the integrator.
class Ball():
# ...
def step(self, state_dct, t, *args):
""" Called by the solver at each time step """
F_net = self.F_gravity() + self.F_ledge() + self.F_bounce()
acceleration = F_net / self.mass
derivatives = {'position': self.state['velocity'],
'velocity': acceleration}
return derivatives
This just sums the forces, calculates the acceleration by applying elementary physics, then returns the acceleration.
Now let’s set up the Solver:
class Solver(npsolve.Solver):
def solve(self, t_end=3.0, n=100001):
self.npsolve_init() # Initialise
t_vec = np.linspace(0, t_end, n)
solution = odeint(self.step, self.npsolve_initial_values, t_vec)
dct = self.as_dct(solution)
dct['time'] = t_vec
return dct
This is very much like the solver in the quickstart example. Here though, we’re using the as_dct method to convert the outputs to a dictionary, in which each key is variable name, and each value is an array of the values through time.
Let’s set up a function to run it and plot the results.
def run(partials, t_end=3.0, n=100001):
solver = Solver()
solver.connect(partials)
return solver.solve(t_end=t_end, n=n)
def plot(dct):
plt.plot(dct['position'][:,0], dct['position'][:,1], label='position')
plt.axhline(Y_SURFACE, c='r')
plt.plot([0, X_LEDGE], [Y_LEDGE, Y_LEDGE], 'r:')
plt.ylim(0, 6)
plt.xlabel('x')
plt.ylabel('y')
Here, we’re making the run method a bit more generic. It’s going to take a list of Partial instances, connect them to the solver, call the solve method, then return the result and the partials. Now we can run it!
ball = Ball()
dct = run(ball)
plot(dct)
We’ve made a bouncing ball!

If we zoom way in on the bounce, you can see it’s actually smooth.

Tutorial 3 - Timeseries input¶
We might have inputs we want to use that aren’t functions. They might be measured timeseries data, for example, such as position over time. Or, they could be hypothetical data. For this tutorial, we’re just use some random numbers to make a particle move about.
First, the setup:
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
import npsolve
from npsolve.utils import Timeseries
from tutorial_2 import run
Notice, we’re going to reuse the Solver and run function we set up in Tutorial 2, but for a completely different model.
Now let’s start to write a Particle class:
class Particle(npsolve.Partial):
def __init__(self):
super().__init__() # Don't forget to call this!
self.time_points = np.linspace(0, 1, 51)
np.random.seed(0)
self.positions = np.random.rand(51, 2) * 10
self.xts = Timeseries(self.time_points, self.positions[:,0])
self.yts = Timeseries(self.time_points, self.positions[:,1])
self.add_var('position', init=self.positions[0,:])
We’re creating some timeseries data in the time_points and positions attributes. Then, for each x or y axis, we’re creating a Timeseries object. We pass in the x values (the time points) and the values at those times (positions). The Timeseries class takes care of the rest, and will smoothly intepolate between those points using a spline. Notice also that we’re setting the initial values of the position variable to the first pair of (x,y) values in the positions array.
Now, let’s write the step method.
class Particle(npsolve.Partial):
# ...
def step(self, state_dct, t, *args):
''' Called by the solver at each time step
Calculate acceleration based on the
'''
velocity = np.array([self.xts(t, der=1), self.yts(t, der=1)])
derivatives = {'position': velocity}
return derivatives
We’re getting the velocity in each axis by simply calling the Timeseries instances we set up in the constructor. We pass them in the current x value (in this case the time), and the derivative we want (der). In this case, we want the first derivative of the position, which is time.
We can reuse our run method and Solver from Tutorial 2. We’ll set up some plot functions though:
def plot(dct, particle):
plt.plot(dct['position'][:,0], dct['position'][:,1], linewidth=0.5)
plt.scatter(particle.positions[:,0], particle.positions[:,1], c='r',
marker='.')
plt.xlabel('x')
plt.ylabel('y')
def plot_vs_time(dct, particle):
fig, axes = plt.subplots(2, 1, sharex=True)
axes[0].plot(dct['time'], dct['position'][:,0], linewidth=0.5)
axes[0].scatter(particle.time_points, particle.positions[:,0], c='r',
marker='.')
axes[0].set_xlabel('time')
axes[0].set_ylabel('x')
axes[1].plot(dct['time'], dct['position'][:,1], linewidth=0.5)
axes[1].scatter(particle.time_points, particle.positions[:,1], c='r',
marker='.')
axes[1].set_xlabel('time')
axes[1].set_ylabel('y')
Now everything’s set. Let’s run it!
particle = Particle()
dct = run([particle], t_end=1.0)
plot(dct, particle)
plot_vs_time(dct, particle)
Here’s how our particle has moved…

And we can see how the Timeseries instances have controlled the velocity, and hence position, over time.

In real-world models, you’ll probably want to use the Timeseries classes together with other model equations and logic.
- Tip:
- You can make a subclass of a Partial instance and overwrite the step method, so that the derivatives are set by Timeseries classes. Then, you can easily switch between the original Partial instance and one in which one or more derivatives are set by timeseries data!
Tutorial 4 - Sharing values between objects¶
In more complex models, values often need to be shared between different Partial instances. Sometimes, those values are state variables that are declared with the add_var method. In that case, any Partial connected to the Solver will get access to them through their state dictionary (i.e. self.state[<variable name>]). But some shared values are not state variables. How do we handle that?
We use the fastwire package. It provides a convenient, event-like way to share variables. This tutorial will give an example. We’re going to simulate the dynamics of a frictionless slider moving in the x axis with a pendulum attached that is free to move in the x and y axes. We’ll add a sinusoidal force to the slider to excite the system dynamics.
First, a little setup:
import numpy as np
import matplotlib.pyplot as plt
import npsolve
from tutorial_2 import run
G = np.array([0, -9.80665])
Here, we’ve made G a 2D vector to represent gravity.
Now we’ll get a wire box. A wire box is a collection of wires that we’ll use to pass values. Here’s how we do that:
import fastwire as fw
wire_box = fw.get_wire_box('demo')
You can use wire_box = fw.get_wire_box(‘demo’) in any code module and it’ll get the same wire box, so you don’t have to import that object into other modules.
Now let’s start making the Slider.
class Slider(npsolve.Partial, fw.Wired):
def __init__(self, freq=1.0, mass=1.0):
super().__init__() # Don't forget to call this!
self.freq = freq
self.mass = mass
self.add_var('s_pos', init=np.zeros(2))
self.add_var('s_vel', init=np.zeros(2))
Importantly, we’re inheriting the fw.Wired class. That lets us use fastwire decorators. We’re also making the Slider fully 2D, even though at this stage we only want it to move in x.
We’re doing to connect the Pendulum to the Slider, and the Pendulum will need to know where the Slider is so it can pivot about the right point. Here’s how we make the pivot location and velocity available to the Pendulum:
class Slider(npsolve.Partial, fw.Wired):
# ...
@wire_box.supply('pivot')
def pivot(self, t):
""" The location of the pivot that connects to the pendulum """
return self.state['s_pos'], self.state['s_vel']
We decorate the method with @wire_box.supply(‘pivot’) because we’ve called our wire box wire_box. This tells fastwire that this method supplies the values referred to by the wire called ‘pivot’. We’ll pass in the current time, t, although we don’t need it yet.
Let’s set up a method to create the excitation force:
class Slider(npsolve.Partial, fw.Wired):
# ...
def F_sinusoid(self, t):
""" The force to make the system do something """
return 10 * np.cos(2 * np.pi * (self.freq * t))
Now we can write our step method to return the state derivatives by doing some basic physics.
class Slider(npsolve.Partial, fw.Wired):
# ...
def step(self, state_dct, t, *args):
""" Called by the solver at each time step """
F_pivot = -wire_box['F_pivot'].fetch(t)
F_pivot_x = F_pivot[0]
F_sinusoid_x = self.F_sinusoid(t)
F_net_x = F_pivot_x + F_sinusoid_x
acc = np.array([F_net_x / self.mass, 0])
derivatives = {'s_pos': state_dct['s_vel'],
's_vel': acc}
return derivatives
Notice here we’re going to pull in a force, F_pivot, which is going to be calculated by the Pendulum class. We just have to use the fetch method on the right wire, which here we’ve called F_pivot. For this example, we’ll also pass in the current time ‘t’ to the method that will supply that force (we haven’t written that method yet). We’re flipping the sign because the slider will see the reaction force.
Now, let’s make the Pendulum class.
class Pendulum(npsolve.Partial, fw.Wired):
def __init__(self, mass=1.0, k=1e6, c=1e4, l=1.0):
super().__init__() # Don't forget to call this!
self.mass = mass
self.k = k
self.c = c
self.l = l
self.add_var('p_pos', init=np.array([0, -self.l]))
self.add_var('p_vel', init=np.array([0, 0]))
Again, we’re inheriting fw.Wired. This class has some stiffness (k) and damping c parameters, along with mass (mass) and length (l). It needs to calculate the force that arises because of it’s connection to the Slider. We’re going to model a very stiff, damped connection between the pivot on the Slider and the position of the Pendulum.
class Pendulum(npsolve.Partial, fw.Wired):
# ...
@wire_box.supply('F_pivot')
@npsolve.mono_cached()
def F_pivot(self, t):
""" Work out the force on the pendulum mass """
pivot_pos, pivot_vel = wire_box['pivot'].fetch(t)
rel_pos = pivot_pos - self.state['p_pos']
rel_vel = pivot_vel - self.state['p_vel']
dist = np.linalg.norm(rel_pos)
unit_vec = rel_pos / dist
F_spring = self.k * (dist - self.l) * unit_vec
rel_vel_in_line = np.dot(rel_vel, unit_vec)
F_damping = self.c * rel_vel_in_line * unit_vec
return F_spring + F_damping
We’re again using the @wire_box decorator so that this method will supply the F_pivot wire. The return value, the force at the pivot, will be used by both the Slider (via the F_pivot wire) and the Pendulum (directly). We can’t assume which object will call the F_pivot method first, but we don’t want to have it calculate the result twice. (This is a simple example, but in computationally intensive calculations, reducing calculations can be important.) So, we use the @npsolve.mono_cached() decorator here as well. This caches the result for the current timestep. Subsequent calls simply return that value. The mono_cached() doesn’t care about the value of arguments. If they might change for the same timestep, you can use the multi_cached() decorator instead.
Let’s add the force of gravity now:
class Pendulum(npsolve.Partial, fw.Wired):
# ...
def F_gravity(self):
return self.mass * G
Finally, we’ll make the step method, doing some basic physics to calculate acceleration.
class Pendulum(npsolve.Partial, fw.Wired):
# ...
def step(self, state_dct, t, *args):
''' Called by the solver at each time step
Calculate acceleration based on the
'''
F_net = self.F_pivot(t) + self.F_gravity()
acceleration = F_net / self.mass
derivatives = {'p_pos': state_dct['p_vel'],
'p_vel': acceleration}
return derivatives
Before we run, let’s make some plot functions…
def plot_xs(dct):
plt.plot(dct['time'], dct['s_pos'][:,0], label='slider')
plt.plot(dct['time'], dct['p_pos'][:,0], label='pendulum')
plt.xlabel('time')
plt.ylabel('x')
plt.legend(loc=3)
def plot_trajectories(dct):
plt.plot(dct['s_pos'][:,0], dct['s_pos'][:,1], label='slider')
plt.plot(dct['p_pos'][:,0], dct['p_pos'][:,1], label='pendulum')
plt.xlabel('x')
plt.ylabel('y')
plt.xlim(-1.5, 1.5)
plt.ylim(-1.2, 1.2)
plt.gca().set_aspect('equal')
plt.legend(loc=2)
Finally, we’ll make a little function to run the model and plot the results.
def execute(freq):
partials = [Slider(freq=freq), Pendulum()]
dct = run(partials, t_end=10.0, n=10001)
plot_xs(dct)
plot_trajectories(dct)
Let’s see what happens at 2 Hz:
execute(2.0)


Nothing very interesting. Both objects just oscillate, as you might expect. Now let’s try at 1 Hz:
execute(1.0)


The Pendulum is wobbling around a bit more now. Let’s try at 0.5 Hz:
execute(1.0)

When we look a the trajectories, we see what’s really happening…

Remember that our pendulum isn’t quite a rigid body - we’ve approximated it as a very stiff, highly damped spring. We should check that the approximation is good by checking that the distance between the pivot and pendulum is very very close to 1.0. Let’s plot the distance:
def plot_distance_check(dct):
diff = dct['p_pos'] - dct['s_pos']
dist = np.linalg.norm(diff, axis=1)
plt.plot(dct['time'], dist)
plt.xlabel('time')
plt.ylabel('length')
plot_distance_check(dct)

Our maximum length error is only 0.0001, compared to our pendulum length of 1.0, so we know the errors due to that approximation will be small.
Tutorial 5 - Using polymorphism¶
In Tutorial 4, we made a Pendulum move under a Slider. What if that pendulum moved under the Particle we made in Tutorial 3 instead? Let’s find out, to demonstrate how npsolve makes it easy to do so.
We’ll start by importing what we need:
import numpy as np
import matplotlib.pyplot as plt
from tutorial_2 import run as t2_run
from tutorial_3 import Particle
from tutorial_4 import Pendulum
import fastwire as fw
wire_box = fw.get_wire_box('demo')
We need to add a pivot method to the Particle, so we’ll subclass it like this.
class Particle2(Particle, fw.Wired):
@wire_box.supply('pivot')
def pivot(self, t):
velocity = np.array([self.xts(t, der=1), self.yts(t, der=1)])
return self.state['position'], velocity
That’s it - now this class will substitute for the old Slider class! We’re returning the position and velocity in the same format that the Pendulum expected from the Slider, and we’ve wired it to the same ‘pivot’ wire.
There’s one small glitch - initial conditions. In Tutorial 4, we conveniently set up the initial conditions right. How do we do that now? Here’s how:
def set_init_condition(particle, pendulum):
init_particle_pos = particle.npsolve_vars['position']['init']
init_pendulum_pos = init_particle_pos - np.array([0.0, 1.0])
pendulum.set_init('p_pos', init_pendulum_pos)
When a class calls add_var, that information gets added to the npsolve_vars attribute. We’re taking that, subtracting our pendulum length from the height, and then calling the set_init method to set the initial position of the pendulum to that position. Easy.
Now we’ll make a function to run the new model.
def run(k=1e6, c=1e4):
particle = Particle2()
pendulum = Pendulum(k=k, c=c)
set_init_condition(particle, pendulum)
partials = [particle, pendulum]
dct = t2_run(partials, t_end=1.0, n=10001)
return dct
And a new plot function to see the results.
def plot_trajectories(dct):
plt.plot(dct['position'][:,0], dct['position'][:,1], linewidth=1.0,
label='particle')
plt.plot(dct['p_pos'][:,0], dct['p_pos'][:,1], linewidth=1.0,
label='pendulum')
plt.xlabel('x')
plt.ylabel('y')
plt.xlim(-2.5, 12.5)
plt.ylim(-2.5, 12.5)
plt.gca().set_aspect('equal')
plt.legend(loc=2)
Let’s run it!
dct = run()
plot_trajectories(dct)

Our pendulum is now hurtling around with a particle!
Let’s check the pendulum length again to ensure it’s behaving as expected.
def plot_distance_check(dct):
diff = dct['p_pos'] - dct['position']
dist = np.linalg.norm(diff, axis=1)
plt.plot(dct['time'], dist)
plt.xlabel('time')
plt.ylabel('length')
plot_distance_check(dct)

Here, our stiff spring and firm damping aren’t quite enough to handle the fast accelerations due to the particle motion. So, we’ll tweak our parameters and run again:
dct = run(k=1e9, c=1e7)
plot_trajectories(dct)
plot_distance_check(dct)

Our Pendulum trajectory is different.

Now, our distance check looks ok, so we can be more confident with this result - as crazy as it is!
Think about what this lets us do. We might write classes for a given situation. Then, say if we run an experiement and get some measured data, we can swap the relevant Partial for one that uses that Timeseries data. Or, perhaps we have a new idea to test - we can easily swap out that part of the model and compare it back to back with the first.
We can validate our classes against unittests, theory, and experimental data. Then, we can run new models that use them without changing anything within those classes. This can provide confidence that we haven’t made any mistakes within those classes in the new model.
Tutorial 6 - Logging variables and stopping¶
We often want to output non-state values during calculation. We may want to end the calculation when a condition is met. Fortunately, these are both easy to do both with npsolve.
npsolve provides a special Integrator class to help you do these. By default, it uses the LSODA integrator in scipy (if scipy is present). However, it’s built to use any set of integrators that work like scipy’s ode. It works by breaking up the time domain into short frames. It uses the integrator to integrate between each frame as normal. scipy’s integrators are stateful, so they can continue with the next frame with minimal overhead.
At each frame, the Integrator class sets a special status flag, which can be anywhere else in the code. When this flag (npsolve.solvers.Final) is set, it means the state values for that time are the ‘final’ (not guesses by the variable-time-step solver). Partial classes can then add values to be logged. The solver also listens to a flag to stop the integration at that point that can be set from anywhere else in the code.
Here’s an example. Let’s change the step method of the Pendulum class in Tutorial 4 to add some logging and raise a stop flag. We’ll first do some imports:
import matplotlib.pyplot as plt
import npsolve
from tutorial_4 import Slider, Pendulum
Now let’s set up our status dictionary and our logger.
from npsolve.solvers import FINAL, STOP
status = npsolve.get_status('demos_status')
logger = npsolve.get_logger('demos_logger')
We’ll use the FINAL and STOP flags with the status dictionary.
Now let’s modify the stop method of the Pendulum class so it looks like this:
class Pendulum2(Pendulum):
def step(self, state_dct, t, *args):
''' Called by the solver at each time step
Calculate acceleration based on the
'''
F_pivot = self.F_pivot(t)
F_gravity = self.F_gravity()
F_net = F_pivot + F_gravity
acceleration = F_net / self.mass
if status[FINAL]:
logger['F_pivot'].append(F_pivot)
logger['acceleration'].append(acceleration)
if F_pivot[1] > 90.0:
status[STOP] = True
derivatives = {'p_pos': state_dct['p_vel'],
'p_vel': acceleration}
return derivatives
We’ve added some logging under if status[FINAL]: and raised a stop flag under if F_pivot[1] > 90.0:.
We’ll use the Integrator class to solve this, since it knows how to use those flags. We’ll write a new run method to use it.
def run(partials, t_end=20.0, n=100001):
solver = npsolve.solvers.Integrator(status=status,
logger=logger,
framerate=n//t_end)
solver.connect(partials)
return solver.run(t_end)
Now some plotting functions:
def plot_F_pivot(dct):
plt.figure()
plt.plot(dct['time'], dct['F_pivot'][:,0], label='F_pivot_x')
plt.plot(dct['time'], dct['F_pivot'][:,1], label='F_pivot_y')
plt.xlabel('time')
plt.ylabel('x')
plt.legend(loc=3)
def plot_acc(dct):
plt.figure()
plt.plot(dct['time'], dct['p_vel'][:,0], label='x_velocity')
plt.plot(dct['time'], dct['acceleration'][:,0], label='x_acceleration')
plt.xlabel('time')
plt.ylabel('x')
plt.legend(loc=3)
Finally, we’ll execute the new code:
def execute(freq):
partials = [Slider(freq=freq), Pendulum2()]
dct = run(partials, t_end=20.0, n=10001)
plot_F_pivot(dct)
plot_acc(dct)
execute(freq=0.5)


It’s as easy as that. Notice first that the integrator has stopped early because the Pendulum2 instance raised a status[STOP] = True flag.
Our logged outputs are now in the output dictionary along with our state variables, which makes it easy to work with them. The logging is controlled by the Partial instances, and we don’t have to change anything else in our code. As a bonus, the Pendulum2 class will still work as normal with the original solver in Tutorial 2 - the logging and stopping just won’t work with it because that solver doesn’t use them.
- Note:
- Be sure that logged variables are logged only once per time step, since otherwise the outputs won’t match up right. In tricky situations, you can use the @npsolve.mono_cached() decorator to do that, since it will only execute the code inside the function once per time step.
Package documentation¶
npsolve.core module¶
Created on Mon Aug 5 14:34:54 2019
@author: Reuben
Npsolve has a simple, small core built on fastwire. It’s designed to give good flexibility without compromising on performance.
-
class
npsolve.core.
Partial
¶ Bases:
object
A base class responsible for a set of variables
Note
__init__ method must be called.
-
add_var
(name, init, safe=True, live=True, **kwargs)¶ Add a new variable
Parameters: - name (str) – The variable name
- init (array-like) – The initial value(s). Can be a scalar or 1D ndarray.
- **kwargs – Optional kew word attributes for the variable.
-
add_vars
(dct)¶ Add multiple variables
Parameters: dct (dict) – A dictionary in which keys are variable names and values are dictionaries with name, initial value, etc.
-
cache_clear
()¶ Clear the cache for all cached methods
-
clear_vars
()¶
-
connect
(cid=None)¶ Connect this instance to the Solver instance
Parameters: - cid (int) – The container id provided the setup_signals method
- the Solver instance. (of) –
-
enable_caching
()¶ Enable or
-
get_init
(name)¶ Get the initial value for a variable
Parameters: name (str) – The variable name
-
set_init
(name, init)¶ Set the initial value for a variable
Parameters: - name (str) – The variable name
- init (array-like) – The initial value(s). Can be a scalar or 1D ndarray.
-
set_meta
(name, **kwargs)¶ Set meta information for a variable
Parameters: **kwargs – Key word attributes for the variable.
-
set_vectors
(state_dct, ret_dct)¶ Override to set up views of the state vector
Parameters: - state_dct (dict) – A dictionary of numpy array views for the state
- all variables. Provided by the Solver. (of) –
- ret_dct (dict) – A similar dictionary of return values. Not
- used. (usually) –
-
step
(state_dct, *args)¶
-
-
class
npsolve.core.
Solver
¶ Bases:
object
The solver that pulls together the partials and allows solving
-
as_dct
(sol)¶ Split out solution array into dictionary of values
Parameters: sol (ndarray) – A 1D or 2D array where columns correspond to state values This convenience method splits out a 2D array into a dictionary of vectors or arrays, with variables as keys.
-
close_signals
()¶ Deactive the signal container
Note
If autoconnecting is enabled, other Partials may connect to the Solver if the container is active.
-
connect
(partials)¶ Connect a dict or list of partials to the Solver instance
Parameters: partials (list, dict, Partial) – A list or dictionary of Partial instances.
-
fetch_partials
()¶ Fetch a dictionary of all connected Partial instances
-
freeze
()¶ Give static copies of vectors to connected Partial instances
Warning
This will prevent the ‘step’ methods from being able to update the values.
-
get_state_dct
(squeeze=True, unitise=True)¶
-
npsolve_finish
()¶ Tidy up after a round of solving
-
npsolve_init
(pinned=None)¶ Initialise the Partials and be ready to solve
Parameters: pinned (dict) – A dictionary of variable-value pairs to hold constant during stepping.
-
one_way_step
(vec, *args, **kwargs)¶ The method to be called every iteration with no return val
Parameters: - vec (ndarray) – The state vector (passed in by the solver)
- args – Optional arguments passed to step method in each Partial.
- kwargs – Optional keyword arguments for each step method call.
Returns: None
- Note: This method relies on other methods being used to inform the
- solver during its iteration.
-
pinned
(dct)¶ A context manager that unpins all variables on exit
-
remove_signals
()¶ Remove the signal container
Note
This helps to keep the signal box tidy.
-
setup_signals
()¶ Setup the fastwire signals that Partial instances will require
Returns: The container id for the signals. Return type: int
-
step
(vec, *args, **kwargs)¶ The method to be called every iteration by the numerical solver
Parameters: - vec (ndarray) – The state vector (passed in by the solver)
- args – Optional arguments passed to step method in each Partial.
- kwargs – Optional keyword arguments for each step method call.
Returns: - A dictionary containing keys for each variable. The values
must match the shape of the state. These will often contain derivatives for integration problems and error or cost values for optimisation problems.
Return type: dict
-
tstep
(t, vec, *args, **kwargs)¶ The method to be called every iteration by the numerical solver
Parameters: - vec (ndarray) – The state vector (passed in by the solver)
- args – Optional arguments passed to step method in each Partial.
- kwargs – Optional keyword arguments for each step method call.
Returns: - A dictionary containing keys for each variable. The values
must match the shape of the state. These will often contain derivatives for integration problems and error or cost values for optimisation problems.
Return type: dict
Note
This method is similar ot the
step()
method, but is used where a time value is passed as the first argument.
-
unfreeze
(state=None)¶ Give ‘live’ vectors to connected Partial instances
Parameters: state (ndarray) – An optional vector to initialise the state.
-
npsolve.cache module¶
Created on Wed Aug 7 07:06:54 2019
@author: Reuben
Simple caching inspired by functools.lru_cache.
Sometimes, Partial instances may need to call each other (via a fastwire fetch is a good method). Caching allows a way to reuse the computations for each step if needed, to avoid having to double-up in those cases.
-
npsolve.cache.
mono_cached
()¶ A cache method that only considers the ‘self’ argument
This works very similar to multi-cache but doesn’t use the make_key function from functools to save a little bit of time.
-
npsolve.cache.
multi_cached
()¶ A cache method that considers arguments
npsolve.soft_functions module¶
Created on Fri May 15 16:17:04 2020
@author: Reuben
These functions can be used to prevent discontinuities, which can cause trouble for numerical methods.
-
npsolve.soft_functions.
above
(x, limit=0.0, scale=0.0001)¶ A smooth step from 0 below a limit to 1 above it
Parameters: - x (int, float, ndarray) – The value(s)
- limit (float) – [OPTIONAL] The value to step at. Defaults to 0.
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: Value(s) between 0 and 1
Return type: float, ndarray
See also
soft_step
-
npsolve.soft_functions.
below
(x, limit=0.0, scale=0.0001)¶ A smooth step from 1 below a limit to 0 above it
Parameters: - x (int, float, ndarray) – The value(s)
- limit (float) – [OPTIONAL] The value to step at. Defaults to 0.
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: Value(s) between 0 and 1
Return type: float, ndarray
See also
soft_step
-
npsolve.soft_functions.
ceil
(x, limit=0.0, scale=0.0001)¶ Limit value to a maximum softly to to prevent discontinuous gradient
Parameters: - x (int, float, ndarray) – The value(s) to soft limit
- limit (float) – [OPTIONAL] The value to limit at. Defaults to 0.
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: The limited value(s)
Return type: float, ndarray
See also
soft_limit
-
npsolve.soft_functions.
clip
(x, lower, upper, scale=0.0001)¶ Limit value to a range softly to to prevent discontinuous gradient
Parameters: - x (int, float, ndarray) – The value(s) to soft limit
- lower (float) – The lower threshold
- upper (float) – The upper threshold
- scale – A scale factor for the softening
Returns: The limited value(s)
Return type: float, ndarray
See also
soft_limit
-
npsolve.soft_functions.
floor
(x, limit=0.0, scale=0.0001)¶ Limit value to a minimum softly to to prevent discontinuous gradient
Parameters: - x (int, float, ndarray) – The value(s) to soft limit
- limit (float) – [OPTIONAL] The value to limit at. Defaults to 0.
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: The limited value(s)
Return type: float, ndarray
See also
soft_limit
-
npsolve.soft_functions.
gaussian
(x, center=0.0, scale=0.0001)¶ A gaussian function, with a peak of 1.0
Parameters: - x (int, float, ndarray) – The value(s)
- center (float) – [OPTIONAL] The x-position of the peak center
- scale (float) – [OPTIONAL] A scale factor for the curve.
-
npsolve.soft_functions.
lim
(x, limit=0.0, side=1, scale=0.0001)¶ Limit the value softly to prevent discontinuous gradient
Parameters: - x (int, float, ndarray) – The value(s) to soft limit
- limit (float) – [OPTIONAL] The value to limit at. Defaults to 0.
- side (int) – [OPTIONAL] 1 for min, -1 for max. Defaults to 1.
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: The limited value(s)
Return type: float, ndarray
Note
This function uses a softplus function to perform smoothing. See https://en.wikipedia.org/wiki/Activation_function. Values for the calculation are clipped to 700 avoid overflow errors, as the max value for a float is exp(709.782).
-
npsolve.soft_functions.
negdiff
(x, limit=0.0, scale=0.0001)¶ Negative-only difference (difference below limit to 0 above limit)
Parameters: - x (int, float, ndarray) – The value(s) to soft limit
- limit (float) – [OPTIONAL] The value to limit at. Defaults to 0.
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: The limited value(s)
Return type: float, ndarray
See also
soft_limit
-
npsolve.soft_functions.
outside
(x, lower, upper, scale=0.0001)¶ Steps smoothly from 1 outside a range to 0 inside it
Parameters: - x (int, float, ndarray) – The value(s)
- lower (float) – The lower threshold
- upper (float) – The upper threshold
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: Value(s) between 0 and 1
Return type: float, ndarray
See also
soft_step
-
npsolve.soft_functions.
posdiff
(x, limit=0.0, scale=0.0001)¶ Positive-only difference (0 below limit to difference above limit)
Parameters: - x (int, float, ndarray) – The value(s) to soft limit
- limit (float) – [OPTIONAL] The value to limit at. Defaults to 0.
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: The limited value(s)
Return type: float, ndarray
See also
soft_limit
-
npsolve.soft_functions.
sign
(x, scale=0.0001)¶ A smooth step from -1 below 0 to +1 above it
Parameters: - x (int, float, ndarray) – The value(s)
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: Value(s) between 0 and 1
Return type: float, ndarray
Note
This function uses a sigmoid function to perform smoothing. See https://en.wikipedia.org/wiki/Sigmoid_function. Values for the calculation are clipped to avoid overflow errors.
-
npsolve.soft_functions.
step
(x, limit=0.0, side=1, scale=0.0001)¶ A smooth step to prevent discontinuous gradient
Parameters: - x (int, float, ndarray) – The value(s)
- limit (float) – [OPTIONAL] The value to step at. Defaults to 0.
- side (int) – [OPTIONAL] 1 for min, -1 for max. Defaults to 1.
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: Value(s) between 0 and 1
Return type: float, ndarray
Note
This function uses a sigmoid function to perform smoothing. See https://en.wikipedia.org/wiki/Sigmoid_function. Values for the calculation are clipped to avoid overflow errors.
-
npsolve.soft_functions.
within
(x, lower, upper, scale=0.0001)¶ Steps smoothly from 0 outside a range to 1 inside it
Parameters: - x (int, float, ndarray) – The value(s)
- lower (float) – The lower threshold
- upper (float) – The upper threshold
- scale (float) – [OPTIONAL] A scale factor for the softening
Returns: Value(s) between 0 and 1
Return type: float, ndarray
See also
soft_step
npsolve.solvers module¶
Created on Fri May 22 10:36:19 2020
@author: Reuben
This module contains more specialised solvers based on scipy.
-
class
npsolve.solvers.
Integrator
(status, logger, framerate=60.0, interface_cls=None, integrator_name='lsoda', squeeze=True, x_name='time', update_inits=False, **kwargs)¶ Bases:
npsolve.core.Solver
A versatile integrator, with extra logging and stop flag
This integrator allows variables to be logged during the integration, which are then included in the output. In addition, Partial instances can set a flag to stop the integration at any point.
Parameters: - status (defaultdict) – A dictionary that contains status flags. Key flags are npsolve.solvers.FINAL and npsolve.solvers.STOP (which are strings). The default should be a function that returns None. Obtain one by calling npsolve.get_status(<name>).
- logger (defaultdict) – A dictionary in which the default values are lists. Obtain one by calling npsolve.get_solver(<name>).
- framerate (float) – [OPTIONAL] The number of return values per unit x (which is often time). Defaults to 60.0.
- interface_cls (class) – [OPTIONAL] The class of interface to use for integrator algorithms. Defaults to scipy.intergrate.ode if scipy is found.
- integrator_name (str) – [OPTIONAL] The name of the integrator to use. Defaults to ‘lsoda’.
- squeeze (bool) – [OPTIONAL] Ensure output arrays are squeezed. Defaults to True.
- x_name (str) – [OPTIONAL]: The name for the x value, which is logged in the outputs. Defaults to ‘time’.
- update_inits (bool) – [OPTIONAL] Update the initial values of the Partial instances with the solution at the end. Defaults to False.
- keyword arguments (Other) – [OPTIONAL] Are passed to the integrator by the call interface_cls.set_integrator(integrator_name, **kwargs).
Returns: A dictionary of integrated values. The values are ndarrays, which are at the framerate specified by the ‘framerate’ argument.
Return type: dict
- Adding logged variables:
Only log variables when the solver has finalised the current frame. Integrators like scipy’s ode ‘lsoda’ use variable time steps, and take numerous guesses at the state as they jump around in time. Once it has reached an accurate state for the x value at the end of the frame, status[npsolve.solvers.FINAL] is set to True. Only log values when this flat is True. An example:
if status[FINAL]: logger['variable_name_1'] = current_value
- Stopping the integration:
- Stop the integration by setting status[npsolve.solvers.STOP] to True.
-
run
(end, **kwargs)¶ Run the solver
Parameters: end (float) – The end point for the integration. Integration starts from 0 and will end at this value. Often this is a time. Returns: A dictionary where keys are the variable names and other logged names, and the values are ndarrays of the values through time. Return type: dict
npsolve.utils module¶
Created on Sat Sep 14 08:31:52 2019
@author: Reuben
This module contains helper functions and methods that help you get the most from npsolve.
-
class
npsolve.utils.
Dict_Container
¶ Bases:
dict
-
class
npsolve.utils.
List_Container
¶ Bases:
dict
-
class
npsolve.utils.
List_Container_Container
¶ Bases:
dict
-
class
npsolve.utils.
Set_Container
¶ Bases:
dict
-
class
npsolve.utils.
Timeseries
(xs, ys)¶ Bases:
object
A utility class to specify values from time-series data
Parameters: - xs (ndarray) – A 1D array of x values. Must be monotonically increasing.
- ys (ndarray) – A 1D array of y values
- Usage:
The Timeseries class is callable. It interpolates values smoothly between the inputs using splines. It offers the 1st integral to the 3rd derivative of values.
timeseries = Timeseries(xs, ys) # Create timeseries(5) # Value at x=5 timeseries(5, -1) # 1st integral (antiderivative) at x=5 timeseries(5, 0) # Value at x=5 timeseries(5, 1) # 1st derivative at x=5 timeseries(5, 2) # 2nd derivative at x=5 timeseries(5, 3) # 3rd derivative at x=5
Note
Use Timeseries.from_csv to generate values from a csv file.
-
classmethod
from_csv
(fname, x_col=0, y_col=1, skip_header=0, delimiter=', ', **kwargs)¶ Create a Timeseries from csv data
Parameters: - fname (str) – The filename
- x_col (int) – The column index of the x data (0 is the first column)
- y_col (int) – The column index of the y data
- skip_header (int) – [Optional] Number of header rows to skip. Defaults to 0.
- delimiter (str) – [Optional] Delimiter. Defaults to ‘,’.
- **kwargs – [Optional] Other keyword arguments passed to numpy.genfromtxt.
-
get
(x, der=0, ext=3)¶ Get an interpolated value
Parameters: - x (float, ndarray) – The x value(s).
- der (int) – [Optional] The derivative number. Defaults to 0.
- ext (int) – [Optional] What to do outside the range of xs. See scipy.interpolate.splev for details. Defaults to 3, which means to return the boundary values.
Returns: The y value(s)
Return type: ndarray
-
npsolve.utils.
get_dict
(name)¶
-
npsolve.utils.
get_list
(name)¶
-
npsolve.utils.
get_list_container
(name)¶
-
npsolve.utils.
get_logger
(name)¶
-
npsolve.utils.
get_set
(name)¶
-
npsolve.utils.
get_status
(name)¶
-
npsolve.utils.
none
()¶