Verlet integrator + friction - physics

I have been following "A Verlet based approach for 2D game physics" on Gamedev.net and I have written something similar.
The problem I am having is that the boxes slide along the ground too much.
How can I add a simple rested state thing where the boxes will have more friction and only slide a tiny bit?

Just introduce a small, constant acceleration on moving objects that points in the direction opposite to the motion. And make sure it can't actually reverse the motion; if you detect that in an integration step, just set the velocity to zero.
If you want to be more realistic, the acceleration should derive from a force which is proportional to the normal force between the object and the surface it's sliding on.
You can find this in any basic physics text, as "kinetic friction" or "sliding friction".

At the verlet integration: r(t)=2.00*r(t-dt)-1.00*r(t-2dt)+2at²
change the multipliers to 1.99 and 0.99 for friction
Edit: this is more true:
r(t)=(2.00-friction_mult.)*r(t-dt)-(1.00-friction_mult.)*r(t-2dt)+at²

Here is a simple time stepping scheme (symplectic Euler method with manually resolved LCP) for a box with Coulomb friction and a spring (frictional oscillator)
mq'' + kq + mu*sgn(q') = F(t)
import numpy as np
import matplotlib.pyplot as plt
q0 = 0 # initial position
p0 = 0 # initial momentum
t_start = 0 # initial time
t_end = 10 # end time
N = 500 # time points
m = 1 # mass
k = 1 # spring stiffness
muN = 0.5 # friction force (slip and maximal stick)
omega = 1.5 # forcing radian frequency [RAD]
Fstat = 0.1 # static component of external force
Fdyn = 0.6 # amplitude of harmonic external force
F = lambda tt,qq,pp: Fstat + Fdyn*np.sin(omega*tt) - k*qq - muN*np.sign(pp) # total force, note sign(0)=0 used to disable friction
zero_to_disable_friction = 0
omega0 = np.sqrt(k/m)
print("eigenfrequency f = {} Hz; eigen period T = {} s".format(omega0/(2*np.pi), 2*np.pi/omega0))
print("forcing frequency f = {} Hz; forcing period T = {} s".format(omega/(2*np.pi), 2*np.pi/omega))
time = np.linspace(t_start, t_end, N) # time grid
h = time[1] - time[0] # time step
q = np.zeros(N+1) # position
p = np.zeros(N+1) # momentum
absFfriction = np.zeros(N+1)
q[0] = q0
p[0] = p0
for n, tn in enumerate(time):
p1slide = p[n] + h*F(tn, q[n], p[n]) # end-time momentum, assuming sliding
q1slide = q[n] + h*p1slide/m # end-time position, assuming sliding
if p[n]*p1slide > 0: # sliding goes on
q[n+1] = q1slide
p[n+1] = p1slide
absFfriction[n] = muN
else:
q1stick = q[n] # assume p1 = 0 at t=tn+h
Fstick = -p[n]/h - F(tn, q1stick, zero_to_disable_friction) # friction force needed to stop at t=tn+h
if np.abs(Fstick) <= muN:
p[n+1] = 0 # sticking
q[n+1] = q1stick
absFfriction[n] = np.abs(Fstick)
else: # sliding starts or passes zero crossing of velocity
q[n+1] = q1slide # possible refinements (adapt to slip-start or zero crossing)
p[n+1] = p1slide
absFfriction[n] = muN

Related

Non Linear MPC optimization of a 2 dimensional drone

I am trying to simulate a drone on a 2-dimensional lunar surface. The drone can apply thrust the z-axis of the body, and the drone can change the angle of its body from -90 degrees to +90 degrees.
The first planned acceleration in the y direction that the MPC function gives is a negative value that exceeds the the lunar accel_g, which I set to be 1.635 m/s^2; thus, the drone cancels out the initial velocity really quickly. This should not happen since I set the constraints of body angle in such that the thrust will never be able to reduce the vertical velocity: vertical velocity of the drone should be reduced only by the lunar gravity. I can not find what is wrong with the code.
** is there a way I can apply rotation to the marker of the plot? I want to change the cross marker so that it can represent the changes in attitude. **
function run_mpc(initial_position, initial_velocity, initial_angle)
model = Model(Ipopt.Optimizer)
Δt = 0.1
num_time_steps = 20 # Change this -> Affects Optimization
max_acceleration_Thr = 3 # Max Thrust / Mass
max_pitch_angle = 90
accel_g = 1.635 # 1/6 of Earth G
des_pos = [-1,0]
#variables model begin
position[1:2, 1:num_time_steps]
velocity[1:2, 1:num_time_steps]
acceleration[1:2, 1:num_time_steps]
-max_pitch_angle <= angle[1:num_time_steps] <= max_pitch_angle
0 <= accel_Thr[1:num_time_steps] <= max_acceleration_Thr
end
# Dynamics constraints
#NLconstraint(model, [i=2:num_time_steps, j=[1]], acceleration[j, i] == accel_Thr[i-1]*sind(angle[i-1]))
#NLconstraint(model, [i=2:num_time_steps, j=[2]], acceleration[j, i] == (accel_Thr[i-1]*cosd(angle[i-1]))-accel_g)
#NLconstraint(model, [i=2:num_time_steps, j=1:2],
velocity[j, i] == velocity[j, i - 1] + (acceleration[j, i - 1]) * Δt)
#NLconstraint(model, [i=2:num_time_steps, j=1:2],
position[j, i] == position[j, i - 1] + velocity[j, i - 1] * Δt)
# Cost function: minimize final position and final velocity
# For Moving to [-2,0] with min. vertical velocity,
# sum(([-2,0]-position[:, end]).^2)+ sum(velocity[[2], end].^2)
#NLobjective(model, Min,
100 * sum((des_pos[i]-position[i, num_time_steps])^2 for i in 1:2)+ sum(velocity[i, num_time_steps]^2 for i in 1:2))
# Initial conditions:
#NLconstraint(model, [i=1:2], position[i, 1] == initial_position[i])
#NLconstraint(model, [i=1:2], velocity[i, 1] == initial_velocity[i])
#NLconstraint(model, angle[1] == initial_angle)
optimize!(model)
return value.(position), value.(velocity), value.(acceleration), value.(angle[2:end])
end;
begin
# The robot's starting position and velocity
q = [1.0, 0.0]
v = [-2.0, 2.0]
ang = 45
Δt = 0.1
# Recording Position, Acceleration, Attitude, Planned Positions
qs_x = []
qs_y = []
as_x = []
as_y = []
angs = []
q_plans = []
u_plans = []
anim = #animate for i in 1:90 # This determies the number of MPC to be run
# Plot the current position & Attitude
plot(label = "Drone",[q[1]], [q[2]], marker=(:rect, 10), xlim=(-2, 2), ylim=(-2, 2))
plot!(label = "Body Axis",[q[1]], [q[2]], marker=(:cross, 18, :grey))
push!(qs_x,q[1])
push!(qs_y,q[2])
# Run the MPC control optimization
q_plan, v_plan, u_plan, ang_plan = run_mpc(q, v, ang)
# Draw the planned future states from the MPC optimization
plot!(label = "Opt. Path", q_plan[1, :], q_plan[2, :], linewidth=5, arrow=true, c=:orange)
# Draw the planned acceleration
plot!(label = "Opt. Accel",u_plan[1, 1:2], u_plan[2, 1:2], linewidth=3, arrow=true, c=:red)
# Save Acceleration & Angle Data to csv
u = u_plan[:, 1]
push!(as_x, u[1])
push!(as_y, u[2])
push!(angs, ang)
push!(u_plans, u_plan)
# Apply the planned acceleration&Attitude and simulate one step in time
global ang = ang_plan[1]
global v += u * Δt
global q += v * Δt
end
gif(anim, "~/Downloads/NLmpc_angle.gif", fps=60)
end

