planetary obliquity, trace rotation axis along orbit with python - python

I'm trying to make a simple simulation of a planet that is being orbited by a moon. So far I have a 2 body problem that solves the planet and moon orbit. Now I would like to add a fixed rotation axis to the planet and see how it is affected by the moon. Any idea how this can be done by using python?
The two body problem can be run with the code below:
import pylab
import math
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
# Set Constants
G = 6.67e-11
AU = 1.5e11
daysec = 24.0*60*60
Ma =5.972e24 # Planet mass in Kg
Mb = 7.348e22 # Moon mass in Kg
gravconst = G*Ma*Mb
# Set up starting conditions
# Planet
xa = 0.0
ya = 0.0
za = 0.0
xva = 0.0
yva = 0.0
zva = 0.0
# Moon
xb = 384400000
yb = 0.0
zb = 0.0
xvb = 0.0
yvb = 1000.0
zvb = 0.0
# Time steps
t = 0.0
dt = 0.01*daysec
# Coordinate lists
xalist = []
yalist = []
xblist = []
yblist = []
zalist = []
zblist = []
# Loop
while t < 100.0*daysec:
# Compute Force
rx = xb-xa
ry = yb-ya
rz = zb-za
modr3 = (rx**2+ry**2+rz**2)**1.5
fx = -gravconst*rx/modr3
fy = -gravconst*ry/modr3
fz = -gravconst*rz/modr3
# Update quantities
# Moon
xvb += fx*dt/Mb
yvb += fy*dt/Mb
zvb += fz*dt/Mb
xb += xvb*dt
yb += yvb*dt
zb += zvb*dt
# Planet
xva += -fx*dt/Ma
yva += -fy*dt/Ma
zva += -fz*dt/Ma
xa += xva*dt
ya += yva*dt
za += zva*dt
t += dt
# Saving them in lists
xalist.append(xa)
yalist.append(ya)
zalist.append(za)
xblist.append(xb)
yblist.append(yb)
zblist.append(zb)
xalist[:] = [x / 1e6 for x in xalist]
yalist[:] = [x / 1e6 for x in yalist]
zalist[:] = [x / 1e6 for x in zalist]
xblist[:] = [x / 1e6 for x in xblist]
yblist[:] = [x / 1e6 for x in yblist]
zblist[:] = [x / 1e6 for x in zblist]
#Creating the point to represent the planet at the origin (not to scale),
plt.scatter(0,0,s=200,color='blue')
plt.annotate('Planet', xy=(-45,-50))
plt.scatter(xblist[0],0,s=100,color='grey')
plt.annotate('Mond', xy=(xblist[0]-45,-50))
# Plotting
pylab.plot(xalist, yalist, "-g")
pylab.plot(xblist, yblist, "-r")
plt.axhline(0, color='black')
plt.axvline(0, color='black')
pylab.axis("equal")
pylab.xlabel("X (Mio. Meter)")
pylab.ylabel("Y (Mio. Meter)")
pylab.show()

Not an answer as I am no expert in the matter but just some hints instead (was unreadable in form of comment)
The stuff you want to add is quite complicated as you would need take into account:
moving masses of both bodies
so you need "contact surfaces elevation" of body that has any moving masses (like oceans, magma, rotating core etc) so you can compute the real center of gravity for each time. Also you need to apply forces to the moving mass itself too (which is not driven just by gravity and rotation but also with resonance and mainly inertia) do not forget Earth has also core and magma around it not just oceans so you need to take into account at least 3 surfaces...
non homogenity of mass distribution of both bodies
so you can compute center of gravity, and the quadratic mass inertia for rotations in respect to actual rotation axis
planet/moon is usually at least 3 body problem not just 2
as there is the local star too affecting the moon orbit quite a lot...
Depending on the numbers some effects will be so small that can be discarded but some are not (especially with inertia+resonance in place).
The rotation equations are similar to the position/speed/acceleration like you already got. Its called Newton D'Alembert integration/physics but you would need to implement transform matrices to do this.
See few related QAs:
Rigid Body Physics Rotations
realistic n-body solar system
Pay attention to the last link for accuracy improvement of your integration as right now its very bad and the orbit will be deformed no matter how small the dt due to fact that the gravity vector changes with time but right now you affect it in wrong direction (that is correct only at the start of dt interval) for each integration iteration.
As you can see that is a lot of stuff to handle and most simulation programs I saw do not do it (mine included) ... they fake it by nutation and precession constants instead.

