"""Stores and manages particles and updates their positions basedon the velocity on the grid."""importnumpyasnpfrompyro.utilimportmsg
[docs]classParticle:""" Class to hold properties of a single (massless) particle. This class could be extended (i.e. inherited from) to model e.g. massive/charged particles. """def__init__(self,x,y,u=0,v=0):self.x=xself.y=yself.u=uself.v=v
[docs]defpos(self):""" Return position vector. """returnnp.array([self.x,self.y])
[docs]defupdate(self,u,v,dt):""" Advect the particle and update its velocity. """self.u=uself.v=vself.x+=u*dtself.y+=v*dt
[docs]definterpolate_velocity(self,myg,u,v):""" Interpolate the x- and y-velocities defined on grid myg to the particle's position. Parameters ---------- myg : Grid2d grid which the velocities are defined on u : ArrayIndexer x-velocity v : ArrayIndexer y_velocity """# find what cell it lives inx_idx=(self.x-myg.xmin)/myg.dx-0.5y_idx=(self.y-myg.ymin)/myg.dy-0.5x_frac=x_idx%1y_frac=y_idx%1# get the index of the particle's closest bottom left cell# we'll add one as going to use buf'd grids -# this will catch the cases where the particle is on the edges# of the grid.x_idx=int(x_idx)+1y_idx=int(y_idx)+1# interpolate velocityu_vel=(1-x_frac)*(1-y_frac)*u.v(buf=1)[x_idx,y_idx]+ \
x_frac*(1-y_frac)*u.v(buf=1)[x_idx+1,y_idx]+ \
(1-x_frac)*y_frac*u.v(buf=1)[x_idx,y_idx+1]+ \
x_frac*y_frac*u.v(buf=1)[x_idx+1,y_idx+1]v_vel=(1-x_frac)*(1-y_frac)*v.v(buf=1)[x_idx,y_idx]+ \
x_frac*(1-y_frac)*v.v(buf=1)[x_idx+1,y_idx]+ \
(1-x_frac)*y_frac*v.v(buf=1)[x_idx,y_idx+1]+ \
x_frac*y_frac*v.v(buf=1)[x_idx+1,y_idx+1]returnu_vel,v_vel
[docs]classParticles:""" Class to hold multiple particles. """def__init__(self,sim_data,bc,n_particles,particle_generator="grid",pos_array=None,init_array=None):""" Initialize the Particles object. Particles are stored as a dictionary, with their keys being tuples of their initial position. This was done in order to have a simple way to access the initial particle positions when plotting. However, this assumes that no two particles are initialised with the same initial position, which is fine for the massless particle case, however could no longer be a sensible thing to do if have particles have other properties (e.g. mass). Parameters ---------- sim_data : CellCenterData2d object The cell-centered simulation data bc : BC object Boundary conditions n_particles : int Number of particles particle_generator : string or function String with generator name of custom particle generator function pos_array : float array Array of particle positions to use with particle initialization init_array : float array Array of initial particle positions required for plotting from file. """self.sim_data=sim_dataself.bc=bcself.particles={}ifn_particles<=0:msg.fail("ERROR: n_particles = %s <= 0"%(n_particles))ifcallable(particle_generator):# custom particle generator functionself.particles=particle_generator(n_particles)else:ifparticle_generator=="random":self.randomly_generate_particles(n_particles)elifparticle_generator=="grid":self.grid_generate_particles(n_particles)elifparticle_generator=="array":self.array_generate_particles(pos_array,init_array)else:msg.fail("ERROR: do not recognise particle generator %s"%(particle_generator))self.n_particles=len(self.particles)
[docs]defgrid_generate_particles(self,n_particles):""" Generate particles equally spaced across the grid. Currently has the same number of particles in the x and y directions (so dx != dy unless the domain is square) - may be better to scale this. If necessary, shall increase/decrease n_particles in order to fill grid. """sq_n_particles=int(round(np.sqrt(n_particles)))ifsq_n_particles**2!=n_particles:msg.warning(f"WARNING: Changing number of particles from {n_particles} to {sq_n_particles**2}")myg=self.sim_data.gridxs,step=np.linspace(myg.xmin,myg.xmax,num=sq_n_particles,endpoint=False,retstep=True)xs+=0.5*stepys,step=np.linspace(myg.ymin,myg.ymax,num=sq_n_particles,endpoint=False,retstep=True)ys+=0.5*stepforxinxs:foryinys:self.particles[(x,y)]=Particle(x,y)
[docs]defarray_generate_particles(self,pos_array,init_array=None):""" Generate particles based on array of their positions. This is used when reading particle data from file. Parameters ---------- pos_array : float array Array of particle positions. init_array : float array Array of initial particle positions. """# check a pos_array has been passed to the constructorifpos_arrayisNone:msg.fail("ERROR: Array of particle positions has not been passed into Particles constructor.\ Cannot generate particles.")ifinit_arrayisNone:for(x,y)inpos_array:self.particles[(x,y)]=Particle(x,y)else:for(ix,iy),(x,y)inzip(init_array,pos_array):self.particles[(ix,iy)]=Particle(x,y)
[docs]defupdate_particles(self,dt,u=None,v=None):r""" Update the particles on the grid. This is based off the ``AdvectWithUcc`` function in AMReX, which used the midpoint method to advance particles using the cell-centered velocity. We will explicitly pass in u and v if they cannot be accessed from the ``sim_data`` using ``get_var("velocity")``. Parameters ---------- dt : float timestep u : ArrayIndexer object x-velocity v : ArrayIndexer object y-velocity """myg=self.sim_data.gridif(uisNone)and(visNone):u,v=self.sim_data.get_var("velocity")elifuisNone:u=self.sim_data.get_var("x-velocity")elifvisNone:v=self.sim_data.get_var("y-velocity")forpinself.particles.values():# save old position and predict location at dt/2initial_position=p.pos()u_vel,v_vel=p.interpolate_velocity(myg,u,v)p.update(u_vel,v_vel,0.5*dt)# find velocity at dt/2u_vel,v_vel=p.interpolate_velocity(myg,u,v)# update to final time using original position and vel at dt/2p.x,p.y=initial_positionp.update(u_vel,v_vel,dt)self.enforce_particle_boundaries()
[docs]defenforce_particle_boundaries(self):""" Enforce the particle boundaries TODO: copying the dict and adding everything back again is messy - think of a better way to do this? Did it this way so don't have to remove items from a dictionary while iterating it for outflow boundaries. """old_particles=self.particles.copy()self.particles={}myg=self.sim_data.gridxlb=self.bc.xlbxrb=self.bc.xrbylb=self.bc.ylbyrb=self.bc.yrbwhileold_particles:k,p=old_particles.popitem()# -x boundaryifp.x<myg.xmin:ifxlbin["outflow","neumann"]:continueifxlb=="periodic":p.x=myg.xmax+p.x-myg.xminelifxlbin["reflect-even","reflect-odd","dirichlet"]:p.x=2*myg.xmin-p.xelse:msg.fail("ERROR: xlb = %s invalid BC for particles"%(xlb))# +x boundaryifp.x>myg.xmax:ifxrbin["outflow","neumann"]:continueifxrb=="periodic":p.x=myg.xmin+p.x-myg.xmaxelifxrbin["reflect-even","reflect-odd","dirichlet"]:p.x=2*myg.xmax-p.xelse:msg.fail("ERROR: xrb = %s invalid BC for particles"%(xrb))# -y boundaryifp.y<myg.ymin:ifylbin["outflow","neumann"]:continueifylb=="periodic":p.y=myg.ymax+p.y-myg.yminelifylbin["reflect-even","reflect-odd","dirichlet"]:p.y=2*myg.ymin-p.yelse:msg.fail("ERROR: ylb = %s invalid BC for particles"%(ylb))# +y boundaryifp.y>myg.ymax:ifyrbin["outflow","neumann"]:continueifyrb=="periodic":p.y=myg.ymin+p.y-myg.ymaxelifyrbin["reflect-even","reflect-odd","dirichlet"]:p.y=2*myg.ymax-p.yelse:msg.fail("ERROR: yrb = %s invalid BC for particles"%(yrb))self.particles[k]=pself.n_particles=len(self.particles)
[docs]defget_positions(self):""" Return an array of current particle positions. """returnnp.array([[p.x,p.y]forpinself.particles.values()])
[docs]defget_init_positions(self):""" Return initial positions of the particles as an array. We defined the particles as a dictionary with their initial positions as the keys, so this just becomes a restructuring operation. """returnnp.array([[x,y]for(x,y)inself.particles.keys()])
[docs]defwrite_particles(self,f):""" Output the particles' positions (and initial positions) to an HDF5 file. Parameters ---------- f : h5py object HDF5 file to write to """gparticles=f.create_group("particles")gparticles.create_dataset("init_particle_positions",data=self.get_init_positions())gparticles.create_dataset("particle_positions",data=self.get_positions())