Euler#
Panel dashboard illustrating Euler’s Method#
Euler’s method is a numerical integration technique for solving ordinary differential equations. Specifically, given \(y' = f( x, y )\) and a starting point \((x_0, y_0)\), Euler’s method provides approximate values of \(y\) for \(x>x_0\). To explore how this method works, this notebook from Egbert Ammicht sets up a dashboard using Panel, HoloViews, and Bokeh. You will need to have a live Python process running (not just an Anaconda.org notebook or web page), which you can set up using conda install -c pyviz panel holoviews bokeh
.
import param, panel as pn, holoviews as hv, numpy as np, pandas as pd
hv.extension('bokeh')
pn.extension()
#print(pn.__name__, pn.__version__, hv.__name__, hv.__version__ )
Vector field background#
To start, let’s consider the linear approximation \(y' \approx \frac{y(x+h)-y(x)}{h}\), where \(h\) is a small value controlling the integration step size. We can evaluate this equation on a set of \(x\) grid points \(x_n = x_0 + n h\), to obtain a series of \(y\) values using \(y_{n+1} = y_n + h f( x_n, y_n)\). We can use this approach to draw a vector field showing the direction of the integral at each location in the \(x,y\) space, overlaid on a grayscale plot of the same data:
def background(func, size=(500,500)):
"""
Given the ODE y'=f(x,y),
bg,vec,xaxis_line,yaxis_line = background()
returns a grayscale image of the slopes y',
a vector field representation of the slopes, and
a set of axis lines for -5<x<5, -5<y<5
"""
# compute the data
vals = np.linspace( -5, 5, num=150 )
X, Y = np.meshgrid(vals, vals)
clines = func(X,Y) # f(x,y)
theta = np.arctan2( clines, 1) # angle of the slope y' at x,y
# Obtain the vector field (subsample the grid)
h,w=size
vf_opts = dict(size_index=3, height=h, width=w, xticks=9, yticks=9, alpha=0.3, muted_alpha=0.05)
vec_field = hv.VectorField((vals[::3],vals[::3], theta[::3,::3],0*clines[::3,::3]+1),
label='vector_field' ).options(**vf_opts)
# Normalize the given array so that it can be used with the RGB element's alpha channel
def norm(arr):
arr = (arr-arr.min())
return arr/arr.max()
normXY = norm(clines)
img_field = hv.RGB( (vals, vals, normXY, normXY, normXY, 0*clines+0.1), vdims=['R','G','B','A'] )\
.options(width=size[0], height=size[1], shared_axes=False)
# finally, we add the axes as VLine, HLine and return an array of the plot Elements
hv_opts = dict( color='k', alpha=0.8, line_width=1.5)
return [img_field,vec_field, hv.HLine(0).options(**hv_opts),
hv.VLine(0).options(**hv_opts)]
# Test it:
hv.Overlay(background(lambda x,y: np.sin(x*y), size=(400,400))).options(show_legend=False).relabel("y' = sin(x y)" ) +\
hv.Overlay(background(lambda x,y: x*np.sin(5*y), size=(400,400))).options(show_legend=False).relabel("y' = x sin(5 y)" )
WARNING:param.VectorFieldPlot: The `size_index` parameter is deprecated in favor of size style mapping, e.g. `size=dim('size')**2`.
WARNING:param.VectorFieldPlot: The `size_index` parameter is deprecated in favor of size style mapping, e.g. `size=dim('size')**2`.
Euler Integration Curves#
We can now overlay a series of lines approximating the integral for various step sizes at a given starting point, to show how the step size affects the accuracy.
def euler_step(x,y,h,func):
"""x <- x +h, y_<- y + h f(x,y)"""
hs = h * func(x,y)
x = x + h
y = y + hs
return x,y
def euler_table(x0,y0,n,h,func):
"""
Compute up to n euler steps with step size h for y' = f(x,y) starting from (x0,y0),
returning the results in an hv.Table
"""
xl = [x0]; yl=[y0]
for i in range(n):
x0,y0 = euler_step(x0,y0,h,func)
xl.append(x0);yl.append(y0)
if np.abs(x0) > 5. or np.abs(y0) > 5. : break # we ran off the grid
return hv.Table(pd.DataFrame(dict(x=xl,y=yl)), kdims=['x'],vdims=['y'])
line_colors = hv.Cycle(["Red","Orange","LightGreen","Green"])
def euler_curve(x0,y0,n,h,func):
"""
Compute up to n euler steps with step size h for y' = f(x,y) starting from (x0,y0)
return the results in an hv.Curve
"""
return euler_table(x0,y0,n,h,func).to.curve( label= 'h=%6.3f'%h).options(color=line_colors)
def append_euler_plots( l, start, func, n=10000, h=[.5,.2,.01,.0011] ):
for hi in h: l.append( euler_curve(*start, n, hi, func) )
return l
# Example functions
funcs = {
"y' = sin(xy)" : lambda x,y: np.sin(x*y),
"y' = sin(x) sin(y)" : lambda x,y: np.sin(x)*np.sin(y),
"y' = cos(x)" : lambda x,y: np.cos(x),
"y' = exp(x /(x**2 + 1))" : lambda x,y: np.exp(-x/( x**2 + 1)),
"y' = x**2 sin(5y)" : lambda x,y: x**2*np.sin(5*y),
"y' = x sin(5y)" : lambda x,y: x *np.sin(5*y),
"y' = x tan(y)" : lambda x,y: x *np.tan(y),
"y' = x / cosh(y)" : lambda x,y: x/np.cosh(y),
}
f1_key = "y' = sin(xy)"
l1 = background(funcs[f1_key])
append_euler_plots(l1, (-5,np.pi/4.75),funcs[f1_key] )
f2_key = "y' = sin(x) sin(y)"
l2 = background(funcs[f2_key])
append_euler_plots(l2, (-5, np.pi/4.75),funcs[f2_key] )
append_euler_plots(l2, (-5,-np.pi/4.75),funcs[f2_key] )
# We need to call redim in case some curve overshot the grid (might instead use apply_ranges=False to Curves)
pos_opts = dict(legend_position='right', toolbar='above',width=450,height=350)
hv.Overlay(l1).redim.range(x=(-5,5),y=(-5,5)).options(**pos_opts).relabel(f1_key) +\
hv.Overlay(l2).redim.range(x=(-5,5),y=(-5,5)).options(**pos_opts).relabel(f2_key)
WARNING:param.VectorFieldPlot: The `size_index` parameter is deprecated in favor of size style mapping, e.g. `size=dim('size')**2`.
WARNING:param.VectorFieldPlot: The `size_index` parameter is deprecated in favor of size style mapping, e.g. `size=dim('size')**2`.
User-selectable starting point#
The above plots use a fixed starting point, but we can let the user select anything in the x.y plane, using a HoloViews Tap stream:
from holoviews.streams import SingleTap
func_sel = "y' = x tan(y)"
l1 = background(funcs[func_sel])
tap = SingleTap(transient=True)
pos_opts = dict(legend_position='right', toolbar='above')
# Add 4 initial curves; not sure why they are needed for the display to get updated
append_euler_plots(l1, (-5, np.pi/4.75),funcs[func_sel] )
def react_to_tap(x,y):
if x is not None and y is not None and x < 5. and y < 5.:
# Avoids firing when clicked on a legend
del l1[4:] # temorary hack: want a reset button later
append_euler_plots( l1, (x,y), funcs[func_sel] )
return hv.Overlay(l1).redim.range(x=(-5,5),y=(-5,5)).options(**pos_opts).relabel( func_sel )
hv.DynamicMap( react_to_tap, streams=[ tap ]).options(**pos_opts).relabel( func_sel )
WARNING:param.VectorFieldPlot: The `size_index` parameter is deprecated in favor of size style mapping, e.g. `size=dim('size')**2`.
A dashboard#
We can now put everything together into a panel, capturing the above code into an object with a parameter to select the function to show. By default, the panel will be shown directly in this notebook, but you can instead launch the object marked .servable()
below as a separate dashboard using:
panel serve –show euler.ipynb
class EulersMethodExplorer(param.Parameterized):
function_ = param.ObjectSelector(default="y' = sin(xy)", objects=list(funcs.keys()))
@param.depends('function_')
def view(self):
l1 = background(funcs[self.function_])
tap = SingleTap(transient=True)
pos_opts = dict(legend_position='right', toolbar='above', width=700)
append_euler_plots(l1, (-5, np.pi/4.75),funcs[self.function_] )
def react_to_tap(x,y):
if x is not None and y is not None and x < 5. and y < 5.:
del l1[4:] # temorary hack: want a reset button later
append_euler_plots( l1, (x,y), funcs[self.function_] )
return hv.Overlay(l1).redim.range(x=(-5,5),y=(-5,5)).options(**pos_opts).relabel( self.function_ )
return hv.DynamicMap( react_to_tap, streams=[ tap ]).options(**pos_opts).relabel( self.function_ )
#EulersMethodExplorer(name="").view()
explorer = EulersMethodExplorer(name="")
widgets = pn.panel(explorer.param, width=600)
header = pn.panel("""
<a href="https://en.wikipedia.org/wiki/Leonhard_Euler"><img width=180
src="https://upload.wikimedia.org/wikipedia/commons/d/d7/Leonhard_Euler.jpg"></a>
### Euler's Method
<i>This [Panel](https://github.com/holoviz/panel) app lets you explore
the behavior of
[Euler's Method](https://tutorial.math.lamar.edu/Classes/DE/EulersMethod.aspx)
for various differential equations.<br><br>
Choose a particular equation, then click on a starting point on the plot.
The curves then show increasingly better approximations to the integral, with
green indicating the most accurate (stepsize h=0.001).<br><br>
Plot elements can be muted by clicking on the legend.</i>
""", width=200, height=250)
pn.Row(header, pn.Spacer(width=50),
pn.Column(pn.Spacer(height=10), widgets, pn.Spacer(height=10), explorer.view)).servable()
WARNING:param.VectorFieldPlot: The `size_index` parameter is deprecated in favor of size style mapping, e.g. `size=dim('size')**2`.