Package: npsolve
npsolveThenpsolvepackage is a small, simple package built onnumpyandfastwireto make it easy to use object-oriented classes and methods for
the calculation step for numerical solvers.Many numerical solvers (like those inscipy) 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.Basic usage tutorialLet'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 theadd_varmethod to setup variables and their initial values.importnumpyasnpimportnpsolveclassComponent1(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)classComponent2(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 theirstateattribute. It's a dictionary. Theadd_varmethod
sets initial values into the instance's state dictionary. Later, theSolverwill ultimately replace thestateattribute 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. Thestepmethod 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
internalstateattribute instead. So, we'll add some more methods:classComponent1(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)defstep(self,state_dct,t,*args):""" Called by the solver at each time stepCalculate acceleration based on the net force."""acceleration=1.0*self.state['force']derivatives={'position':self.velocity,'velocity':acceleration}returnderivativesclassComponent2(npsolve.Partial):def__init__(self):super().__init__()# Don't forget to call this!self.add_var('force',init=-0.1)defcalculate(self,t):''' Some arbitrary calculations based on current time tand 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}returnderivativesdefstep(self,state_dct,t,*args):''' Called by the solver at each time step '''returnself.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:fromscipy.integrateimportodeintclassSolver(npsolve.Solver):defsolve(self):self.npsolve_init()# Initialiseself.t_vec=np.linspace(0,10,1001)result=odeint(self.step,self.npsolve_initial_values,self.t_vec)returnresultLet's look at what's going on in thesolvemethod. By default, Solvers
have astepmethod that's ready to use. (They also have aone_way_stepmethod that doesn't expect return values from the Partials, and atstepmethod that expects a time value as the first argument.) After initialisation,
the initial values set by the Partial classes are captured in thenpsolve_initial_valuesattribute. By default, the Solver'sstepmethod
returns a vector of all the return values, the same size as the Solver'snpsolve_initial_valuesarray. 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 theconnectmethod of the Solver. They'll link up automatically throughfastwire.defrun():solver=Solver()partials=[Component1(),Component2()]solver.connect(partials)res=solver.solve()returnres,solverLet's set up a plot to see the results. Use thenpsolve_slicesattribute of
the Solver to get the right columns.importmatplotlib.pyplotaspltdefplot(res,s):slices=s.npsolve_slicesplt.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()Run it and see what happens!res,s=run()plot(res,s)