# Dynamics of non-interacting dusty particles ## Package Installation I wrote a simple package that simulate dusty partilces. ![](https://i.imgur.com/EOGhPrW.png) ## Gravity force ![](https://i.imgur.com/DVI3VgM.png) ## Electrostatic force ![](https://i.imgur.com/v7a8jr0.png) ## Thermophoresis force ![](https://i.imgur.com/eVO2Tdy.png) ## Neutral drag force ![](https://i.imgur.com/7AGskyf.png) ## Ion drag force ![](https://i.imgur.com/VJCdewA.png) ## Ion drag force (Reflective) ![](https://i.imgur.com/4r9wEwn.png) ## Code ```python= # publish package # python setup.py sdist # python -m twine upload dist/* import numpy import math import scipy import scipy.interpolate from tqdm import tqdm kb = 1.38064852 * 1e-23 eps0 = 8.854187817e-12 ec = 1.60217662e-19 class Simulation: # initialize the simulation def __init__(self, total_time: float, dt: float, temperature_i: float, temperature_n: float, mass_n: float, mass_i: float, density_n: float, density_i: float, debye_length: float, E_field: float, conductivity: float, v_thn: float, v_n: float, v_i: float, grid_size: float, boundary: dict) -> None: self.total_time = total_time self.dt = dt self.temperature_i = temperature_i self.temperature_n = temperature_n self.mass_n = mass_n self.density_n = density_n self.density_i = density_i self.mass_i = mass_i self.particles_pos = [] self.particles_vel = [] self.particles_r = [] self.particles_rho = [] self.particles_charge = [] self.mass = [] self.debye_length = debye_length self.E_field = E_field self.conductivity = conductivity self.v_thn = v_thn self.v_n = v_n self.v_i = v_i self.grid_size = grid_size self.boundary = boundary # add non-interacting particles def particles(self, pos: float, vel: float, r: float, rho: float, charge: float) -> None: for i in range(len(pos)): self.particles_pos.append(pos[i]) self.particles_vel.append(vel[i]) self.particles_r.append(r[i]) self.particles_rho.append(rho[i]) self.particles_charge.append(charge[i]) def get_trajectory(self, i): return self.traj_pos[:, i, :], self.traj_vel[:, i, :] def get_trajectory_all(self): return self.traj_pos[:, :, :], self.traj_vel[:, :, :] # run the simulation def run(self) -> None: # initialize the variables self.particles_pos = numpy.array(self.particles_pos) self.particles_vel = numpy.array(self.particles_vel) self.particles_r = numpy.array(self.particles_r) num_part = len(self.particles_pos) self.traj_pos = numpy.zeros((int(self.total_time / self.dt), num_part, 3)) self.traj_vel = numpy.zeros((int(self.total_time / self.dt), num_part, 3)) # calculate mass of dusty particles for i in range(num_part): self.mass.append(self.particles_rho[i] * self.particles_r[i]**3 * 4/3 * math.pi) # calculate gradient T gradTn = numpy.gradient(self.temperature_n, self.grid_size[0], self.grid_size[1], self.grid_size[2]) self.gradient_Tn = numpy.array(gradTn) # run the simulation for i in tqdm(range(int(self.total_time / self.dt))): self.move(num_part, step=i) # move particles def move(self, num_part, step): self.traj_pos[step, :] = self.particles_pos self.traj_vel[step, :] = self.particles_vel new_x = numpy.empty(self.particles_pos.shape) new_v = numpy.empty(self.particles_vel.shape) for i in range(num_part): x1, v1 = self.rk4(i, self.dt, self.particles_pos[i, :], self.particles_vel[i, :]) new_x[i, :] = x1 new_v[i, :] = v1 self.particles_pos = new_x self.particles_vel = new_v # check if the particle is in the boundary def boundary_check(self, x0): msg = 'ok' # xmin if x0[0] <= self.boundary['xmin'][1]: if self.boundary['xmin'][0] == 'reflect': msg = 'reflect_xmin' elif self.boundary['xmin'][0] == 'open': msg = 'open_xmin' # xmax if x0[0] >= self.boundary['xmax'][1]: if self.boundary['xmax'][0] == 'reflect': msg = 'reflect_xmax' elif self.boundary['xmax'][0] == 'open': msg = 'open_xmax' # ymin if x0[1] <= self.boundary['ymin'][1]: if self.boundary['ymin'][0] == 'reflect': msg = 'reflect_ymin' elif self.boundary['ymin'][0] == 'open': msg = 'open_ymax' # ymax if x0[1] >= self.boundary['ymax'][1]: if self.boundary['ymax'][0] == 'reflect': msg = 'reflect_ymax' elif self.boundary['ymax'][0] == 'open': msg = 'open_ymax' # zmin if x0[2] <= self.boundary['zmin'][1]: if self.boundary['zmin'][0] == 'reflect': msg = 'reflect_zmin' elif self.boundary['zmin'][0] == 'open': msg = 'open_zmin' # zmax if x0[2] >= self.boundary['zmax'][1]: if self.boundary['zmax'][0] == 'reflect': msg = 'reflect_zmax' elif self.boundary['zmax'][0] == 'open': msg = 'open_zmax' return msg # Ruggle-Kutta method def rk4(self, i, dt: float, x0: float, v0: float): msg = self.boundary_check(x0) if msg == 'ok': k1 = dt * self.total_force(i, x0) / self.mass[i] g1 = v0 * dt msg = self.boundary_check(x0 + g1 / 2.0) if msg == 'ok': k2 = dt * self.total_force(i, x0 + g1 / 2.0) / self.mass[i] g2 = dt * (v0 + k1 / 2.0) msg = self.boundary_check(x0 + g2 / 2.0) if msg == 'ok': k3 = dt * self.total_force(i, x0 + g2 / 2.0) / self.mass[i] g3 = dt * (v0 + k2 / 2.0) msg = self.boundary_check(x0 + g3) if msg == 'ok': k4 = dt * self.total_force(i, x0 + g3) / self.mass[i] g4 = dt * (v0 + k3) v1 = v0 + (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0 x1 = x0 + (g1 + 2.0 * g2 + 2.0 * g3 + g4) / 6.0 return x1, v1 if msg == 'open_xmin' or 'open_xmax': v1 = numpy.array([0, 0, 0]) x1 = x0 if msg == 'open_ymin' or 'open_ymax': v1 = numpy.array([0, 0, 0]) x1 = x0 if msg == 'open_zmin' or 'open_zmax': v1 = numpy.array([0, 0, 0]) x1 = x0 if msg == 'reflect_xmin' or 'reflect_xmax': v1 = v0 * numpy.array([-1, 1, 1]) x1 = x0 + dt * v0 * numpy.array([-1, 1, 1]) if msg == 'reflect_ymin' or 'reflect_ymax': v1 = v0 * numpy.array([1, -1, 1]) x1 = x0 + dt * v0 * numpy.array([1, -1, 1]) if msg == 'reflect_zmin' or 'reflect_zmax': v1 = v0 * numpy.array([1, 1, -1]) x1 = x0 + dt * v1 return x1, v1 # calculate the total force def total_force(self, i, x0): # create meshgrid xmin, xmax = self.boundary['xmin'][1], self.boundary['xmax'][1] ymin, ymax = self.boundary['ymin'][1], self.boundary['ymax'][1] zmin, zmax = self.boundary['zmin'][1], self.boundary['zmax'][1] x = numpy.linspace(xmin, xmax, int((xmax - xmin) / self.grid_size[0])) y = numpy.linspace(ymin, ymax, int((ymax - ymin) / self.grid_size[1])) z = numpy.linspace(zmin, zmax, int((zmax - zmin) / self.grid_size[2])) points = (x, y, z) total = numpy.zeros(3) total += self.gravity(i) total += self.electrostatic(i, x0, points) total += self.thermophoresis(i, x0, points) total += self.neutral_drag(i, x0, points) total += self.ion_drag(i, x0, points) return total # calculate gravity force Fg = 4/3 * pi * r^3 * rho * g def gravity(self, i): Fg = numpy.array([0, 0, -4/3 * math.pi * self.particles_r[i]**3 * self.particles_rho[i] * 9.8]) return Fg # calculate electrostatic force Fe = Qd * E * ( 1 + (rd / ld)^2 / 3*(1 + rd / ld) ) def electrostatic(self, i, x0, points): Qd = self.particles_charge[i] rd = self.particles_r[i] ld = self.debye_length # interpolate the electric field Ex_field = self.E_field[0] Ey_field = self.E_field[1] Ez_field = self.E_field[2] Ex = scipy.interpolate.interpn(points, Ex_field, x0, method='linear') Ey = scipy.interpolate.interpn(points, Ey_field, x0, method='linear') Ez = scipy.interpolate.interpn(points, Ez_field, x0, method='linear') E_interp = numpy.array([Ex[0], Ey[0], Ez[0]]) Fe = Qd * E_interp * (1 + (rd / ld)**2 / 3 * (1 + rd / ld)) return Fe # calculate thermophoresis force FT = -32/15 * (pi * m_n / 8 / kb / Tn)**0.5 * r_d^2 * k_tr * grad_Tn def thermophoresis(self, i, x0, points): # interpolate the temperature Tn = scipy.interpolate.interpn(points, self.temperature_n, x0, method='linear') grad_Tnx = scipy.interpolate.interpn(points, self.gradient_Tn[0], x0, method='linear') grad_Tny = scipy.interpolate.interpn(points, self.gradient_Tn[1], x0, method='linear') grad_Tnz = scipy.interpolate.interpn(points, self.gradient_Tn[2], x0, method='linear') Ft = - 32.0 / 15.0 * (math.pi * self.mass_n / 8.0 / kb / Tn)**0.5 * \ self.particles_r[i]**2 * self.conductivity * numpy.array([grad_Tnx[0], grad_Tny[0], grad_Tnz[0]]) return Ft # calculate neutral drag force Fn = - 4/3 * pi * r_d^2 * n_n * m_n * v_thn * (v_d - v_n) def neutral_drag(self, i, x0, points) -> list: n_n = scipy.interpolate.interpn(points, self.density_n, x0, method='linear') v_thn = scipy.interpolate.interpn(points, self.v_thn, x0, method='linear') v_nx = scipy.interpolate.interpn(points, self.v_n[0], x0, method='linear') v_ny = scipy.interpolate.interpn(points, self.v_n[1], x0, method='linear') v_nz = scipy.interpolate.interpn(points, self.v_n[2], x0, method='linear') v_n = numpy.array([v_nx[0], v_ny[0], v_nz[0]]) Fn = - 4.0 / 3.0 * math.pi * self.particles_r[i]**2 * n_n * self.mass_n * v_thn * (self.particles_vel[i] - v_n) return Fn # calculate ion drag force Fi = F_i-coll + F_i-or = ni * mi * vi * vs * pi * bc^2 + # ni * mi * vi * vs * 4 * pi * bn_pi/2^2 * gamma def ion_drag(self, i, x0, points): Ti = scipy.interpolate.interpn(points, self.temperature_i, x0, method='linear') n_i = scipy.interpolate.interpn(points, self.density_n, x0, method='linear') v_ix = scipy.interpolate.interpn(points, self.v_i[0], x0, method='linear') v_iy = scipy.interpolate.interpn(points, self.v_i[1], x0, method='linear') v_iz = scipy.interpolate.interpn(points, self.v_i[2], x0, method='linear') v_i = numpy.array([v_ix[0], v_iy[0], v_iz[0]]) v_s = (8.0 * kb * Ti / math.pi / self.mass_i + numpy.dot(v_i, v_i))**0.5 phi_d = self.particles_charge[i] / 4.0 / math.pi / eps0 / self.particles_r[i] bc2 = self.particles_r[i]**2 * (1.0 - 2 * self.particles_charge[i] * phi_d / self.mass_i / numpy.dot(v_i, v_i)) F_icoll = n_i * self.mass_i * v_i * v_s * math.pi * bc2**2 bpi2 = ec * self.particles_charge[i] / (4.0 * math.pi * eps0 * self.mass_i * numpy.dot(v_s, v_s)) Gamma = 0.5 * math.log((self.debye_length**2 + bpi2**2) / (bc2 + bpi2**2)) F_ior = n_i * self.mass_i * v_i * v_s * 4.0 * math.pi * bpi2**2 * Gamma Fi = F_icoll + F_ior return Fi ```