Related

Gekko optimal control. How to create multiple termination objectives/conditions?

So I am simulating plane flight. The plane flies certain distance (pathx and pathy variables) and then the simulation stops, when certain pathx and pathy values are reached. The solver is trying to minimize the fuel consumption (m.Maximize(mass*tf*final), by maximizing the mass value. The solver controls the accelerator pedal position (Tcontr).
Right now the termination condition is defined like this:
For X axis:
m.Equation(x*final<=pathx)
and
m.Minimize(final*(x-pathx)**2)
And for Y axis:
m.Equation(y*final<=pathy)
and
m.Minimize(final*(y-pathy)**2)
And right now the simulation ends when the desired X value is achieved, while desired Y values is not being achieved.
How do I force the simulation to end when both desired (X and Y) values are achieved?
My code:
import numpy as np
import matplotlib.pyplot as plt
from gekko import GEKKO
import math
#Gekko model
m = GEKKO(remote=False)
#Time points
nt = 11
tm = np.linspace(0,100,nt)
m.time = tm
# Variables
Ro = m.Var(value=1.1)#air density
g = m.Const(value=9.80665)
pressure = m.Var(value=101325)#
T = m.Var(value=281,lb=100)#temperature
T0 = m.Const(value=288)#temperature at see level
S = m.Const(value=122.6)
Cd = m.Const(value=0.1)#drag coef
Cl = m.Var(value=1)#lift couef
FuelFlow = m.Var()
D = m.Var(value=25000,lb=0)#drag
Thrmax = m.Const(value=200000)#maximum throttle
Thr = m.Var()#throttle
V = m.Var(value=100,lb=50,ub=240)#velocity
gamma = m.Var(value=0)# Flight-path angle
gammaa = gamma.value
Xi = m.Var(value=0)# Heading angle
Xii = Xi.value
x = m.Var(value=0,lb=0)#x position
y = m.Var(value=0,lb=0)#y position
h = m.Var(value=1000,lb=0)# height
mass = m.Var(value=60000,lb=10000)
pathx = m.Const(value=50000) #intended distance length
pathy = m.Const(value=50000)
L = m.Var(value=0.1)#lift
p = np.zeros(nt)
p[-1] = 1.0
final = m.Param(value=p)
m.options.MAX_ITER=10000 # iteration number
#Fixed Variable
tf = m.FV(value=1,lb=0.0001,ub=1000.0)#
tf.STATUS = 1
# Controlled parameters
Tcontr = m.MV(value=0.2,lb=0.1,ub=1)# solver controls throttle pedal position
Tcontr.STATUS = 1
Tcontr.DCOST = 0
#Mu = m.Var(value=0)
Mu = m.MV(value=0,lb=-1.5,ub=1.5)# solver controls bank angle
Mu.STATUS = 1
Mu.DCOST = 0
Muu = Mu.value
# Equations
m.Equation(Thr==Tcontr*Thrmax)
m.Equation(FuelFlow==0.75882*(1+(V/2938.5)))
m.Equation(D==0.5*Ro*(V**2)*Cd*S)
m.Equation(mass.dt()==tf*(-Thr*(FuelFlow/60000)))#
m.Equation(V.dt()==tf*((Thr-D)/mass))#
m.Equation(x.dt()==tf*(V*(math.cos(gammaa.value))*(math.cos(Xii.value))))#
m.Equation(x*final<=pathx)
#pressure and density part
m.Equation(T==T0-(0.0065*h))
m.Equation(pressure==101325*(1-(0.0065*h)/T0)**((g*0.0289652)/(8.31446*0.0065)))#
m.Equation(Ro*(8.31446*T)==(pressure*0.0289652))
#2D addition part
m.Equation(L==0.5*Ro*(V**2)*Cl*S)
m.Equation(Xi.dt()==tf*((L*math.sin(Muu.value))/(mass*V)))
m.Equation(y.dt()==tf*(V*(math.cos(gammaa.value))*(math.sin(Xii.value))))#
m.Equation(y*final<=pathy)
# Objective Function
m.Minimize(final*(x-pathx)**2) #1D part
m.Minimize(final*(y-pathy)**2) #2D part
m.Maximize(mass*tf*final) #objective function
m.options.IMODE = 6
m.options.NODES = 2 # it was 3 before
m.options.MV_TYPE = 1
m.options.SOLVER = 3
#m.open_folder() # to search for infeasibilities
m.solve()
tm = tm * tf.value[0]
fig, axs = plt.subplots(8)
fig.suptitle('Results')
axs[0].plot(tm,Tcontr,'r-',LineWidth=2,label=r'$Tcontr$')
axs[0].legend(loc='best')
axs[1].plot(tm,V.value,'b-',LineWidth=2,label=r'$V$')
axs[1].legend(loc='best')
axs[2].plot(tm,x.value,'r--',LineWidth=2,label=r'$x$')
axs[2].legend(loc='best')
axs[3].plot(tm,D.value,'g-',LineWidth=2,label=r'$D$')
axs[3].legend(loc='best')
axs[4].plot(tm,mass.value,'g:',LineWidth=2,label=r'$mass$')
axs[4].legend(loc='best')
axs[5].plot(tm,T.value,'p-',LineWidth=2,label=r'$T$')
axs[5].legend(loc='best')
axs[6].plot(tm,Mu.value,'p-',LineWidth=2,label=r'$Mu$')
axs[6].legend(loc='best')
axs[7].plot(tm,y.value,'p-',LineWidth=2,label=r'$y$')
axs[7].legend(loc='best')
plt.xlabel('Time')
#plt.ylabel('Value')
plt.show()
Important Update
It looks like termination conditions (all of the above) work as intended. Y axis values can not be reached, because it looks like the math involving it is not working properly.
Y value is being calculated by this equation:
I have turned into this: m.Equation(y.dt()==tf*(V*(math.cos(gammaa.value))*(math.sin(Xii.value))))
Where: V is true air speed (works properly);
tf is a variable that controls simulation time (works properly);
gammaa is derived from gamma, which is flight-path angle, which is 0 in this simulation (works properly);
Xii is derived from Xi, which is heading angle (the main culprit).
Xi value is defined by this equation:
I turned it into this: m.Equation(Xi.dt()==tf*((L*math.sin(Muu.value))/(mass*V)))
Where: L is lift force (works properly);
mass is mass (works properly);
V is true air speed (works properly);
Muu is derived from Mu, which is bank angle, the solver controlled variable (probably the bad apple here).
Y value calculation should be working like this: solver changes Mu, which affects The heading angle Xi, which then should affect Y value.
After some experiments it looks like the range of changes of the Mu value during the simulation is so small, that it barely affects Y value. But it should be a lot wider since Mu boundaries are quite wide (lb=-1.5,ub=1.5). I tried to remove those boundaries, but it did not affect the results of the simulation.
What could be messing up everything here?
Try adding a hard terminal constraint for both:
m.Equation((x-pathx)*final==0)
m.Equation((y-pathy)*final==0)
Alternatively, increase the weight on the terminal condition.
w = 1e3
m.Minimize(w*final*(x-pathx)**2) #1D part
m.Minimize(w*final*(y-pathy)**2) #2D part
You may need to use one or both strategies. Terminal conditions can be challenging to solve.
Response to Important Update
Use the Gekko functions with m.sin() and m.cos() instead of the math functions. Also, don't use .value in the equations. Gekko needs the full symbolic graph so that it can compile the equations into byte-code and produce function evaluations and the derivatives with automatic differentiation.
m.Equation(y.dt()==tf*(V*(m.cos(gammaa))*(m.sin(Xii))))
It also helps to avoid divide by zero by multiplying any denominator over to the other side.
m.Equation(mass*V*Xi.dt()==tf*((L*m.sin(Muu))))

Numerical solution for a pendulum

I am having a trouble in providing a graphical representation of my system, which happens to be a harmonically driven pendulum. The problem is shown below for reference.
Problem
The source code I used is shown below using the Verlet Scheme.
#Import needed modules
import numpy as np
import matplotlib.pyplot as plt
#Initialize variables (Initial conditions)
g = 9.8 #Gravitational Acceleration
L = 2.0 #Length of the Pendulum
A0 = 3.0 #Initial amplitude of the driving acceleration
v0 = 0.0 #Initial velocity
theta0 = 90*np.pi/180 #Initial Angle
drivingPeriod = 20.0 #Driving Period
#Setting time array for graph visualization
tau = 0.1 #Time Step
tStop = 10.0 #Maximum time for graph visualization derived from Kinematics
t = np.arange(0., tStop+tau, tau) #Array of time
theta = np.zeros(len(t))
v = np.zeros(len(t))
#Verlet Method
theta[0] = theta0
v[0] = v0
for i in range(len(t)-1):
accel = -((g + (A0*np.sin((2*np.pi*t) / drivingPeriod)))/L) * np.sin(theta[i])
theta[i+1] = theta[i] + tau*v[i] + 0.5*tau**2*accel[i]
v[i+1] = v[i] + 0.5*tau*(accel[i] + accel[i+1])
#Plotting and saving the resulting graph
fig, ax1 = plt.subplots(figsize=(7.5,4.5))
ax1.plot(t,theta*(180/np.pi))
ax1.set_xlabel("Time (t)")
ax1.set_ylabel("Theta")
plt.show()
A sample output is shown.
Output
The pendulum should just go back to its initial angle. How can I solve this issue? Notice that as time evolves, my angle measure (degrees) also increases. I want it to only have a domain of 0 degrees to 360 degrees.
Please change the array computation
accel = -((g + (A0*np.sin((2*np.pi*t) / drivingPeriod)))/L) * np.sin(theta[i])
theta[i+1] = theta[i] + tau*v[i] + 0.5*tau**2*accel[i]
into the proper element computation at the correct place
theta[i+1] = theta[i] + tau*v[i] + 0.5*tau**2*accel[i]
accel[i+1] = -((g + (A0*np.sin((2*np.pi*t[i+1]) / drivingPeriod)))/L) * np.sin(theta[i+1])
Note that you need to compute accel[0] separately outside the loop.
It makes the code more readable if you separate out the specifics of the physical model and declare at the start
def acceleration(t,theta):
return -((g + (A0*np.sin((2*np.pi*t) / drivingPeriod)))/L) * np.sin(theta)
so that later you just call
accel[i+1]=acceleration(t[i+1],theta[i+1])
And even then, with a forced oscillation your system is open, it is possible that the forcing action pumps energy into the pendulum, causing it to start to rotate. This is what your graph shows.
The Verlet method like any symplectic method only promises to somewhat have a constant energy if the system is closed and conservative, that is, in the most common cases, no outside influence and all forces are gradient forces.

Parallel and sequential runs give different results in FiPy when using periodic mesh

I have run the following code to simulate a flow around a cylinder in a 2D mesh:
from fipy import CellVariable, FaceVariable, Grid2D, DiffusionTerm, ImplicitSourceTerm, PeriodicGrid2DTopBottom, DistanceVariable, Viewer
from fipy.tools import numerix
L = 1.0
N = 50
dL = L / N
viscosity = 1
U = 1.
#0.8 for pressure and 0.5 for velocity are typical relaxation values for SIMPLE
pressureRelaxation = 0.8
velocityRelaxation = 0.5
if __name__ == '__main__':
sweeps = 500
else:
sweeps = 5
mesh = PeriodicGrid2DTopBottom(nx=N, ny=N, dx=dL, dy=dL)
pressure = CellVariable(mesh=mesh, name='pressure')
pressureCorrection = CellVariable(mesh=mesh)
xVelocity = CellVariable(mesh=mesh, name='X velocity')
yVelocity = CellVariable(mesh=mesh, name='Y velocity')
velocity = FaceVariable(mesh=mesh, rank=1)
pfi=3000.
lfi=0.01
x, y = mesh.cellCenters
var1 = DistanceVariable(name='distance to center', mesh=mesh, value=numerix.sqrt((x-N*dL/2.)**2+(y-N*dL/2.)**2))
rad=0.1
pi_fi= CellVariable(mesh=mesh, value=0.,name='Fluid-interface energy map')
pi_fi.setValue(pfi*numerix.exp(-1.*(var1-rad)/lfi), where=(var1 > rad) )
pi_fi.setValue(pfi, where=(var1 <= rad))
xVelocityEq = DiffusionTerm(coeff=viscosity) - pressure.grad.dot([1., 0.]) - ImplicitSourceTerm(pi_fi)
yVelocityEq = DiffusionTerm(coeff=viscosity) - pressure.grad.dot([0., 1.]) - ImplicitSourceTerm(pi_fi)
ap = CellVariable(mesh=mesh, value=1.)
coeff = 1./ ap.arithmeticFaceValue*mesh._faceAreas * mesh._cellDistances
pressureCorrectionEq = DiffusionTerm(coeff=coeff) - velocity.divergence
from fipy.variables.faceGradVariable import _FaceGradVariable
volume = CellVariable(mesh=mesh, value=mesh.cellVolumes, name='Volume')
contrvolume=volume.arithmeticFaceValue
xVelocity.constrain(U, mesh.facesLeft | mesh.facesRight)
yVelocity.constrain(0., mesh.facesLeft | mesh.facesRight)
X, Y = mesh.faceCenters
pressureCorrection.constrain(0., mesh.facesLeft & (Y < dL))
if __name__ == '__main__':
viewer = Viewer(vars=(pressure, xVelocity, yVelocity, velocity),
xmin=0., xmax=1., ymin=0., ymax=1., colorbar=True)
from builtins import range
for sweep in range(sweeps):
## solve the Stokes equations to get starred values
xVelocityEq.cacheMatrix()
xres = xVelocityEq.sweep(var=xVelocity,
underRelaxation=velocityRelaxation)
xmat = xVelocityEq.matrix
yres = yVelocityEq.sweep(var=yVelocity,
underRelaxation=velocityRelaxation)
## update the ap coefficient from the matrix diagonal
ap[:] = -numerix.asarray(xmat.takeDiagonal())
## update the face velocities based on starred values with the
## Rhie-Chow correction.
## cell pressure gradient
presgrad = pressure.grad
## face pressure gradient
facepresgrad = _FaceGradVariable(pressure)
velocity[0] = xVelocity.arithmeticFaceValue \
+ contrvolume / ap.arithmeticFaceValue * \
(presgrad[0].arithmeticFaceValue-facepresgrad[0])
velocity[1] = yVelocity.arithmeticFaceValue \
+ contrvolume / ap.arithmeticFaceValue * \
(presgrad[1].arithmeticFaceValue-facepresgrad[1])
velocity[..., mesh.exteriorFaces.value] = 0.
velocity[0, mesh.facesLeft.value] = U
velocity[0, mesh.facesRight.value] = U
## solve the pressure correction equation
pressureCorrectionEq.cacheRHSvector()
## left bottom point must remain at pressure 0, so no correction
pres = pressureCorrectionEq.sweep(var=pressureCorrection)
rhs = pressureCorrectionEq.RHSvector
## update the pressure using the corrected value
pressure.setValue(pressure + pressureRelaxation * pressureCorrection )
## update the velocity using the corrected pressure
xVelocity.setValue(xVelocity - pressureCorrection.grad[0] / \
ap * mesh.cellVolumes)
yVelocity.setValue(yVelocity - pressureCorrection.grad[1] / \
ap * mesh.cellVolumes)
if __name__ == '__main__':
if sweep%10 == 0:
print('sweep:', sweep, ', x residual:', xres, \
', y residual', yres, \
', p residual:', pres, \
', continuity:', max(abs(rhs)))
viewer.plot()
print(pressure.globalValue[..., 510])
print(xVelocity.globalValue[..., 510])
print(yVelocity.globalValue[..., 510])
This should solve the Navier-stokes equations with top/bottom periodic conditions and a cylinder in the center of the mesh (that's why there's an implicit source term in my equations). Velocity is equal to one at the left and at the right boundaries, and it is parallel to the x-axis. This example corresponds roughly to a flow through many cylindrical obstacles equally spaced. When I run it without parallel computing, pressure and velocity profiles look ok. The values printed for cell 510 are 2.788 (pressure), 1.104 (xvelocity) and -0.289 (yvelocity).
However, when I run it in parallel mode (say using 2 processors), the profiles look weird. For the velocity profile, the plot between y= 0.2 and y= 0.8 is more or less similar to the plot from the sequential computing between y=0 and y=1. The pressure profile is quite different though. The values that are printed for cell 510 are -3.163 (pressure), 1.209 (xvelocity) and -0.044 (yvelocity).
To use Grid2D instead of PeriodicGrid2DTopBottom, I included extra boundary conditions which I believe will be equivalent to using a periodic grid in this example. The new BCs are then:
xVelocity.constrain(U, mesh.facesLeft | mesh.facesRight)
xVelocity.faceGrad.constrain(mesh.faceNormals * 0., where=mesh.facesBottom | mesh.facesTop)
yVelocity.constrain(0., mesh.exteriorFaces)
By doing so, I get the same output by running in sequential or in parallel with two processors: 2.784 (pressure), 1.104 (xvelocity) and -0.290 (yvelocity).
Were my boundary conditions underspecified when I used the periodic grid? (I guess that would explain two different solutions for the same problem) Or parallel computing and periodic meshes are not getting along for some reason?
Unfortunately, periodic meshes are currently broken for parallel runs.

Implementing initial conditions for a numerically solved differential equation

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

Simulating electron motion - differential equation with adaptive step size in python

I am trying to simulate how the oscillating electric field of an intense laser will push around an electron that is near the Coulomb potential of a +1 ion. The laser field is
E = Eo sin(wt), in the y direction.
and the Coulomb potential is
F = ke q1*q2/r^2, in the r direction.
The strong electric field causes the electron to tunnel ionize, so the initial condition of the electron is to be displaced from the atom in the y-direction. Then, the electron is pushed back and forth by the laser field and has a chance to interact with the Coulomb potential. I want to simulate how the Coulomb potential affects the flight of the electron. The simulations need to be in three dimensions, because I eventually want to include more complex laser fields that push the electron in the x and y directions and the electron can start with momentum in the z direction.
At first, I thought that this would be easy. Below is the code that I used to step through time in very small steps (1e-18 sec). When the electron is not near the ion, this works fine. However, for electrons that pass close to the ion, the results depend strongly on the time-step used in the simulations. If I make the time-step smaller, the calculations take a very long time.
So, I think in this case I am supposed to use an adaptive timestep. Also, from what I have read, the Runge-Kutta methods are supposed to be superior to the simple approach I am using. However, I don't think that scipy.odeint applies to three-dimensions. Any ideas on how to improve the accuracy and speed of these simulations?
Here is the figure showing how the time-step has a huge impact on the results (a bad thing):
And here is my code:
import numpy as np
import matplotlib.pyplot as plt
q = 1.602e-19 #Coulombs Charge of electron
h_bar = 1.054e-34 #J*s Plank's Constant div by 2Pi
c = 3.0e8 #m/s Speed of light
eo = 8.8541e-12 #C^2/(Nm^2) Permittivity of vacuum
me = 9.109e-31 #kg Mass of electron
ke = 8.985551e9 #N m^2 C-2 Coulomb's constant
def fly_trajectory(wavelength,intensity,tb=0,pulseFWHM=40.e-15,
final_time=100e-15,time_step=.001e-15,Ip=15.13,v0=(2e4,0,0)):
#Intensity is in w/cm2. Ip is in eV. Otherwise it's SI units throughout.
#The electric field of the later is in the y-direction
Ip = 15.13 * q #Calculate the ionization potential of the atom in Joules
Eo = np.sqrt(2*intensity*10**4/(c*8.85e-12)) # Electric field in V/m
w = c/wavelength * 2. * np.pi #Angular frequency of the laser
times = np.arange(tb,final_time,time_step)
Ey = Eo*np.sin(w*times) * np.exp(-times**2/(2*(pulseFWHM / 2.35482)**2))
Eb = Ey[0] #E-field at time of birth (time of tunneling)
if Eb == 0: return 0,0 #No field --> no electrons
tunnel_position = -Ip / (Eb*q)
x,y,z = 0,tunnel_position,0
vx,vy,vz = v0
y_list = np.zeros(len(times)) #store the y-values for later
for index in range(0,len(times)):
r = np.sqrt(x**2+y**2+z**2)
rx = x/r; ry = y/r; rz=z/r
Fcy = -q**2 * ke/(r**2) * ry
ay = Ey[index]*(-q)/me + Fcy/me #only y includes the laser
vy = vy + ay*time_step
y = y + vy * time_step
Fcx = -q**2 * ke/(r**2) * rx
ax = (-q)/me + Fcx/me
vx = vx + ax*time_step
x = x + vx * time_step
Fcz = -q**2 * ke/(r**2) * rz
az = (-q)/me + Fcz/me
vz = vz + az*time_step
z = z + vz * time_step
y_list[index] = y
return times,y_list
for tb in np.linspace(0.25*2.66e-15,0.5*2.66e-15,5):
print tb
times,ys = fly_trajectory(800e-9,2e14,tb=tb,time_step=.01e-15)
plt.plot(times,ys,color='r')
times,ys = fly_trajectory(800e-9,2e14,tb=tb,time_step=.001e-15)
plt.plot(times,ys,color='b')
#plot legend and labels:
plt.plot(0,0,color='r',label='10e-18 sec step')
plt.plot(0,0,color='b',label='1e-18 sec step')
plt.xlim(0,10e-15); plt.ylim(-1e-8,1e-8)
leg = plt.legend(); leg.draw_frame(False)
plt.xlabel('Time (sec)')
plt.ylabel('Y-distance (meters)')
plt.show()
As Warren Weckesser suggested, I can simply follow the Scipy cookbook for the coupled mass-spring system. First, I need to write my "right side" equations as:
x' = vx
y' = vy
z' = vz
vx' = Ac*x/r
vy' = Ac*y/r + q*E/m
vz' = Ac*z/r
where Ac=keq^2/(mr^2) is the magnitude of the acceleration due to the Coulomb potential and E is the time-dependent electric field of the laser. Then, I can use scipy.integrate.odeint to find the solutions. This is faster and more reliable than the method that I was using previously.
Here is what the electron trajectories look like with odeint. Now none of them fly away crazily:
And here is the code:
import numpy as np
import matplotlib.pyplot as plt
import scipy.integrate
q = 1.602e-19 #Coulombs Charge of electron
c = 3.0e8 #m/s Speed of light
eo = 8.8541e-12 #C^2/(Nm^2) Permittivity of vacuum
me = 9.109e-31 #kg Mass of electron
ke = 8.985551e9 #N m^2 C-2 Coulomb's constant
def tunnel_position(tb,intensity,wavelength,pulseFWHM,Ip):
Ip = 15.13 * q
Eb = E_laser(tb,intensity,wavelength,pulseFWHM)
return -Ip / (Eb*q)
def E_laser(t,intensity,wavelength,pulseFWHM):
w = c/wavelength * 2. * np.pi #Angular frequency of the laser
Eo = np.sqrt(2*intensity*10**4/(c*8.85e-12)) # Electric field in V/m
return Eo*np.sin(w*t) * np.exp(-t**2/(2*(pulseFWHM / 2.35482)**2))
def vectorfield(variables,t,params):
x,y,z,vx,vy,vz = variables
intensity,wavelength,pulseFWHM,tb = params
r = np.sqrt(x**2+y**2+z**2)
Ac = -ke*q**2/(r**2*me)
return [vx,vy,vz,
Ac*x/r,
Ac*y/r + q/me * E_laser((t-tb),intensity,wavelength,pulseFWHM),
Ac*z/r]
Ip = 15.13 # Ionization potential of Argon eV
intensity = 2e14
wavelength = 800e-9
pulseFWHM = 40e-15
period = wavelength/c
t = np.linspace(0,20*period,10000)
birth_times = np.linspace(0.01*period,0.999*period,50)
max_field = np.max(np.abs(E_laser(birth_times,intensity,wavelength,pulseFWHM)))
for tb in birth_times:
x0 = 0
y0 = tunnel_position(tb,intensity,wavelength,pulseFWHM,Ip)
z0 = 0
vx0 = 2e4
vy0 = 0
vz0 = 0
p = [intensity,wavelength,pulseFWHM,tb]
w0 = [x0,y0,z0,vx0,vy0,vz0]
solution,info = scipy.integrate.odeint(vectorfield,w0,t, args=(p,),full_output=True)
print 'Tb: %.2f fs - smallest step : %.05f attosec'%((tb*1e15),np.min(info['hu'])*1e18)
y = solution[:,1]
importance = (np.abs(E_laser(tb,intensity,wavelength,pulseFWHM))/max_field)
plt.plot(t,y,alpha=importance*0.8,lw=1)
plt.xlabel('Time (sec)')
plt.ylabel('Y-distance (meters)')
plt.show()

Categories

Resources