Implementing initial conditions for a numerically solved differential equation - python
Imagine someone jumping off a balcony under a certain angle theta and velocity v0 (the height of the balcony is denoted as ystar). Looking at this problem in 2D and considering drag you get a system of differential equations which can be solved with a Runge-Kutta method (I choose explicit-midpoint, not sure what the butcher tableu for this one is). I implemented this and it works perfectly fine, for some given initial conditions I get the trajectory of the moving particle.
My problem is that I want to fix two of the initial conditions (starting point on the x-axis is zero and on the y-axis is ystar) and make sure that the trajectory goes trough a certain point on the x-axis (let's call it xstar). For this of course exist multiple combinations of the other two initial conditions, which in this case are the velocities in the x- and y-direction. The problem is that I don't know how to implement that.
The code that I used to solve the problem up to this point:
1) Implementation of the Runge-Kutta method
import numpy as np
import matplotlib.pyplot as plt
def integrate(methode_step, rhs, y0, T, N):
star = (int(N+1),y0.size)
y= np.empty(star)
t0, dt = 0, 1.* T/N
y[0,...] = y0
for i in range(0,int(N)):
y[i+1,...]=methode_step(rhs,y[i,...], t0+i*dt, dt)
t = np.arange(N+1) * dt
return t,y
def explicit_midpoint_step(rhs, y0, t0, dt):
return y0 + dt * rhs(t0+0.5*dt,y0+0.5*dt*rhs(t0,y0))
def explicit_midpoint(rhs,y0,T,N):
return integrate(explicit_midpoint_step,rhs,y0,T,N)
2) Implementation of the right-hand-side of the differential equation and the nessecery parameters
A = 1.9/2.
cw = 0.78
rho = 1.293
g = 9.81
# Mass and referece length
l = 1.95
m = 118
# Position
xstar = 8*l
ystar = 4*l
def rhs(t,y):
lam = cw * A * rho /(2 * m)
return np.array([y[1],-lam*y[1]*np.sqrt(y[1]**2+y[3]**2),y[3],-lam*y[3]*np.sqrt(y[1]**2+y[3]**2)-g])
3) solving the problem with it
# Parametrize the two dimensional velocity with an angle theta and speed v0
v0 = 30
theta = np.pi/6
v0x = v0 * np.cos(theta)
v0y = v0 * np.sin(theta)
# Initial condintions
z0 = np.array([0, v0x, ystar, v0y])
# Calculate solution
t, z = explicit_midpoint(rhs, z0, 5, 1000)
4) Visualization
plt.figure()
plt.plot(0,ystar,"ro")
plt.plot(x,0,"ro")
plt.plot(z[:,0],z[:,1])
plt.grid(True)
plt.xlabel(r"$x$")
plt.ylabel(r"$y$")
plt.show()
To make the question concrete: With this set up in mind, how do I find all possible combinations of v0 and theta such that z[some_element,0]==xstar
I tried of course some things, mainly the brute force method of fixing theta and then trying out all the possible velocities (in an intervall that makes sense) but finally didn't know how to compare the resulting arrays with the desired result...
Since this is mainly a coding issue I hope stack overflow is the right place to ask for help...
EDIT:
As requested here is my try to solve the problem (replacing 3) and 4) from above)..
theta = np.pi/4.
xy = np.zeros((50,1001,2))
z1 = np.zeros((1001,2))
count=0
for v0 in range(0,50):
v0x = v0 * np.cos(theta)
v0y = v0 * np.sin(theta)
z0 = np.array([0, v0x, ystar, v0y])
# Calculate solution
t, z = explicit_midpoint(rhs, z0, 5, 1000)
if np.around(z[:,0],3).any() == round(xstar,3):
z1[:,0] = z[:,0]
z1[:,1] = z[:,2]
break
else:
xy[count,:,0] = z[:,0]
xy[count,:,1] = z[:,2]
count+=1
plt.figure()
plt.plot(0,ystar,"ro")
plt.plot(xstar,0,"ro")
for k in range(0,50):
plt.plot(xy[k,:,0],xy[k,:,1])
plt.plot(z[:,0],z[:,1])
plt.grid(True)
plt.xlabel(r"$x$")
plt.ylabel(r"$y$")
plt.show()
I'm sure that I'm using the .any() function wrong, the idea there is to round the values of z[:,0] to three digits and than compare them to xstar, if it matches the loop should terminate and retrun the current z, if not it should save it in another array and then increase v0.
Edit 2018-07-16
Here I post a corrected answer taking into account the drag by air.
Below is a python script to compute the set of (v0,theta) values so that the air-dragged trajectory passes through (x,y) = (xstar,0) at some time t=tstar. I used the trajectory without air-drag as the initial guess and also to guess the dependence of x(tstar) on v0 for the first refinement. The number of iterations needed to arrive at the correct v0 was typically 3 to 4. The script finished in 0.99 seconds on my laptop, including the time for generating figures.
The script generates two figures and one text file.
fig_xdrop_v0_theta.png
The black dots indicates the solution set (v0,theta)
The yellow line indicates the reference (v0,theta) which would be a solution if there were no air drag.
fig_traj_sample.png
Checking that the trajectory (blue solid line) passes through (x,y)=(xstar,0) when (v0,theta) is sampled from the solution set.
The black dashed line shows a trajectory without drag by air as a reference.
output.dat
contains the numerical data of (v0,theta) as well as the landing time tstar and number of iteration needed to find v0.
Here begins script.
#!/usr/bin/env python3
import numpy as np
import scipy.integrate
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.image as img
mpl.rcParams['lines.linewidth'] = 2
mpl.rcParams['lines.markeredgewidth'] = 1.0
mpl.rcParams['axes.formatter.limits'] = (-4,4)
#mpl.rcParams['axes.formatter.limits'] = (-2,2)
mpl.rcParams['axes.labelsize'] = 'large'
mpl.rcParams['xtick.labelsize'] = 'large'
mpl.rcParams['ytick.labelsize'] = 'large'
mpl.rcParams['xtick.direction'] = 'out'
mpl.rcParams['ytick.direction'] = 'out'
############################################
len_ref = 1.95
xstar = 8.0*len_ref
ystar = 4.0*len_ref
g_earth = 9.81
#
mass = 118
area = 1.9/2.
cw = 0.78
rho = 1.293
lam = cw * area * rho /(2.0 * mass)
############################################
ngtheta=51
theta_min = -0.1*np.pi
theta_max = 0.4*np.pi
theta_grid = np.linspace(theta_min, theta_max, ngtheta)
#
ngv0=100
v0min =6.0
v0max =18.0
v0_grid=np.linspace(v0min, v0max, ngv0)
# .. this grid is used for the initial coarse scan by reference trajecotry
############################################
outf=open('output.dat','w')
print('data file generated: output.dat')
###########################################
def calc_tstar_ref_and_x_ref_at_tstar_ref(v0, theta, ystar, g_earth):
'''return the drop time t* and drop point x(t*) of a reference trajectory
without air drag.
'''
vx = v0*np.cos(theta)
vy = v0*np.sin(theta)
ts_ref = (vy+np.sqrt(vy**2+2.0*g_earth*ystar))/g_earth
x_ref = vx*ts_ref
return (ts_ref, x_ref)
def rhs_drag(yvec, time, g_eath, lamb):
'''
dx/dt = v_x
dy/dt = v_y
du_x/dt = -lambda v_x sqrt(u_x^2 + u_y^2)
du_y/dt = -lambda v_y sqrt(u_x^2 + u_y^2) -g
yvec[0] .. x
yvec[1] .. y
yvec[2] .. v_x
yvec[3] .. v_y
'''
vnorm = (yvec[2]**2+yvec[3]**2)**0.5
return [ yvec[2], yvec[3], -lamb*yvec[2]*vnorm, -lamb*yvec[3]*vnorm -g_earth]
def try_tstar_drag(v0, theta, ystar, g_earth, lamb, tstar_search_grid):
'''one trial run to find the drop point x(t*), y(t*) of a trajectory
under the air drag.
'''
tinit=0.0
tgrid = [tinit]+list(tstar_search_grid)
yvec_list = scipy.integrate.odeint(rhs_drag,
[0.0, ystar, v0*np.cos(theta), v0*np.sin(theta)],
tgrid, args=(g_earth, lam))
y_drag = [yvec[1] for yvec in yvec_list]
x_drag = [yvec[0] for yvec in yvec_list]
if y_drag[0]<0.0:
ierr=-1
jtstar=0
tstar_braket=None
elif y_drag[-1]>0.0:
ierr=1
jtstar=len(y_drag)-1
tstar_braket=None
else:
ierr=0
for jt in range(len(y_drag)-1):
if y_drag[jt+1]*y_drag[jt]<=0.0:
tstar_braket=[tgrid[jt],tgrid[jt+1]]
if abs(y_drag[jt+1])<abs(y_drag[jt]):
jtstar = jt+1
else:
jtstar = jt
break
tstar_est = tgrid[jtstar]
x_drag_at_tstar_est = x_drag[jtstar]
y_drag_at_tstar_est = y_drag[jtstar]
return (tstar_est, x_drag_at_tstar_est, y_drag_at_tstar_est, ierr, tstar_braket)
def calc_x_drag_at_tstar(v0, theta, ystar, g_earth, lamb, tstar_est,
eps_y=1.0e-3, ngt_search=20,
rel_range_lower=0.8, rel_range_upper=1.2,
num_try=5):
'''compute the dop point x(t*) of a trajectory under the air drag.
'''
flg_success=False
tstar_est_lower=tstar_est*rel_range_lower
tstar_est_upper=tstar_est*rel_range_upper
for jtry in range(num_try):
tstar_search_grid = np.linspace(tstar_est_lower, tstar_est_upper, ngt_search)
tstar_est, x_drag_at_tstar_est, y_drag_at_tstar_est, ierr, tstar_braket \
= try_tstar_drag(v0, theta, ystar, g_earth, lamb, tstar_search_grid)
if ierr==-1:
tstar_est_upper = tstar_est_lower
tstar_est_lower = tstar_est_lower*rel_range_lower
elif ierr==1:
tstar_est_lower = tstar_est_upper
tstar_est_upper = tstar_est_upper*rel_range_upper
else:
if abs(y_drag_at_tstar_est)<eps_y:
flg_success=True
break
else:
tstar_est_lower=tstar_braket[0]
tstar_est_upper=tstar_braket[1]
return (tstar_est, x_drag_at_tstar_est, y_drag_at_tstar_est, flg_success)
def find_v0(xstar, v0_est, theta, ystar, g_earth, lamb, tstar_est,
eps_x=1.0e-3, num_try=6):
'''solve for v0 so that x(t*)==x*.
'''
flg_success=False
v0_hist=[]
x_drag_at_tstar_hist=[]
jtry_end=None
for jtry in range(num_try):
tstar_est, x_drag_at_tstar_est, y_drag_at_tstar_est, flg_success_x_drag \
= calc_x_drag_at_tstar(v0_est, theta, ystar, g_earth, lamb, tstar_est)
v0_hist.append(v0_est)
x_drag_at_tstar_hist.append(x_drag_at_tstar_est)
if not flg_success_x_drag:
break
elif abs(x_drag_at_tstar_est-xstar)<eps_x:
flg_success=True
jtry_end=jtry
break
else:
# adjust v0
# better if tstar_est is also adjusted, but maybe that is too much.
if len(v0_hist)<2:
# This is the first run. Use the analytical expression of
# dx(tstar)/dv0 of the refernece trajectory
dx = xstar - x_drag_at_tstar_est
dv0 = dx/(tstar_est*np.cos(theta))
v0_est += dv0
else:
# use linear interpolation
v0_est = v0_hist[-2] \
+ (v0_hist[-1]-v0_hist[-2]) \
*(xstar -x_drag_at_tstar_hist[-2])\
/(x_drag_at_tstar_hist[-1]-x_drag_at_tstar_hist[-2])
return (v0_est, tstar_est, flg_success, jtry_end)
# make a reference table of t* and x(t*) of a trajectory without air drag
# as a function of v0 and theta.
tstar_ref=np.empty((ngtheta,ngv0))
xdrop_ref=np.empty((ngtheta,ngv0))
for j1 in range(ngtheta):
for j2 in range(ngv0):
tt, xx = calc_tstar_ref_and_x_ref_at_tstar_ref(v0_grid[j2], theta_grid[j1], ystar, g_earth)
tstar_ref[j1,j2] = tt
xdrop_ref[j1,j2] = xx
# make an estimate of v0 and t* of a dragged trajectory for each theta
# based on the reference trajectroy's landing position xdrop_ref.
tstar_est=np.empty((ngtheta,))
v0_est=np.empty((ngtheta,))
v0_est[:]=-1.0
# .. null value
for j1 in range(ngtheta):
for j2 in range(ngv0-1):
if (xdrop_ref[j1,j2+1]-xstar)*(xdrop_ref[j1,j2]-xstar)<=0.0:
tstar_est[j1] = tstar_ref[j1,j2]
# .. lazy
v0_est[j1] \
= v0_grid[j2] \
+ (v0_grid[j2+1]-v0_grid[j2])\
*(xstar-xdrop_ref[j1,j2])/(xdrop_ref[j1,j2+1]-xdrop_ref[j1,j2])
# .. linear interpolation
break
print('compute v0 for each theta under air drag..')
# compute v0 for each theta under air drag
theta_sol_list=[]
tstar_sol_list=[]
v0_sol_list=[]
outf.write('# theta v0 tstar numiter_v0\n')
for j1 in range(ngtheta):
if v0_est[j1]>0.0:
v0, tstar, flg_success, jtry_end \
= find_v0(xstar, v0_est[j1], theta_grid[j1], ystar, g_earth, lam, tstar_est[j1])
if flg_success:
theta_sol_list.append(theta_grid[j1])
v0_sol_list.append(v0)
tstar_sol_list.append(tstar)
outf.write('%26.16e %26.16e %26.16e %10i\n'
%(theta_grid[j1], v0, tstar, jtry_end+1))
theta_sol = np.array(theta_sol_list)
v0_sol = np.array(v0_sol_list)
tstar_sol = np.array(tstar_sol_list)
### Check a sample
jsample=np.size(v0_sol)//3
theta_sol_sample= theta_sol[jsample]
v0_sol_sample = v0_sol[jsample]
tstar_sol_sample= tstar_sol[jsample]
ngt_chk = 50
tgrid = np.linspace(0.0, tstar_sol_sample, ngt_chk)
yvec_list = scipy.integrate.odeint(rhs_drag,
[0.0, ystar,
v0_sol_sample*np.cos(theta_sol_sample),
v0_sol_sample*np.sin(theta_sol_sample)],
tgrid, args=(g_earth, lam))
y_drag_sol_sample = [yvec[1] for yvec in yvec_list]
x_drag_sol_sample = [yvec[0] for yvec in yvec_list]
# compute also the trajectory without drag starting form the same initial
# condiiton by setting lambda=0.
yvec_list = scipy.integrate.odeint(rhs_drag,
[0.0, ystar,
v0_sol_sample*np.cos(theta_sol_sample),
v0_sol_sample*np.sin(theta_sol_sample)],
tgrid, args=(g_earth, 0.0))
y_ref_sample = [yvec[1] for yvec in yvec_list]
x_ref_sample = [yvec[0] for yvec in yvec_list]
#######################################################################
# canvas setting
#######################################################################
f_size = (8,5)
#
a1_left = 0.15
a1_bottom = 0.15
a1_width = 0.65
a1_height = 0.80
#
hspace=0.02
#
ac_left = a1_left+a1_width+hspace
ac_bottom = a1_bottom
ac_width = 0.03
ac_height = a1_height
###########################################
############################################
# plot
############################################
#------------------------------------------------
print('plotting the solution..')
fig1=plt.figure(figsize=f_size)
ax1 =plt.axes([a1_left, a1_bottom, a1_width, a1_height], axisbg='w')
im1=img.NonUniformImage(ax1,
interpolation='bilinear', \
cmap=mpl.cm.Blues, \
norm=mpl.colors.Normalize(vmin=0.0,
vmax=np.max(xdrop_ref), clip=True))
im1.set_data(v0_grid, theta_grid/np.pi, xdrop_ref )
ax1.images.append(im1)
plt.contour(v0_grid, theta_grid/np.pi, xdrop_ref, [xstar], colors='y')
plt.plot(v0_sol, theta_sol/np.pi, 'ok', lw=4, label='Init Cond with Drag')
plt.legend(loc='lower left')
plt.xlabel(r'Initial Velocity $v_0$', fontsize=18)
plt.ylabel(r'Angle of Projection $\theta/\pi$', fontsize=18)
plt.yticks([-0.50, -0.25, 0.0, 0.25, 0.50])
ax1.set_xlim([v0min, v0max])
ax1.set_ylim([theta_min/np.pi, theta_max/np.pi])
axc =plt.axes([ac_left, ac_bottom, ac_width, ac_height], axisbg='w')
mpl.colorbar.Colorbar(axc,im1)
axc.set_ylabel('Distance from Blacony without Drag')
# 'Distance from Blacony $x(t^*)$'
plt.savefig('fig_xdrop_v0_theta.png')
print('figure file genereated: fig_xdrop_v0_theta.png')
plt.close()
#------------------------------------------------
print('plotting a sample trajectory..')
fig1=plt.figure(figsize=f_size)
ax1 =plt.axes([a1_left, a1_bottom, a1_width, a1_height], axisbg='w')
plt.plot(x_drag_sol_sample, y_drag_sol_sample, '-b', lw=2, label='with drag')
plt.plot(x_ref_sample, y_ref_sample, '--k', lw=2, label='without drag')
plt.axvline(x=xstar, color=[0.3, 0.3, 0.3], lw=1.0)
plt.axhline(y=0.0, color=[0.3, 0.3, 0.3], lw=1.0)
plt.legend()
plt.text(0.1*xstar, 0.6*ystar,
r'$v_0=%5.2f$'%(v0_sol_sample)+'\n'+r'$\theta=%5.2f \pi$'%(theta_sol_sample/np.pi),
fontsize=18)
plt.text(xstar, 0.5*ystar, 'xstar', fontsize=18)
plt.xlabel(r'Horizontal Distance $x$', fontsize=18)
plt.ylabel(r'Height $y$', fontsize=18)
ax1.set_xlim([0.0, 1.5*xstar])
ax1.set_ylim([-0.1*ystar, 1.5*ystar])
plt.savefig('fig_traj_sample.png')
print('figure file genereated: fig_traj_sample.png')
plt.close()
outf.close()
Here is the figure fig_xdrop_v0_theta.png.
Here is the figure fig_traj_sample.png.
Edit 2018-07-15
I realized that I overlooked that the question considers the drag by air. What a shame on me. So, my answer below is not correct. I'm afraid that deleting my answer by myself looks like hiding a mistake, and I leave it below for now. If people think it's annoying that an incorrect answer hanging around, I'm O.K. someone delete it.
The differential equation can actually be solved by hand,
and it does not require much computational resource
to map out how far the person reach from the balcony
on the ground as a function of the initial velocity v0 and the
angle theta. Then, you can select the condition (v0,theta)
such that distance_from_balcony_on_the_ground(v0,theta) = xstar
from this data table.
Let's write the horizontal and vertical coordinates of the
person at time t is x(t) and y(t), respectively.
I think you took x=0 at the wall of the building and y=0
as the ground level, and I do so here, too. Let's say the
horizontal and vertical velocity of the person at time t
are v_x(t) and v_y(t), respectively.
The initial conditions at t=0 are given as
x(0) = 0
y(0) = ystar
v_x(0) = v0 cos theta
v_y(0) = v0 sin theta
The Newton eqution you are solving is,
dx/dt = v_x .. (1)
dy/dt = v_y .. (2)
m d v_x /dt = 0 .. (3)
m d v_y /dt = -m g .. (4)
where m is the mass of the person,
and g is the constant which I don't know the English name of,
but we all know what it is.
From eq. (3),
v_x(t) = v_x(0) = v0 cos theta.
Using this with eq. (1),
x(t) = x(0) + \int_0^t dt' v_x(t') = t v0 cos theta,
where we also used the initial condition. \int_0^t means
integral from 0 to t.
From eq. (4),
v_y(t)
= v_y (0) + \int_0^t dt' (-g)
= v0 sin theta -g t,
where we used the initial condition.
Using this with eq. (3) and also using the initial condition,
y(t)
= y(0) + \int_0^t dt' v_y(t')
= ystar + t v0 sin theta -t^2 (g/2).
where t^2 means t squared.
From the expression for y(t), we can get the time tstar
at which the person hits the ground. That is, y(tstar) =0.
This equation can be solved by quadratic formula
(or something similar name) as
tstar = (v0 sin theta + sqrt((v0 sin theta)^2 + 2g ystar)/g,
where I used a condition tstar>0. Now we know
the distance from the balcony the person reached when he hit
the ground as x(tstar). Using the expression for x(t) above,
x(tstar) = (v0 cos theta) (v0 sin theta + sqrt((v0 sin theta)^2 + 2g ystar))/g.
.. (5)
Actually x(tstar) depends on v0 and theta as well as g and ystar.
You hold g and ystar as constants, and you want to find
all (v0,theta) such that x(tstar) = xstar for a given xstar value.
Since the right hand side of eq. (5) can be computed cheaply,
you can set up grids for v0 and theta and compute xstar
on this 2D grid. Then, you can see where roughly is the solution set
of (v0,theta) lies. If you need precise solution, you can pick up
a segment which encloses the solution from this data table.
Below is a python script that demonstrates this idea.
I also attach here a figure generated by this script.
The yellow curve is the solution set (v0,theta) such that the
person hit the ground at xstar from the wall
when xstar = 8.0*1.95 and ystar=4.0*1.95 as you set.
The blue color coordinate indicates x(tstar), i.e., how far the
person jumped from the balcony horizontally.
Note that at a given v0 (higher than a threshold value aruond v0=9.9),
the there are two theta values (two directions for the person
to project himself) to reach the aimed point (x,y) = (xstar,0).
The smaller branch of the theta value can be negative, meaning that the person can jump downward to reach the aimed point, as long as the initial velocity is sufficiently high.
The script also generates a data file output.dat, which has
the solution-enclosing segments.
#!/usr/bin/python3
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.image as img
mpl.rcParams['lines.linewidth'] = 2
mpl.rcParams['lines.markeredgewidth'] = 1.0
mpl.rcParams['axes.formatter.limits'] = (-4,4)
#mpl.rcParams['axes.formatter.limits'] = (-2,2)
mpl.rcParams['axes.labelsize'] = 'large'
mpl.rcParams['xtick.labelsize'] = 'large'
mpl.rcParams['ytick.labelsize'] = 'large'
mpl.rcParams['xtick.direction'] = 'out'
mpl.rcParams['ytick.direction'] = 'out'
############################################
len_ref = 1.95
xstar = 8.0*len_ref
ystar = 4.0*len_ref
g_earth = 9.81
############################################
ngv0=100
v0min =0.0
v0max =20.0
v0_grid=np.linspace(v0min, v0max, ngv0)
############################################
outf=open('output.dat','w')
print('data file generated: output.dat')
###########################################
def x_at_tstar(v0, theta, ystar, g_earth):
vx = v0*np.cos(theta)
vy = v0*np.sin(theta)
return (vy+np.sqrt(vy**2+2.0*g_earth*ystar))*vx/g_earth
ngtheta=100
theta_min = -0.5*np.pi
theta_max = 0.5*np.pi
theta_grid = np.linspace(theta_min, theta_max, ngtheta)
xdrop=np.empty((ngv0,ngtheta))
# x(t*) as a function of v0 and theta.
for j1 in range(ngv0):
for j2 in range(ngtheta):
xdrop[j1,j2] = x_at_tstar(v0_grid[j1], theta_grid[j2], ystar, g_earth)
outf.write('# domain [theta_lower, theta_upper] that encloses the solution\n')
outf.write('# theta such that x_at_tstart(v0,theta, ystart, g_earth)=xstar\n')
outf.write('# v0 theta_lower theta_upper x_lower x_upper\n')
for j1 in range(ngv0):
for j2 in range(ngtheta-1):
if (xdrop[j1,j2+1]-xstar)*(xdrop[j1,j2]-xstar)<=0.0:
outf.write('%26.16e %26.16e %26.16e %26.16e %26.16e\n'
%(v0_grid[j1], theta_grid[j2], theta_grid[j2+1],
xdrop[j1,j2], xdrop[j1,j2+1]))
print('See output.dat for the segments enclosing solutions.')
print('You can hunt further for precise solutions using this data.')
#######################################################################
# canvas setting
#######################################################################
f_size = (8,5)
#
a1_left = 0.15
a1_bottom = 0.15
a1_width = 0.65
a1_height = 0.80
#
hspace=0.02
#
ac_left = a1_left+a1_width+hspace
ac_bottom = a1_bottom
ac_width = 0.03
ac_height = a1_height
###########################################
############################################
# plot
############################################
print('plotting..')
fig1=plt.figure(figsize=f_size)
ax1 =plt.axes([a1_left, a1_bottom, a1_width, a1_height], axisbg='w')
im1=img.NonUniformImage(ax1,
interpolation='bilinear', \
cmap=mpl.cm.Blues, \
norm=mpl.colors.Normalize(vmin=0.0,
vmax=np.max(xdrop), clip=True))
im1.set_data(v0_grid, theta_grid/np.pi, np.transpose(xdrop))
ax1.images.append(im1)
plt.contour(v0_grid, theta_grid/np.pi, np.transpose(xdrop), [xstar], colors='y')
plt.xlabel(r'Initial Velocity $v_0$', fontsize=18)
plt.ylabel(r'Angle of Projection $\theta/\pi$', fontsize=18)
plt.yticks([-0.50, -0.25, 0.0, 0.25, 0.50])
ax1.set_xlim([v0min, v0max])
ax1.set_ylim([theta_min/np.pi, theta_max/np.pi])
axc =plt.axes([ac_left, ac_bottom, ac_width, ac_height], axisbg='w')
mpl.colorbar.Colorbar(axc,im1)
# 'Distance from Blacony $x(t^*)$'
plt.savefig('fig_xdrop_v0_theta.png')
print('figure file genereated: fig_xdrop_v0_theta.png')
plt.close()
outf.close()
So after some trying out I found a way to achieve what I wanted... It is the brute force method that I mentioned in my starting post, but at least now it works...
The idea is quite simple: define a function find_v0 which finds for a given theta a v0. In this function you take a starting value for v0 (I choose 8 but this was just a guess from me), then take the starting value and check with the difference function how far away the interesting point is from (xstar,0). The interesting point in this case can be determined by setting all points on the x-axis that are bigger than xstar to zero (and their corresponding y-values) and then trimming of all the zeros with trim_zeros, now the last element of correspond to the desired output. If the output of the difference function is smaller than a critical value (in my case 0.1) pass the current v0 on, if not, increase it by 0.01 and do the same thing again.
The code for this looks like this (again replacing 3) and 4) ):
th = np.linspace(0,np.pi/3,100)
def find_v0(theta):
v0=8
while(True):
v0x = v0 * np.cos(theta)
v0y = v0 * np.sin(theta)
z0 = np.array([0, v0x, ystar, v0y])
# Calculate solution
t, z = explicit_midpoint(rhs, z0, 5, 1000)
for k in range(1001):
if z[k,0] > xstar:
z[k,0] = 0
z[k,2] = 0
x = np.trim_zeros(z[:,0])
y = np.trim_zeros(z[:,2])
diff = difference(x[-1],y[-1])
if diff < 0.1:
break
else: v0+=0.01
return v0#,x,y[0:]
v0 = np.zeros_like(th)
from tqdm import tqdm
count=0
for k in tqdm(th):
v0[count] = find_v0(k)
count+=1
v0_interp = interpolate.interp1d(th,v0)
plt.figure()
plt.plot(th,v0_interp(th),"g")
plt.grid(True)
plt.xlabel(r"$\theta$")
plt.ylabel(r"$v_0$")
plt.show()
The problem with this thing is that it takes forever to compute (with the current settings around 5-6 mins). If anyone has some hints how to improve the code to get a little bit faster or has a different approach it would be still appreciated.
Assuming that the velocity in x direction never goes down to zero, you can take x as independent parameter instead of the time. The state vector is then time, position, velocity and the vector field in this state space is scaled so that the vx component is always 1. Then integrate from zero to xstar to compute the state (approximation) where the trajectory meets xstar as x-value.
def derivs(u,x):
t,x,y,vx,vy = u
v = hypot(vx,vy)
ax = -lam*v*vx
ay = -lam*v*vy - g
return [ 1/vx, 1, vy/vx, ax/vx, ay/vx ]
odeint(derivs, [0, x0, y0, vx0, vy0], [0, xstar])
or with your own integration method. I used odeint as documented interface to show how this derivatives function is used in the integration.
The resulting time and y-value can be extreme
Related
Poincare Section of a system of second order odes
It is the first time I am trying to write a Poincare section code at Python. I borrowed the piece of code from here: https://github.com/williamgilpin/rk4/blob/master/rk4_demo.py and I have tried to run it for my system of second order coupled odes. The problem is that I do not see what I was expecting to. Actually, I need the Poincare section when x=0 and px>0. I believe that my implementation is not the best out there. I would like to: Improve the way that the initial conditions are chosen. Apply the correct conditions (x=0 and px>0) in order to acquire the correct Poincare section. Create one plot with all the collected poincare section data, not four separate ones. I would appreciate any help. This is the code: from matplotlib.pyplot import * from scipy import * from numpy import * # a simple Runge-Kutta integrator for multiple dependent variables and one independent variable def rungekutta4(yprime, time, y0): # yprime is a list of functions, y0 is a list of initial values of y # time is a list of t-values at which solutions are computed # # Dependency: numpy N = len(time) y = array([thing*ones(N) for thing in y0]).T for ii in xrange(N-1): dt = time[ii+1] - time[ii] k1 = dt*yprime(y[ii], time[ii]) k2 = dt*yprime(y[ii] + 0.5*k1, time[ii] + 0.5*dt) k3 = dt*yprime(y[ii] + 0.5*k2, time[ii] + 0.5*dt) k4 = dt*yprime(y[ii] + k3, time[ii+1]) y[ii+1] = y[ii] + (k1 + 2.0*(k2 + k3) + k4)/6.0 return y # Miscellaneous functions n= 1.0/3.0 kappa1 = 0.1 kappa2 = 0.1 kappa3 = 0.1 def total_energy(valpair): (x, y, px, py) = tuple(valpair) return .5*(px**2 + py**2) + (1.0/(1.0*(n+1)))*(kappa1*np.absolute(x)**(n+1)+kappa2*np.absolute(y-x)**(n+1)+kappa3*np.absolute(y)**(n+1)) def pqdot(valpair, tval): # input: [x, y, px, py], t # takes a pair of x and y values and returns \dot{p} according to the Hamiltonian (x, y, px, py) = tuple(valpair) return np.array([px, py, -kappa1*np.sign(x)*np.absolute(x)**n+kappa2*np.sign(y-x)*np.absolute(y-x)**n, kappa2*np.sign(y-x)*np.absolute(y-x)**n-kappa3*np.sign(y)*np.absolute(y)**n]).T def findcrossings(data, data1): # returns indices in 1D data set where the data crossed zero. Useful for generating Poincare map at 0 prb = list() for ii in xrange(len(data)-1): if (((data[ii] > 0) and (data[ii+1] < 0)) or ((data[ii] < 0) and (data[ii+1] > 0))) and data1[ii] > 0: prb.append(ii) return array(prb) t = linspace(0, 1000.0, 100000) print ("step size is " + str(t[1]-t[0])) # Representative initial conditions for E=1 E = 1 x0=0 y0=0 init_cons = [[x0, y0, np.sqrt(2*E-(1.0*i/10.0)*(1.0*i/10.0)-2.0/(n+1)*(kappa1*np.absolute(x0)**(n+1)+kappa2*np.absolute(y0-x0)**(n+1)+kappa3*np.absolute(y0)**(n+1))), 1.0*i/10.0] for i in range(-10,11)] outs = list() for con in init_cons: outs.append( rungekutta4(pqdot, t, con) ) # plot the results fig1 = figure(1) for ii in xrange(4): subplot(2, 2, ii+1) plot(outs[ii][:,1],outs[ii][:,3]) ylabel("py") xlabel("y") title("Full trajectory projected onto the plane") fig1.suptitle('Full trajectories E = 1', fontsize=10) # Plot Poincare sections at x=0 and px>0 fig2 = figure(2) for ii in xrange(4): subplot(2, 2, ii+1) xcrossings = findcrossings(outs[ii][:,0], outs[ii][:,3]) yints = [.5*(outs[ii][cross, 1] + outs[ii][cross+1, 1]) for cross in xcrossings] pyints = [.5*(outs[ii][cross, 3] + outs[ii][cross+1, 3]) for cross in xcrossings] plot(yints, pyints,'.') ylabel("py") xlabel("y") title("Poincare section x = 0") fig2.suptitle('Poincare Sections E = 1', fontsize=10) show()
You need to compute the derivatives of the Hamiltonian correctly. The derivative of |y-x|^n for x is n*(x-y)*|x-y|^(n-2)=n*sign(x-y)*|x-y|^(n-1) and the derivative for y is almost, but not exactly (as in your code), the same, n*(y-x)*|x-y|^(n-2)=n*sign(y-x)*|x-y|^(n-1), note the sign difference. With this correction you can take larger time steps, with correct linear interpolation probably even larger ones, to obtain the images I changed the integration of the ODE to t = linspace(0, 1000.0, 2000+1) ... E_kin = E-total_energy([x0,y0,0,0]) init_cons = [[x0, y0, (2*E_kin-py**2)**0.5, py] for py in np.linspace(-10,10,8)] outs = [ odeint(pqdot, con, t, atol=1e-9, rtol=1e-8) ) for con in init_cons[:8] ] Obviously the number and parametrization of initial conditions may change. The computation and display of the zero-crossings was changed to def refine_crossing(a,b): tf = -a[0]/a[2] while abs(b[0])>1e-6: b = odeint(pqdot, a, [0,tf], atol=1e-8, rtol=1e-6)[-1]; # Newton step using that b[0]=x(tf) and b[2]=x'(tf) tf -= b[0]/b[2] return [ b[1], b[3] ] # Plot Poincare sections at x=0 and px>0 fig2 = figure(2) for ii in xrange(8): #subplot(4, 2, ii+1) xcrossings = findcrossings(outs[ii][:,0], outs[ii][:,3]) ycrossings = [ refine_crossing(outs[ii][cross], outs[ii][cross+1]) for cross in xcrossings] yints, pyints = array(ycrossings).T plot(yints, pyints,'.') ylabel("py") xlabel("y") title("Poincare section x = 0") and evaluating the result of a longer integration interval
using a Python script w 2nd Order Runge Kutta method to solve the equation of a pendulum, how do I add a calculation of the KE, PE, and TE?
I am unable to figure out how to program my script to plot KE, PE, and TE. I have included multiple ########### in the parts of my code where I feel the problem lies. def pendulum_runge_kutta(theta0,omega0,g,tfinal,dt): # initialize arrays t = np.arange(0.,tfinal+dt,dt) # time array t npoints = len(t) theta = np.zeros(npoints) # position array theta omega = np.zeros(npoints) # position array omega Ke = np.zeros(npoints) Pe = np.zeros(npoints) L=4 g = 9.81 t2=np.linspace(0,tfinal,1000) theta0=0.01 omega0=0 # exact solution for thetaExact = theta0*np.cos((g/L)**(1/2)*t2) # SECOND ORDER RUNGE_KUTTA SOLUTION theta[0] = theta0 omega[0] = omega0 #Ke[0] = ####################################### #Pe[0] =###################################### m=1.0 for i in range(npoints-1): # compute midpoint position (not used!) and velocity thetamid = theta[i] + omega[i]*dt omegamid = omega[i] - (g/L)*np.sin(theta[i])*dt/2 # use midpoint velocity to advance position theta[i+1] = theta[i] + omegamid*dt omega[i+1] = omega[i] -(g/L)*np.sin(thetamid)*dt/2 ###########calculate Ke, Pe, Te############ Ke[i+1] = 0.5*m*(omega[i+1]*L)**2 Pe[i+1] = m*g*L*np.sin(theta[i+1]) Te = Ke+Pe #plot result of Ke, Pe, Te pl.figure(1) pl.plot(t,Ke,'c-',label='kinetic energy') pl.plot(t,Pe,'m-',label='potential energy') pl.plot(t,Te,'g-',label='total energy') pl.title('Ke, Pe, and Te') pl.legend(loc='lower right') pl.show() #now plot the results pl.figure(2) pl.plot(t,theta,'ro',label='2oRK') pl.plot(t2,thetaExact,'r',label='Exact') pl.grid('on') pl.legend() pl.xlabel('Time (s)') pl.ylabel('Theta') pl.title('Theta vs Time') pl.show() #call function pendulum_runge_kutta(0.01, 0, 9.8, 12, .1)
The formulas for the midpoint method apply uniformly to all components of the first order system. Thus it should be dt/2 for both midpoint values and the full dt for the full time step. The potential energy should contain the integral of sin(x), C-cos(x). For instance, Pe[i+1] = m*g*L*(1-np.cos(theta[i+1])) The formulas for the energies at time point i+1 also apply at time point 0. You have to move the mass declaration up, for instance after the length L=4 line. And a final remark, the indicated exact solution is for the small angle approximation, there is no finitely expressible exact solution for the general physical pendulum. For the given amplitude of theta0=0.01, the small angle approximation should be good enough, but be aware of this for larger swings.
Using FiPy and Mayavi to solve the diffusion equation in 3D
I'm interested in solving, \frac{\delta \phi}{\delta t} - D \nabla^2 \phi - \alpha \phi - \gamma \phi = 0 The following is working, but I have a few questions: Is it possible to increase performance with FiPy? I feel like the nx, ny, nz bins are very small here, despite a long computation time. I don't understand why the arrays X, Y, and Z are so large. Notice in the first frame, we are zoomed in. How can I force the extents to automatically be [0..nx, 0..ny, 0..nz] in all plots? Data for the first frame is a sphere of points with values 1.0 surrounded by 0.0. Why does there appear to be a gradient? Is Mayavi interpolating? If so, how can I disable this? Code: from fipy import * import mayavi.mlab as mlab import numpy as np import time # Spatial parameters nx = ny = nz = 30 # bins dx = dy = dz = 1 # Must this be an integer? L = nx * dx # Diffusion and time step D = 1. dt = 10.0 * dx**2 / (2. * D) steps = 4 # Initial value and radius of concentration phi0 = 1.0 r = 3.0 # Rates alpha = 1.0 # Source coeficcient gamma = .01 # Sink coeficcient mesh = Grid3D(nx=nx, ny=ny, nz=nz, dx=dx, dy=dy, dz=dz) X, Y, Z = mesh.cellCenters # These are large arrays phi = CellVariable(mesh=mesh, name=r"$\phi$", value=0.) src = phi * alpha # Source term (zeroth order reaction) degr = -gamma * phi # Sink term (degredation) eq = TransientTerm() == DiffusionTerm(D) + src + degr # Initial concentration is a sphere located in the center of a bounded cube phi.setValue(1.0, where=( ((X-nx/2))**2 + (Y-ny/2)**2 + (Z-nz/2)**2 < r**2) ) # Solve start_time = time.time() results = [phi.getNumericValue().copy()] for step in range(steps): eq.solve(var=phi, dt=dt) results.append(phi.getNumericValue().copy()) print 'Time elapsed:', time.time() - start_time # Plot for i, res in enumerate(results): fig = mlab.figure() res = res.reshape(nx, ny, nz) mlab.contour3d(res, opacity=.3, vmin=0, vmax=1, contours=100, transparent=True, extent=[0, 10, 0, 10, 0, 10]) mlab.colorbar() mlab.savefig('diffusion3d_%i.png'%(i+1)) mlab.close() Time elapsed: 68.2 seconds
It's hard to tell from your question, but in the course of diagnosing things, I discovered that the LinearLUSolver scales very poorly as the dimension of the problem increases (see https://github.com/usnistgov/fipy/issues/474). For this symmetric problem, PySparse should use the PCG solver and Trilinos should use GMRES. If you didn't install either of these, then you'll get the SciPy sparse solvers, which defaults to LU (I don't know why; something for us to look into), and things will be really slow in 3D. Try adding solver=LinearGMRESSolver() to your eq.solve(...) statement. As far as the size of X, Y, and Z, you've declared a 30*30*30 cube of cells, so each of the cell center coordinate vectors will be 27000 elements long. Did you have a different expectation for cellCenters? I suggest you subclass our MayaviDaemon class, or at least look at how it sets up the display in Mayavi. In short, we set a data_set_clipper to the desired bounds. I don't know.
Solving ODE numerically with Python
I am solving an ODE for an harmonic oscillator numerically with Python. When I add a driving force it makes no difference, so I'm guessing something is wrong with the code. Can anyone see the problem? The (h/m)*f0*np.cos(wd*i) part is the driving force. import numpy as np import matplotlib.pyplot as plt # This code solves the ODE mx'' + bx' + kx = F0*cos(Wd*t) # m is the mass of the object in kg, b is the damping constant in Ns/m # k is the spring constant in N/m, F0 is the driving force in N, # Wd is the frequency of the driving force and x is the position # Setting up timeFinal= 16.0 # This is how far the graph will go in seconds steps = 10000 # Number of steps dT = timeFinal/steps # Step length time = np.linspace(0, timeFinal, steps+1) # Creates an array with steps+1 values from 0 to timeFinal # Allocating arrays for velocity and position vel = np.zeros(steps+1) pos = np.zeros(steps+1) # Setting constants and initial values for vel. and pos. k = 0.1 m = 0.01 vel0 = 0.05 pos0 = 0.01 freqNatural = 10.0**0.5 b = 0.0 F0 = 0.01 Wd = 7.0 vel[0] = vel0 #Sets the initial velocity pos[0] = pos0 #Sets the initial position # Numerical solution using Euler's # Splitting the ODE into two first order ones # v'(t) = -(k/m)*x(t) - (b/m)*v(t) + (F0/m)*cos(Wd*t) # x'(t) = v(t) # Using the definition of the derivative we get # (v(t+dT) - v(t))/dT on the left side of the first equation # (x(t+dT) - x(t))/dT on the left side of the second # In the for loop t and dT will be replaced by i and 1 for i in range(0, steps): vel[i+1] = (-k/m)*dT*pos[i] + vel[i]*(1-dT*b/m) + (dT/m)*F0*np.cos(Wd*i) pos[i+1] = dT*vel[i] + pos[i] # Ploting #---------------- # With no damping plt.plot(time, pos, 'g-', label='Undampened') # Damping set to 10% of critical damping b = (freqNatural/50)*0.1 # Using Euler's again to compute new values for new damping for i in range(0, steps): vel[i+1] = (-k/m)*dT*pos[i] + vel[i]*(1-(dT*(b/m))) + (F0*dT/m)*np.cos(Wd*i) pos[i+1] = dT*vel[i] + pos[i] plt.plot(time, pos, 'b-', label = '10% of crit. damping') plt.plot(time, 0*time, 'k-') # This plots the x-axis plt.legend(loc = 'upper right') #--------------- plt.show()
The problem here is with the term np.cos(Wd*i). It should be np.cos(Wd*i*dT), that is note that dT has been added into the correct equation, since t = i*dT. If this correction is made, the simulation looks reasonable. Here's a version with F0=0.001. Note that the driving force is clear in the continued oscillations in the damped condition. The problem with the original equation is that np.cos(Wd*i) just jumps randomly around the circle, rather than smoothly moving around the circle, causing no net effect in the end. This can be best seen by plotting it directly, but the easiest thing to do is run the original form with F0 very large. Below is F0 = 10 (ie, 10000x the value used in the correct equation), but using the incorrect form of the equation, and it's clear that the driving force here just adds noise as it randomly moves around the circle.
Note that your ODE is well behaved and has an analytical solution. So you could utilize sympy for an alternate approach: import sympy as sy sy.init_printing() # Pretty printer for IPython t,k,m,b,F0,Wd = sy.symbols('t,k,m,b,F0,Wd', real=True) # constants consts = {k: 0.1, # values m: 0.01, b: 0.0, F0: 0.01, Wd: 7.0} x = sy.Function('x')(t) # declare variables dx = sy.Derivative(x, t) d2x = sy.Derivative(x, t, 2) # the ODE: ode1 = sy.Eq(m*d2x + b*dx + k*x, F0*sy.cos(Wd*t)) sl1 = sy.dsolve(ode1, x) # solve ODE xs1 = sy.simplify(sl1.subs(consts)).rhs # substitute constants # Examining the solution, we note C3 and C4 are superfluous xs2 = xs1.subs({'C3':0, 'C4':0}) dxs2 = xs2.diff(t) print("Solution x(t) = ") print(xs2) print("Solution x'(t) = ") print(dxs2) gives Solution x(t) = C1*sin(3.16227766016838*t) + C2*cos(3.16227766016838*t) - 0.0256410256410256*cos(7.0*t) Solution x'(t) = 3.16227766016838*C1*cos(3.16227766016838*t) - 3.16227766016838*C2*sin(3.16227766016838*t) + 0.179487179487179*sin(7.0*t) The constants C1,C2 can be determined by evaluating x(0),x'(0) for the initial conditions.
Sampling on a spline to a given max chordal deviation
I'm looking for a fast way to sample points on a spline such that a polygon or linestring through those points does not exceed a given chordal error to the original spline. I have an algorithm I wrote some time ago that produces the results in the picture (see code below if interested; I'm not expecting anyone to pore over it). It works OK, but fast it is not (~2 seconds on my computer to generate that graphic). Is there something easier, maybe built into numpy or scipy that will accomplish this? Thanks import numpy as np from scipy.optimize import brentq from scipy.interpolate import splev def get_rhos(ts, tck): """Get (signed) rhos (1/rad of curvature) for a given set of t values. """ tanvs = np.array(splev(ts, tck, der=1)).T accvs = np.array(splev(ts, tck, der=2)).T if tanvs.ndim == 1: tanvs = tanvs.reshape(1, -1) accvs = accvs.reshape(1, -1) crossp = np.cross(accvs, tanvs, axis=1) tanvms = np.array([np.sqrt(np.dot(v, v)) for v in tanvs]) rhos = crossp / tanvms**3 return rhos def calc_rad(pt0, pt1, pt2, calcdrop=False): """Calculate a radius from three points on the arc. Lifted from http://www.physicsforums.com/showthread.php?t=173847 """ pt0 = np.array(pt0) pt1 = np.array(pt1) pt2 = np.array(pt2) v0 = pt1 - pt0 v1 = pt2 - pt0 v2 = pt2 - pt1 a = np.sqrt(np.dot(v0, v0)) b = np.sqrt(np.dot(v1, v1)) c = np.sqrt(np.dot(v2, v2)) R = (a*b*c) / np.sqrt( 2 * a**2 * b**2 + 2 * b**2 * c**2 + 2 * c**2 * a**2 - a**4 - b**4 - c**4) if calcdrop: # Calculate arc drop drop = R - np.sqrt(R**2 - (b/2.)**2) return R, drop else: return R def chordal_sample(tck, chordaltol, oversample=10): """Given a spline definition and a chordal tolerance (intol/outol), get the t-values for the spline such that, when adjacent points are connected, the chordal tolerance is not violated. Accomplishes this by bracketing a solution, then using the brentq solver to find the point where the chordal error equals the chordal tolerance. Note that a few extra points may be inserted where there are inflections in the cubic; these are sometimes missed by the arc-radius-calculating portion of the code. """ # This is the function we'll need when we have to # go searching for the answer via brentq def makeerrfunc(st, spt, tck, chordaltol): def errfunc(et): mt = (st + et) / 2.0 mpt = np.array(splev(mt, tck)) ept = np.array(splev(et, tck)) _, arcdrop = calc_rad(spt, mpt, ept, calcdrop=True) diff = arcdrop - chordaltol return diff return errfunc # Make sure we're sampling enough points # TODO: How can we be sure? ts = np.linspace(0, 1, oversample * len(tck[1][0])) newts = [0] # Loop through the time values for nt in ts: st = newts[-1] rts = ts[ts > st] # Only consider remaining time values # Step through adjacent pairs of time values and find # ones that bracket the solution. for et0, et1 in zip(rts[0:-1], rts[1:]): # Get a 'middle time' that we can use to calc # a 'middle point' for our arc calculations mt0 = (st + et0) / 2. mt1 = (st + et1) / 2. # Interpolate points at the critical t values ipts = np.array(splev([st, mt0, et0, mt1, et1], tck)) spt, mpt0, ept0, mpt1, ept1 = ipts.T _, arcdrop0 = calc_rad(spt, mpt0, ept0, calcdrop=True) _, arcdrop1 = calc_rad(spt, mpt1, ept1, calcdrop=True) # Have we bracketed the solution yet? If so, use # brentq to find a better one within the bracketed # range, then move on to a new start t. if arcdrop0 > chordaltol: # Check the initial pair errfunc = makeerrfunc(st, spt, tck, chordaltol) mdt = brentq(errfunc, st, et0) newts.append(mdt) break if arcdrop0 <= chordaltol and arcdrop1 > chordaltol: errfunc = makeerrfunc(st, spt, tck, chordaltol) mdt = brentq(errfunc, et0, et1) newts.append(mdt) break # Check for the existence of an inflection point # in the bracketed range by checking the signs # of the two calculated curvatures and looking for # a reversal. if get_rhos(et0, tck)[0] * get_rhos(et1, tck)[0] < 0: newts.append((et0 + et1) / 2.0) break if et1 == 1.0: # No more points to try newts.append(1.0) break return newts if __name__ == '__main__': import matplotlib.pyplot as plt from scipy.interpolate import splprep # Create a hi-res sample spline. Start with some # low-res points and then resample at a higher # res. XY = np.array([[0.0, 1.0, 2.0, 3.0, 2.0, 1.0, 0.0], [0.0, -1.0, -0.5, 0.0, 2.5, 1.2, 2.0]]) tck, u = splprep(XY, s=0) XY = splev(np.linspace(0, 1, 400), tck) tck, u = splprep(XY, s=0) # Get a set of t values that will plot out # a linestring with no more than 0.1 chordal # error to the original. ts = chordal_sample(tck, 0.1) fig, ax = plt.subplots() # Plot the hi-res spline ax.plot(*XY) # Plot the approximated spline ax.plot(*np.array(splev(ts, tck)), marker='o') ax.axis('equal') ax.grid() plt.show()
I found a great solution in one of my favorite modules - Shapely. There's a simplify() method on Shapely geometric objects that takes a tolerance and produces this for the same 0.1 value: Looks better to me, and only took 1.65 ms (for a ~1200x speedup)!