How do I stop python from drawing a new colorbar every iteration

I made this code as a CFD of sorts for fun, and I want to add a color bar to show the velocity of the fluid in different places. Unfortunately, every time it plots a new frame it also plots a new colorbar rather than refreshing the old one. I'd like to get it to refresh rather than draw a new one entirely. Any help would be appreciated. Plotting Begins on line 70
import numpy as np
from matplotlib import pyplot
plot_every = 100
def distance(x1,y1,x2,y2):
return np.sqrt((x2-x1)**2 + (y2-y1)**2)
def main():
Nx = 400 #Cells Across x direction
Ny = 100 #Cells Across y direction
#CELL <> NODE
tau = .53 #kinimatic viscosity
tymestep = tau
Nt = 30000 #total iterations
#Lattice Speeds and Velcoties
NL = 9 #There are 9 differnct velocites, (up, down, left, right, up-left diag, up-right diag, down-left diag, down-right diag, and zero)
#NL would be 27 in 3D flow
cxs = np.array([0,0,1,1,1,0,-1,-1,-1]) #I don't know what this is
cys = np.array([0,1,1,0,-1,-1,-1,0,1]) #I don't know what this is
weights = np.array([4/9,1/9,1/36,1/9,1/36,1/9,1/36,1/9,1/36])
#COMPLETELY DIFFERNT WEIGTS FOR 2D AND 3D FLOW
#Initial Conditions
F = np.ones((Ny,Nx,NL)) + 0.01*np.random.randn(Ny,Nx,NL)
F[:,:,3] = 2.3 #Assigning an inital speed in x direction with right as posative
#Drawing Our cylinder
cylinder = np.full((Ny,Nx), False)
radius = 13
for y in range(0,Ny):
for x in range(0,Nx):
if (distance(Nx//4,Ny//2,x,y) < radius):
cylinder[y][x] = True
#main loop
for it in range(Nt):
#print(it)
F[:,-1, [6,7,8]] = F[:,-2, [6,7,8]] #without this, fluid will bounce off of outside walls (you may want this to happen)
F[:,0, [2,3,4]] = F[:,1, [2,3,4]] #without this, fluid will bounce off of outside walls (you may want this to happen)
for i, cx, cy in zip(range(NL),cxs, cys): #this line is sligtly differnt than his because I think he made a typo
F[:,:,i] = np.roll(F[:,:,i], cx, axis = 1)
F[:,:,i] = np.roll(F[:,:,i], cy, axis = 0)
bndryF = F[cylinder,:]
bndryF = bndryF[:, [0,5,6,7,8,1,2,3,4]] #defines what happens in a colsion (reverse the velocity). This works by setting the up vel to down vel etc
#Fluid Variables
rho = np.sum(F,2) #density
ux = np.sum(F * cxs, 2)/rho #x velocity (momentum/mass)
uy = np.sum(F * cys, 2)/rho #y velocity
F[cylinder,: ] = bndryF
ux[cylinder] = 0 #set all velocities in cylinder = 0
uy[cylinder] = 0 #set all velocities in cylinder = 0
#collisions
Feq = np.zeros(F.shape)
for i, cx, cy, w in zip(range(NL), cxs, cys, weights):
Feq[:, :, i] = rho * w * (
1 + 3*(cx*ux + cy*uy) + 9*(cx*ux + cy*uy)**2/2 - 3*(ux**2 + uy**2)/2
)
F += -1/tau * (F-Feq)
if(it%plot_every == 0):
dfydx = ux[2:, 1:-1] - ux[0:-2, 1: -1]
dfxdy = uy[1: -1, 2:] - uy[1: -1, 0: -2]
curl = dfydx - dfxdy
pyplot.imshow(np.sqrt(ux**2+uy**2),cmap = "bwr")
#pyplot.imshow(curl, cmap = "bwr")
pyplot.colorbar(label="Velocity", orientation="horizontal")
pyplot.pause(0.01)
pyplot.cla()
if __name__ == "__main__":
main()
In your code you are adding a new colorbar at every iteration.
As far as I know, it is impossible to update a colorbar. The workaround is to delete the colorbar of the previous time step, and replace it with a new one.
This is achieved by the update_colorbar function in the code below.
import numpy as np
from matplotlib import pyplot
from matplotlib.cm import ScalarMappable
from matplotlib.colors import Normalize
plot_every = 100
def distance(x1,y1,x2,y2):
return np.sqrt((x2-x1)**2 + (y2-y1)**2)
def update_colorbar(fig, cmap, param, norm=None):
"""The name is misleading: here we create a new colorbar which will be
placed on the same colorbar axis as the original.
"""
# colorbar axes
cax = None
if len(fig.axes) > 1:
cax = fig.axes[-1]
# remove the previous colorbar, if present
if cax is not None:
cax.clear()
if norm is None:
norm = Normalize(vmin=np.amin(param), vmax=np.amax(param))
mappable = ScalarMappable(cmap=cmap, norm=norm)
fig.colorbar(mappable, orientation="horizontal", label="Velocity", cax=cax)
def main():
Nx = 400 #Cells Across x direction
Ny = 100 #Cells Across y direction
#CELL <> NODE
tau = .53 #kinimatic viscosity
tymestep = tau
Nt = 30000 #total iterations
#Lattice Speeds and Velcoties
NL = 9 #There are 9 differnct velocites, (up, down, left, right, up-left diag, up-right diag, down-left diag, down-right diag, and zero)
#NL would be 27 in 3D flow
cxs = np.array([0,0,1,1,1,0,-1,-1,-1]) #I don't know what this is
cys = np.array([0,1,1,0,-1,-1,-1,0,1]) #I don't know what this is
weights = np.array([4/9,1/9,1/36,1/9,1/36,1/9,1/36,1/9,1/36])
#COMPLETELY DIFFERNT WEIGTS FOR 2D AND 3D FLOW
#Initial Conditions
F = np.ones((Ny,Nx,NL)) + 0.01*np.random.randn(Ny,Nx,NL)
F[:,:,3] = 2.3 #Assigning an inital speed in x direction with right as posative
#Drawing Our cylinder
cylinder = np.full((Ny,Nx), False)
radius = 13
for y in range(0,Ny):
for x in range(0,Nx):
if (distance(Nx//4,Ny//2,x,y) < radius):
cylinder[y][x] = True
fig, ax = pyplot.subplots()
cmap = "bwr"
#main loop
for it in range(Nt):
# clear previous images
ax.images.clear()
#print(it)
F[:,-1, [6,7,8]] = F[:,-2, [6,7,8]] #without this, fluid will bounce off of outside walls (you may want this to happen)
F[:,0, [2,3,4]] = F[:,1, [2,3,4]] #without this, fluid will bounce off of outside walls (you may want this to happen)
for i, cx, cy in zip(range(NL),cxs, cys): #this line is sligtly differnt than his because I think he made a typo
F[:,:,i] = np.roll(F[:,:,i], cx, axis = 1)
F[:,:,i] = np.roll(F[:,:,i], cy, axis = 0)
bndryF = F[cylinder,:]
bndryF = bndryF[:, [0,5,6,7,8,1,2,3,4]] #defines what happens in a colsion (reverse the velocity). This works by setting the up vel to down vel etc
#Fluid Variables
rho = np.sum(F,2) #density
ux = np.sum(F * cxs, 2)/rho #x velocity (momentum/mass)
uy = np.sum(F * cys, 2)/rho #y velocity
F[cylinder,: ] = bndryF
ux[cylinder] = 0 #set all velocities in cylinder = 0
uy[cylinder] = 0 #set all velocities in cylinder = 0
#collisions
Feq = np.zeros(F.shape)
for i, cx, cy, w in zip(range(NL), cxs, cys, weights):
Feq[:, :, i] = rho * w * (
1 + 3*(cx*ux + cy*uy) + 9*(cx*ux + cy*uy)**2/2 - 3*(ux**2 + uy**2)/2
)
F += -1/tau * (F-Feq)
if(it%plot_every == 0):
dfydx = ux[2:, 1:-1] - ux[0:-2, 1: -1]
dfxdy = uy[1: -1, 2:] - uy[1: -1, 0: -2]
curl = dfydx - dfxdy
img = np.sqrt(ux**2+uy**2)
ax.imshow(img ,cmap = cmap)
#pyplot.imshow(curl, cmap = "bwr")
update_colorbar(fig, cmap, param=img)
pyplot.pause(0.01)
if __name__ == "__main__":
main()
One thing you can definitely improve is the following line of code, which defines the values visible in the colorbar:
norm = Normalize(vmin=np.amin(param), vmax=np.amax(param))
Specifically, you'd have to choose a wise (conservative) value for vmax=. Currently, vmax=np.amax(param), but the maximum is going to change at every iteration. If I were you, I would chose a value big enough such that np.amax(param) < your_value, in order to ensure consistent colors for each time step.

A Simple Bayesian Network with a Coin-Flipping Problem

I am trying to implement a Bayesian network and solve a regression problem using PYMC3. In my model, I have a fair coin as the parent node. If the parent node is H, the child node selects the normal distribution N(5,0.2); if T, the child selects N(0,0.5). Here is an illustration of my network.
To simulate this network, I generated a sample dataset and tried doing Bayesian regression using the code below. Currently, the model does regression only for the child node as if the parent node does not exist. I would greatly appreciate it if anyone can let me know how to implement the conditional probability P(D|C). Ultimately, I am interested in finding the probability distribution for mu1 and mu2. Thank you!
# Generate data for coin flip P(C) and store in c1
theta_real = 0.5 # unkown value in a real experiment
n_sample = 10
c1 = bernoulli.rvs(p=theta_real, size=n_sample)
# Generate data for normal distribution P(D|C) and store in d1
np.random.seed(123)
mu1 = 0
sigma1 = 0.5
mu2 = 5
sigma2 = 0.2
d1 = []
for index, item in enumerate(c1):
if item == 0:
d1.extend(normal(mu1, sigma1, 1))
else:
d1.extend(normal(mu2, sigma2, 1))
# I start building PYMC3 model here
c1_tensor = theano.shared(np.array(c1))
d1_tensor = theano.shared(np.array(d1))
with pm.Model() as model:
# define prior for c1. I am not sure how to do this.
#c1_present = pm.Categorical('c1',observed=c1_tensor)
# how do I incorporate P(D | C)
mu_prior = pm.Normal('mu', mu=2, sd=2, shape=1)
sigma_prior = pm.HalfNormal('sigma', sd=2, shape=1)
y_likelihood = pm.Normal('y', mu=mu_prior, sd=sigma_prior, observed=d1_tensor)
You could use the Dirichlet distribution as a prior for the coin toss and NormalMixture as the prior of the two Gaussians. In the following snippet I changed the fairness of the coin and increased the number of coin tosses, but you could adjust these in any way want:
import numpy as np
import pymc3 as pm
from scipy.stats import bernoulli
# Generate data for coin flip P(C) and store in c1
theta_real = 0.2 # unkown value in a real experiment
n_sample = 2000
c1 = bernoulli.rvs(p=theta_real, size=n_sample)
# Generate data for normal distribution P(D|C) and store in d1
np.random.seed(123)
mu1 = 0
sigma1 = 0.5
mu2 = 5
sigma2 = 0.2
d1 = []
for index, item in enumerate(c1):
if item == 0:
d1.extend(np.random.normal(mu1, sigma1, 1))
else:
d1.extend(np.random.normal(mu2, sigma2, 1))
with pm.Model() as model:
w = pm.Dirichlet('p', a=np.ones(2))
mu = pm.Normal('mu', 0, 20, shape=2)
sigma = np.array([0.5,0.2])
pm.NormalMixture('like',w=w,mu=mu,sigma=sigma,observed=np.array(d1))
trace = pm.sample()
pm.summary(trace)
This will give you the following:
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
mu__0 4.981222 0.023900 0.000491 4.935044 5.027420 2643.052184 0.999637
mu__1 -0.007660 0.004946 0.000095 -0.017388 0.001576 2481.146286 1.000312
p__0 0.213976 0.009393 0.000167 0.195602 0.231803 2245.905021 0.999302
p__1 0.786024 0.009393 0.000167 0.768197 0.804398 2245.905021 0.999302
The parameters are recovered nicely as you can also see from the traceplots:
The above implementation will give you the posterior of theta_real, mu1 and mu2 but I could not get convergence when I added sigma1 and sigma2 as parameters to be estimated by the data (even though the prior was quite narrow):
with pm.Model() as model:
w = pm.Dirichlet('p', a=np.ones(2))
mu = pm.Normal('mu', 0, 20, shape=2)
sigma = pm.HalfNormal('sigma', sd=2, shape=2)
pm.NormalMixture('like',w=w,mu=mu,sigma=sigma,observed=np.array(d1))
trace = pm.sample()
print(pm.summary(trace))
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [sigma, mu, p]
Sampling 4 chains: 100%|██████████| 4000/4000 [00:10<00:00, 395.57draws/s]
The acceptance probability does not match the target. It is 0.883057127209148, but should be close to 0.8. Try to increase the number of tuning steps.
The gelman-rubin statistic is larger than 1.4 for some parameters. The sampler did not converge.
The estimated number of effective samples is smaller than 200 for some parameters.
mean sd mc_error ... hpd_97.5 n_eff Rhat
mu__0 1.244021 2.165433 0.216540 ... 5.005507 2.002049 212.596596
mu__1 3.743879 2.165122 0.216510 ... 5.012067 2.002040 235.750129
p__0 0.643069 0.248630 0.024846 ... 0.803369 2.004185 30.966189
p__1 0.356931 0.248630 0.024846 ... 0.798632 2.004185 30.966189
sigma__0 0.416207 0.125435 0.012517 ... 0.504110 2.009031 17.333177
sigma__1 0.271763 0.125539 0.012533 ... 0.497208 2.007779 19.217223
[6 rows x 7 columns]
Based on that you most likely will need to reparametrize if you also wanted to estimate the two standard deviations from this data.
This answer is to supplement #balleveryday's answer, which suggests the Gaussian Mixture Model, but had some trouble getting the symmetry breaking to work. Admittedly, the symmetry breaking in the official example is done in the context of Metropolis-Hastings sampling, whereas I think NUTS might be a little more sensitive to encountering impossible values (not sure). Here's what worked for me:
import numpy as np
import pymc3 as pm
from scipy.stats import bernoulli
import theano.tensor as tt
# everything should reproduce
np.random.seed(123)
n_sample = 2000
# Generate data for coin flip P(C) and store in c1
theta_real = 0.2 # unknown value in a real experiment
c1 = bernoulli.rvs(p=theta_real, size=n_sample)
# Generate data for normal distribution P(D|C) and store in d1
mu1, mu2 = 0, 5
sigma1, sigma2 = 0.5, 0.2
d1 = np.empty_like(c1, dtype=np.float64)
d1[c1 == 0] = np.random.normal(mu1, sigma1, np.sum(c1 == 0))
d1[c1 == 1] = np.random.normal(mu2, sigma2, np.sum(c1 == 1))
with pm.Model() as gmm_asym:
# mixture vector
w = pm.Dirichlet('p', a=np.ones(2))
# Gaussian parameters (testval helps start off ordered)
mu = pm.Normal('mu', 0, 20, shape=2, testval=[-10, 10])
sigma = pm.HalfNormal('sigma', sd=2, shape=2)
# break symmetry, forcing mu[0] < mu[1]
order_means_potential = pm.Potential('order_means_potential',
tt.switch(mu[1] - mu[0] < 0, -np.inf, 0))
# observed
pm.NormalMixture('like', w=w, mu=mu, sigma=sigma, observed=d1)
# reproducible sampling
tr_gmm_asym = pm.sample(tune=2000, target_accept=0.9, random_seed=20191121)
This produces samples with the statistics
mean sd mc_error hpd_2.5 hpd_97.5 n_eff Rhat
mu__0 0.004549 0.011975 0.000226 -0.017398 0.029375 2425.487301 0.999916
mu__1 5.007663 0.008993 0.000166 4.989247 5.024692 2181.134002 0.999563
p__0 0.789983 0.009091 0.000188 0.773059 0.808062 2417.356539 0.999788
p__1 0.210017 0.009091 0.000188 0.191938 0.226941 2417.356539 0.999788
sigma__0 0.497322 0.009103 0.000186 0.480394 0.515867 2227.397854 0.999358
sigma__1 0.191310 0.006633 0.000141 0.178924 0.204859 2286.817037 0.999614
and the traces

How do I solve equations using the Scharfetter-Gummel scheme in FiPy?

I'm trying to use FiPy to simulate solar cells but I'm struggling to get reasonable results even for simple test cases.
My test problem is an abrupt 1D p-n homojunction in the dark in equilibrium. The governing system of equations are the semiconductor equations with no additional generation or recombination.
Poisson's equation determines the electric field (φ) in a semiconductor with dielectric constant, &epsi;, given the densities of electrons (n), holes (p), donors (ND), and acceptors (NA), where the charge of an electron is q:
∇²φ = q(p − n + ND − NA) / &epsi;
Electrons and holes drift and diffuse with current densities, J, depending on their mobilities (μ) and diffusion constants (D):
Jn = qμnnE + qDn∇n
Jp = qμppE − qDp∇n
The evolution of the charge in the system is accounted for with the electron and hole continutiy equations:
∂n/∂t = (∇·Jn) / q
∂p/∂t = − (∇·Jp) / q
which can be expressed in FiPy canonical form as:
∂n/∂t = μn∇·(−n∇φ) + Dn∇²n
∂p/∂t = − (μp∇·(−p∇φ) − Dp∇²n)
To attempt to solve the problem in FiPy I first import modules and define the physical parameters.
from __future__ import print_function, division
import fipy
import numpy as np
import matplotlib.pyplot as plt
eps_0 = 8.8542e-12 # Permittivity of free space, F/m
q = 1.6022e-19 # Charge of an electron, C
k = 1.3807e-23 # Boltzmann constant, J/K
T = 300 # Temperature, K
Vth = (k*T)/q # Thermal voltage, eV
N_ap = 1e22 # Acceptor density in p-type layer, m^-3
N_an = 0 # Acceptor density in n-type layer, m^-3
N_dp = 0 # Donor density in p-type layer, m^-3
N_dn = 1e22 # Donor density in n-type layer, m^-3
mu_n = 1400.0e-4 # Mobilty of electrons, m^2/Vs
mu_p = 450.0e-4 # Mobilty of holes, m^2/Vs
D_p = k*T*mu_p/q # Hole diffusion constant, m^2/s
D_n = k*T*mu_n/q # Electron diffusion constant, m^2/s
eps_r = 11.8 # Relative dielectric constant
n_i = (5.29e19 * (T/300)**2.54 * np.exp(-6726/T))*1e6
V_bias = 0
Then create the mesh, solution variables, and doping profile.
nx = 20000
dx = 0.1e-9
mesh = fipy.Grid1D(dx=dx, nx=nx)
Ln = Lp = (nx/2) * dx
phi = fipy.CellVariable(mesh=mesh, hasOld=True, name='phi')
n = fipy.CellVariable(mesh=mesh, hasOld=True, name='n')
p = fipy.CellVariable(mesh=mesh, hasOld=True, name='p')
Na = fipy.CellVariable(mesh=mesh, name='Na')
Nd = fipy.CellVariable(mesh=mesh, name='Nd')
Then I set some initial values on the cell centers and impose Dirichlet boundary conditions on all parameters.
x = mesh.cellCenters[0]
n0 = n_i**2 / N_ap
nL = N_dn
p0 = N_ap
pL = n_i**2 / N_dn
phi_min = -(Vth)*np.log(p0/n_i)
phi_max = (Vth)*np.log(nL/n_i) + V_bias
Na.setValue(N_an, where=(x >= Lp))
Na.setValue(N_ap, where=(x < Lp))
Nd.setValue(N_dn, where=(x >= Lp))
Nd.setValue(N_dp, where=(x < Lp))
n.setValue(N_dn, where=(x > Lp))
n.setValue(n_i**2 / N_ap, where=(x < Lp))
p.setValue(n_i**2 / N_dn, where=(x >= Lp))
p.setValue(N_ap, where=(x < Lp))
phi.setValue((phi_max - phi_min)*x/((Ln + Lp)) + phi_min)
phi.constrain(phi_min, mesh.facesLeft)
phi.constrain(phi_max, mesh.facesRight)
n.constrain(nL, mesh.facesRight)
n.constrain(n_i**2 / p0, mesh.facesLeft)
p.constrain(n_i**2 / nL, mesh.facesRight)
p.constrain(p0, mesh.facesLeft)
I express Poisson's equation as
eps = eps_0*eps_r
rho = q * (p - n + Nd - Na)
rho.name = 'rho'
poisson = fipy.ImplicitDiffusionTerm(coeff=eps, var=phi) == -rho
the continuity equations as
cont_eqn_n = (fipy.TransientTerm(var=n) ==
(fipy.ExponentialConvectionTerm(coeff=-phi.faceGrad*mu_n, var=n)
+ fipy.ImplicitDiffusionTerm(coeff=D_n, var=n)))
cont_eqn_p = (fipy.TransientTerm(var=p) ==
- (fipy.ExponentialConvectionTerm(coeff=-phi.faceGrad*mu_p, var=p)
- fipy.ImplicitDiffusionTerm(coeff=D_p, var=p)))
and solve by coupling the equations and sweeping:
eqn = poisson & cont_eqn_n & cont_eqn_p
dt = 1e-12
steps = 50
sweeps = 10
for step in range(steps):
phi.updateOld()
n.updateOld()
p.updateOld()
for sweep in range(sweeps):
eqn.sweep(dt=dt)
I have played around with different values for the mesh size, time step, number of time steps, number of sweeps etc. I see some variation but haven't had any luck finding a set of conditions that give me a realistic solution. I think the problem probably lies in the expressions for the current terms.
Usually when solving these equations the current densities are are approximated using the Scharfetter-Gummel (SG) discretization scheme, rather then the direct discretization. In the SG scheme the electron current density (J) through a cell face is approximated as a function of the values of potential (φ) and charge density (n) defined on the centres of cells K and L either side as
Jn,KL=qμnVT[B(δφ/VT)nL − B(−δφ/VT)nK)
where q is the charge on an electron, μn is the electron mobility, VT is the thermal voltage, δφ=φL−φK, and B(x) is the Bernoulli function x/(ex−1).
It's not obvious to me how to implement the scheme in FiPy. I have seen there is a scharfetterGummelFaceVariable but I can't work out from the documentation whether it's suitable or intended for this problem. Looking at the code it seems to only calculate the Bernoulli function multiplied by a factor eφL. Is it possible to directly use the scharfetterGummelFaceVariable to solve this type of problem? If so, how? If not, is there an alternative approach that will allow me to simulate semiconductor devices using FiPy?

Stimuli changes with every frame being displayed.

I have a bit of code (displayed below) that is supposed to display the stimulus for 10 frames. We need pretty exact display times, so using number of frames is a must instead of core.wait(xx) as the display time won't be as precise.
Instead of drawing the stimuli, and leaving it for another 9 frames - the stimuli is re-drawn for every frame.
# Import what is needed
import numpy as np
from psychopy import visual, event, core, logging
from math import sin, cos
import random, math
win = visual.Window(size=(1366, 768), fullscr=True, screen=0, allowGUI=False, allowStencil=False,
monitor='testMonitor', color=[0,0,0], colorSpace='rgb',
blendMode='avg', useFBO=True,
units='deg')
### Definitions of libraries
'''Parameters :
numpy - python package of numerical computations
visual - where all visual stimulus live
event - code to deal with mouse + keyboard input
core - general function for timing & closing the program
logging - provides function for logging error and other messages to one file
random - options for creating arrays of random numbers
sin & cos - for geometry and trigonometry
math - mathematical operations '''
# this is supposed to record all frames
win.setRecordFrameIntervals(True)
win._refreshThreshold=1/65.0+0.004 #i've got 65Hz monitor and want to allow 4ms tolerance
#set the log module to report warnings to the std output window (default is errors only)
logging.console.setLevel(logging.WARNING)
nIntervals=5
# Create space variables and a window
lineSpaceX = 0.55
lineSpaceY = 0.55
patch_orientation = 45 # zero is vertical, going anti-clockwise
surround_orientation = 90
#Jitter values
g_posJitter = 0.05 #gaussian positional jitter
r_posJitter = 0.05 #random positional jitter
g_oriJitter = 5 #gaussian orientation jitter
r_oriJitter = 5 #random orientation jitter
#create a 1-Dimentional array
line = np.array(range(38)) #with values from (0-37) #possibly not needed 01/04/16 DK
#Region where the rectangular patch would appear
#x_rand=random.randint(1,22) #random.randint(Return random integers from low (inclusive) to high (exclusive).
#y_rand=random.randint(1,25)
x_rand=random.randint(6,13) #random.randint(Return random integers from low (inclusive) to high (inclusive).
y_rand=random.randint(6,16)
#rectangular patch dimensions
width=15
height=12
message = visual.TextStim(win,pos=(0.0,-12.0),text='...Press SPACE to continue...')
fixation = visual.TextStim(win, pos=(0.0,0.0), text='X')
# Initialize clock to record response time
rt_clock = core.Clock()
#Nested loop to draw anti-aliased lines on grid
#create a function for this
def myStim():
for x in xrange(1,33): #32x32 grid. When x is 33 will not execute loop - will stop
for y in xrange(1,33): #When y is 33 will not execute loop - will stop
##Define x & y value (Gaussian distribution-positional jitter)
x_pos = (x-32/2-1/2 )*lineSpaceX + random.gauss(0,g_posJitter) #random.gauss(mean,s.d); -1/2 is to center even-numbered stimuli; 32x32 grid
y_pos = (y-32/2-1/2 )*lineSpaceY + random.gauss(0,g_posJitter)
if (x >= x_rand and x < x_rand+width) and (y >= y_rand and y < y_rand+height): # note only "=" on one side
Line_Orientation = random.gauss(patch_orientation,g_oriJitter) #random.gauss(mean,s.d) - Gaussian func.
else:
Line_Orientation = random.gauss(surround_orientation,g_oriJitter) #random.gauss(mean,s.d) - Gaussian func.
#Line_Orientation = random.gauss(Line_Orientation,g_oriJitter) #random.gauss(mean,s.d) - Gaussian func.
#stimOri = random.uniform(xOri - r_oriJitter, xOri + r_oriJitter) #random.uniform(A,B) - Uniform func.
visual.Line(win, units = "deg", start=(0,0), end=(0.0,0.35), pos=(x_pos,y_pos), ori=Line_Orientation, autoLog=False).draw() #Gaussian func.
for frameN in range (10):
myStim()
win.flip()
print x_rand, y_rand
print keys, rt #display response and reaction time on screen output window
I have tried to use the following code to keep it displayed (by not clearing the buffer). But it just draws over it several times.
for frameN in range(10):
myStim()
win.flip(clearBuffer=False)
I realize that the problem could be because I have .draw() in the function that I have defined def myStim():. However, if I don't include the .draw() within the function - I won't be able to display the stimuli.
Thanks in advance for any help.
If I understand correctly, the problem you are facing is that you have to re-draw the stimulus on every flip, but your current drawing function also recreates the entire (random) stimulus, so:
the stimulus changes on each draw between flips, although you need it to stay constant, and
you get a (on some systems quite massive) performance penalty by re-creating the entire stimulus over and over again.
What you want instead is: create the stimulus once, in its entirety, before presentation; and then have this pre-generated stimulus drawn on every flip.
Since your stimulus consists of a fairly large number of visual elements, I would suggest using a class to store the stimulus in one place.
Essentially, you would replace your myStim() function with this class (note that I stripped out most comments, re-aligned the code a bit, and simplified the if statement):
class MyStim(object):
def __init__(self):
self.lines = []
for x in xrange(1, 33):
for y in xrange(1, 33):
x_pos = ((x - 32 / 2 - 1 / 2) * lineSpaceX +
random.gauss(0, g_posJitter))
y_pos = ((y - 32 / 2 - 1 / 2) * lineSpaceY +
random.gauss(0, g_posJitter))
if ((x_rand <= x < x_rand + width) and
(y_rand <= y < y_rand + height)):
Line_Orientation = random.gauss(patch_orientation,
g_oriJitter)
else:
Line_Orientation = random.gauss(surround_orientation,
g_oriJitter)
current_line = visual.Line(
win, units="deg", start=(0, 0), end=(0.0, 0.35),
pos=(x_pos, y_pos), ori=Line_Orientation,
autoLog=False
)
self.lines.append(current_line)
def draw(self):
[line.draw() for line in self.lines]
What this code does on instantiation is in principle identical to your myStim() function: it creates a set of (random) lines. But instead of drawing them onto the screen right away, they are all collected in the list self.lines, and will remain there until we actually need them.
The draw() method traverses through this list, element by element (that is, line by line), and calls every line's draw() method. Note that the stimuli do not have to be re-created every time we want to draw the whole set, but instead we just draw the already pre-created lines!
To get this working in practice, you first need to instantiate the MyStim class:
myStim = MyStim()
Then, whenever you want to present the stimulus, all you have to do is
myStim.draw()
win.flip()
Here is the entire, modified code that should get you started:
import numpy as np
from psychopy import visual, event, core, logging
from math import sin, cos
import random, math
win = visual.Window(size=(1366, 768), fullscr=True, screen=0, allowGUI=False, allowStencil=False,
monitor='testMonitor', color=[0,0,0], colorSpace='rgb',
blendMode='avg', useFBO=True,
units='deg')
# this is supposed to record all frames
win.setRecordFrameIntervals(True)
win._refreshThreshold=1/65.0+0.004 #i've got 65Hz monitor and want to allow 4ms tolerance
#set the log module to report warnings to the std output window (default is errors only)
logging.console.setLevel(logging.WARNING)
nIntervals=5
# Create space variables and a window
lineSpaceX = 0.55
lineSpaceY = 0.55
patch_orientation = 45 # zero is vertical, going anti-clockwise
surround_orientation = 90
#Jitter values
g_posJitter = 0.05 #gaussian positional jitter
r_posJitter = 0.05 #random positional jitter
g_oriJitter = 5 #gaussian orientation jitter
r_oriJitter = 5 #random orientation jitter
x_rand=random.randint(6,13) #random.randint(Return random integers from low (inclusive) to high (inclusive).
y_rand=random.randint(6,16)
#rectangular patch dimensions
width=15
height=12
message = visual.TextStim(win,pos=(0.0,-12.0),text='...Press SPACE to continue...')
fixation = visual.TextStim(win, pos=(0.0,0.0), text='X')
# Initialize clock to record response time
rt_clock = core.Clock()
class MyStim(object):
def __init__(self):
self.lines = []
for x in xrange(1, 33):
for y in xrange(1, 33):
x_pos = ((x - 32 / 2 - 1 / 2) * lineSpaceX +
random.gauss(0, g_posJitter))
y_pos = ((y - 32 / 2 - 1 / 2) * lineSpaceY +
random.gauss(0, g_posJitter))
if ((x_rand <= x < x_rand + width) and
(y_rand <= y < y_rand + height)):
Line_Orientation = random.gauss(patch_orientation,
g_oriJitter)
else:
Line_Orientation = random.gauss(surround_orientation,
g_oriJitter)
current_line = visual.Line(
win, units="deg", start=(0, 0), end=(0.0, 0.35),
pos=(x_pos, y_pos), ori=Line_Orientation,
autoLog=False
)
self.lines.append(current_line)
def draw(self):
[line.draw() for line in self.lines]
myStim = MyStim()
for frameN in range(10):
myStim.draw()
win.flip()
# Clear the screen
win.flip()
print x_rand, y_rand
core.quit()
Please do note that even with this approach, I am dropping frames on a 3-year-old laptop computer with relatively weak integrated graphics chip. But I suspect a modern, fast GPU would be able to handle this amount of visual objects just fine. In the worst case, you could pre-create a large set of stimuli, save them as a bitmap file via win.saveMovieFrames(), and present them as a pre-loaded SimpleImageStim during your actual study.