def LCD_by_rejection(pos, vel, sf_par, sf_per, st, en, jj): ''' Takes in a Maxwellian or pseudo-maxwellian distribution. Outputs the number and indexes of any particle inside the loss cone ''' B0x = fields.eval_B0x(pos[st: en]) N_loss = 1 while N_loss > 0: v_perp = np.sqrt(vel[1, st: en] ** 2 + vel[2, st: en] ** 2) N_loss, loss_idx = calc_losses(vel[0, st: en], v_perp, B0x, st=st) # Catch for a particle on the boundary : Set 90 degree pitch angle (gyrophase shouldn't overly matter) if N_loss == 1: if abs(pos[loss_idx[0]]) == const.xmax: ww = loss_idx[0] vel[0, loss_idx[0]] = 0. vel[1, loss_idx[0]] = np.sqrt(vel[0, ww] ** 2 + vel[1, ww] ** 2 + vel[2, ww] ** 2) vel[2, loss_idx[0]] = 0. N_loss = 0 if N_loss != 0: new_vx = np.random.normal(0., sf_par, N_loss) new_vy = np.random.normal(0., sf_per, N_loss) new_vz = np.random.normal(0., sf_per, N_loss) for ii in range(N_loss): vel[0, loss_idx[ii]] = new_vx[ii] vel[1, loss_idx[ii]] = new_vy[ii] vel[2, loss_idx[ii]] = new_vz[ii] return
def init_totally_random(): pos = np.zeros((3, N), dtype=np.float64) vel = np.zeros((3, N), dtype=np.float64) idx = np.ones(N, dtype=np.int8) * -1 np.random.seed(seed) for jj in range(Nj): idx[idx_start[jj]:idx_end[jj]] = jj # Set particle idx # Particle index ranges st = idx_start[jj] en = idx_end[jj] pos[0, st:en] = np.random.uniform(xmin, xmax, en - st) vel[0, st:en] = np.random.normal(0, vth_par[jj], en - st) + drift_v[jj] vel[1, st:en] = np.random.normal(0, vth_perp[jj], en - st) vel[2, st:en] = np.random.normal(0, vth_perp[jj], en - st) print('Initializing particles off-axis') B0x = fields.eval_B0x(pos[0, :en]) v_perp = np.sqrt(vel[1, :en]**2 + vel[2, :en]**2) gyangle = get_gyroangle_array(vel[:, :en]) rL = v_perp / (qm_ratios[idx[:en]] * B0x) pos[1, :en] = rL * np.cos(gyangle) pos[2, :en] = rL * np.sin(gyangle) return pos, vel, idx
def eval_B0_particle(pos, Bp): ''' Calculates the B0 magnetic field at the position of a particle. B0x is non-uniform in space, and B0r (split into y,z components) is the required value to keep div(B) = 0 These values are added onto the existing value of B at the particle location, Bp. B0x is simply equated since we never expect a non-zero wave field in x. ''' constant = -a * B_eq Bp[0] = eval_B0x(pos[0]) Bp[1] += constant * pos[0] * pos[1] Bp[2] += constant * pos[0] * pos[2] return
def eval_B0_exact(x, v, qmi, approx): ''' DIAGNOSTIC FUNCTION Solves (maybe?) the exact solution for By,Bz radial (coupled quadratic equations of their squares) Need to code this manually to separate out the two real solutions. The +det term of the quadratic always seems to be nan? Probably some mathematical reason for this (b < 4ac? Check later) Just return positive results for each for now, but don't assume this will work generally - just for this particle. How to work out if it should be the positive or negative solution (of the sqrt) taken? Use v_perp cross B? Currently using approximation as guide - not the best solution, but it works ''' B0_particle = np.zeros(3) B0_x = eval_B0x(x) K2 = (a * B_eq * x / qmi) ** 2 # Constant for calculation # Quadratic coefficients of R = By ** 2 :: 4 solutions ay = (v[1] ** 2 + v[2] ** 2) * (-K2) by = - v[2] ** 2 * K2 * B0_x ** 2 cy = (v[2] ** 2 * K2) ** 2 B0_y = np.sqrt(solve_quadratic(ay, by, cy)) # Quadratic coefficients of Q = Bz ** 2 :: 4 solutions az = (v[1] ** 2 + v[2] ** 2) * (-K2) bz = - v[1] ** 2 * K2 * B0_x ** 2 cz = (v[1] ** 2 * K2) ** 2 B0_z = np.sqrt(solve_quadratic(az, bz, cz)) # All three of these will always be positive # Need a way to work out how to set +/- status B0_particle[0] = B0_x B0_particle[1] = B0_y B0_particle[2] = B0_z if approx[1] < 0: B0_particle[1] *= -1.0 if approx[2] < 0: B0_particle[2] *= -1.0 return B0_particle
def interpolate_edges_to_center(B, interp, zero_boundaries=False): ''' Used for interpolating values on the B-grid to the E-grid (for E-field calculation) with a 3D array (e.g. B). Second derivative y2 is calculated on the B-grid, with forwards/backwards difference used for endpoints. interp has one more gridpoint than required just because of the array used. interp[-1] should remain zero. This might be able to be done without the intermediate y2 array since the interpolated points don't require previous point values. ADDS B0 TO X-AXIS ON TOP OF INTERPOLATION ''' y2 = np.zeros(B.shape, dtype=nb.float64) interp *= 0. # Calculate second derivative for jj in range(1, B.shape[1]): # Interior B-nodes, Centered difference for ii in range(1, NC): y2[ii, jj] = B[ii + 1, jj] - 2 * B[ii, jj] + B[ii - 1, jj] # Edge B-nodes, Forwards/Backwards difference if zero_boundaries == True: y2[0, jj] = 0. y2[NC, jj] = 0. else: y2[0, jj] = 2 * B[0, jj] - 5 * B[1, jj] + 4 * B[2, jj] - B[3, jj] y2[NC, jj] = 2 * B[NC, jj] - 5 * B[NC - 1, jj] + 4 * B[NC - 2, jj] - B[ NC - 3, jj] # Do spline interpolation: E[ii] is bracketed by B[ii], B[ii + 1] for jj in range(1, B.shape[1]): for ii in range(NC): interp[ii, jj] = 0.5 * (B[ii, jj] + B[ii + 1, jj] + (1 / 6) * (y2[ii, jj] + y2[ii + 1, jj])) # Add B0x to interpolated array for ii in range(NC): interp[ii, 0] = fields.eval_B0x(E_nodes[ii]) # This bit could be removed to allow B0x to vary in green cells naturally # interp[:ND, 0] = interp[ND, 0] # interp[ND+NX+1:, 0] = interp[ND+NX, 0] return
def eval_B0_particle_1D(pos, vel, Bp, qm): ''' Calculates the B0 magnetic field at the position of a particle. B0x is non-uniform in space, and B0r (split into y,z components) is the required value to keep div(B) = 0 These values are added onto the existing value of B at the particle location, Bp. B0x is simply equated since we never expect a non-zero wave field in x. Could totally vectorise this. Would have to change to give a particle_temp array for memory allocation or something ''' Bp[0] = eval_B0x(pos[0]) cyc_fac = a * B_eq * pos[0] / (qm * Bp[0]) Bp[1] += vel[2] * cyc_fac Bp[2] -= vel[1] * cyc_fac return
def eval_B0_particle(x, v, qmi, b1): ''' Calculates the B0 magnetic field at the position of a particle. Neglects B0_r and thus local cyclotron depends only on B0_x. Includes b1 in cyclotron, but since b1 < B0_r, maybe don't? Also, how accurate is this near the equator? ''' B0_xp = np.zeros(3) B0_xp[0] = eval_B0x(x) b1t = np.sqrt(b1[0] ** 2 + b1[1] ** 2 + b1[2] ** 2) l_cyc = qmi * (B0_xp[0] + b1t) fac = a * B_eq * x / l_cyc B0_xp[1] = v[2] * fac B0_xp[2] = -v[1] * fac return B0_xp
def eval_B0_particle(pos, Bp): ''' Calculates the B0 magnetic field at the position of a particle. B0x is non-uniform in space, and B0r (split into y,z components) is the required value to keep div(B) = 0 These values are added onto the existing value of B at the particle location, Bp. B0x is simply equated since we never expect a non-zero wave field in x. Could totally vectorise this. Would have to change to give a particle_temp array for memory allocation or something ''' rL = np.sqrt(pos[1]**2 + pos[2]**2) B0_r = -a * B_eq * pos[0] * rL Bp[0] = eval_B0x(pos[0]) Bp[1] += B0_r * pos[1] / rL Bp[2] += B0_r * pos[2] / rL return
def eval_B0_particle_1D(pos, vel, idx, Bp): ''' Calculates the B0 magnetic field at the position of a particle. B0x is non-uniform in space, and B0r (split into y,z components) is the required value to keep div(B) = 0 These values are added onto the existing value of B at the particle location, Bp. B0x is simply equated since we never expect a non-zero wave field in x. Could totally vectorise this. Would have to change to give a particle_temp array for memory allocation or something ''' Bp[0] = eval_B0x(pos[0]) constant = a * B_eq for ii in range(idx.shape[0]): l_cyc = qm_ratios[idx[ii]] * Bp[0, ii] Bp[1, ii] += constant * pos[0, ii] * vel[2, ii] / l_cyc Bp[2, ii] -= constant * pos[0, ii] * vel[1, ii] / l_cyc return
def position_update(pos, vel, idx, DT, Ie, W_elec): ''' Updates the position of the particles using x = x0 + vt. Also updates particle nearest node and weighting. INPUT: pos -- Particle position array (Also output) vel -- Particle velocity array (Also output for reflection) idx -- Particle index array (Also output for reflection) DT -- Simulation time step Ie -- Particle leftmost to nearest node array (Also output) W_elec -- Particle weighting array (Also output) Note: This function also controls what happens when a particle leaves the simulation boundary. NOTE :: This reinitialization thing is super unoptimized and inefficient. Maybe initialize array of n_ppc samples for each species, to at least vectorize it Count each one being used, start generating new ones if it runs out (but it shouldn't) # 28/05/2020 :: Removed np.abs() and -np.sign() factors from v_x calculation # See if that helps better simulate the distro function (will "lose" more # particles at boundaries, but that'll just slow things down a bit - should # still be valid) ''' pos[0, :] += vel[0, :] * DT pos[1, :] += vel[1, :] * DT pos[2, :] += vel[2, :] * DT # Check Particle boundary conditions: Re-initialize if at edges for ii in nb.prange(pos.shape[1]): if (pos[0, ii] < xmin or pos[0, ii] > xmax): if particle_reinit == 1: # Fix position if pos[0, ii] > xmax: pos[0, ii] = 2 * xmax - pos[0, ii] elif pos[0, ii] < xmin: pos[0, ii] = 2 * xmin - pos[0, ii] # Re-initialize velocity: Vel_x sign so it doesn't go back into boundary sf_per = np.sqrt(kB * Tper[idx[ii]] / mass[idx[ii]]) sf_par = np.sqrt(kB * Tpar[idx[ii]] / mass[idx[ii]]) if temp_type[idx[ii]] == 0: vel[0, ii] = np.random.normal(0, sf_par) vel[1, ii] = np.random.normal(0, sf_per) vel[2, ii] = np.random.normal(0, sf_per) v_perp = np.sqrt(vel[1, ii]**2 + vel[2, ii]**2) else: particle_PA = 0.0 while np.abs(particle_PA) < loss_cone_xmax: vel[0, ii] = (np.random.normal( 0, sf_par)) * (-np.sign(pos[0, ii])) vel[1, ii] = np.random.normal(0, sf_per) vel[2, ii] = np.random.normal(0, sf_per) v_perp = np.sqrt(vel[1, ii]**2 + vel[2, ii]**2) particle_PA = np.arctan( v_perp / vel[0, ii]) # Calculate particle PA's # Don't foget : Also need to reinitialize position gyrophase (pos[1:2]) B0x = eval_B0x(pos[0, ii]) gyangle = init.get_gyroangle_single(vel[:, ii]) rL = v_perp / (qm_ratios[idx[ii]] * B0x) pos[1, ii] = rL * np.cos(gyangle) pos[2, ii] = rL * np.sin(gyangle) elif particle_periodic == 1: # Mario (Periodic) if pos[0, ii] > xmax: pos[0, ii] += xmin - xmax elif pos[0, ii] < xmin: pos[0, ii] += xmax - xmin # Randomise gyrophase: Prevent bunching at initialization if randomise_gyrophase == True: v_perp = np.sqrt(vel[1, ii]**2 + vel[2, ii]**2) theta = np.random.uniform(0, 2 * np.pi) vel[1, ii] = v_perp * np.sin(theta) vel[2, ii] = v_perp * np.cos(theta) elif particle_reflect == 1: # Reflect if pos[0, ii] > xmax: pos[0, ii] = 2 * xmax - pos[0, ii] elif pos[0, ii] < xmin: pos[0, ii] = 2 * xmin - pos[0, ii] vel[0, ii] *= -1.0 else: # DEACTIVATE PARTICLE (Negative index means they're not pushed or counted in sources) idx[ii] -= 128 vel[:, ii] *= 0.0 assign_weighting_TSC(pos, Ie, W_elec) return
def position_update(pos, vel, idx, DT, Ie, W_elec): ''' Updates the position of the particles using x = x0 + vt. Also updates particle nearest node and weighting. INPUT: pos -- Particle position array (Also output) vel -- Particle velocity array (Also output for reflection) idx -- Particle index array (Also output for reflection) DT -- Simulation time step Ie -- Particle leftmost to nearest node array (Also output) W_elec -- Particle weighting array (Also output) Note: This function also controls what happens when a particle leaves the simulation boundary. As per Daughton et al. (2006). ''' N_lost = np.zeros((2, Nj), dtype=np.int64) pos[0, :] += vel[0, :] * DT pos[1, :] += vel[1, :] * DT pos[2, :] += vel[2, :] * DT # Check Particle boundary conditions: Re-initialize if at edges for ii in nb.prange(pos.shape[1]): if idx[ii] >= 0: if (pos[0, ii] < xmin or pos[0, ii] > xmax): if pos[0, ii] < xmin: N_lost[0, idx[ii]] += 1 else: N_lost[1, idx[ii]] += 1 # Move particle to opposite boundary (Periodic) if particle_periodic == 1: if pos[0, ii] > xmax: pos[0, ii] += xmin - xmax elif pos[0, ii] < xmin: pos[0, ii] += xmax - xmin # Random flux initialization at boundary (Open) elif particle_reinit == 1: if pos[0, ii] > xmax: pos[0, ii] = 2 * xmax - pos[0, ii] elif pos[0, ii] < xmin: pos[0, ii] = 2 * xmin - pos[0, ii] if temp_type[idx[ii]] == 0: vel[0, ii] = np.random.normal(0, vth_par[idx[ii]]) vel[1, ii] = np.random.normal(0, vth_perp[idx[ii]]) vel[2, ii] = np.random.normal(0, vth_perp[idx[ii]]) v_perp = np.sqrt(vel[1, ii]**2 + vel[2, ii]**2) else: particle_PA = 0.0 while np.abs(particle_PA) < loss_cone_xmax: vel[0, ii] = np.random.normal(0, vth_par[ idx[ii]]) # * (-1.0) * np.sign(pos[0, ii]) vel[1, ii] = np.random.normal(0, vth_perp[idx[ii]]) vel[2, ii] = np.random.normal(0, vth_perp[idx[ii]]) v_perp = np.sqrt(vel[1, ii]**2 + vel[2, ii]**2) particle_PA = np.arctan( v_perp / vel[0, ii]) # Calculate particle PA's # Don't foget : Also need to reinitialize position gyrophase (pos[1:2]) B0x = eval_B0x(pos[0, ii]) gyangle = init.get_gyroangle_single(vel[:, ii]) rL = v_perp / (qm_ratios[idx[ii]] * B0x) pos[1, ii] = rL * np.cos(gyangle) pos[2, ii] = rL * np.sin(gyangle) # Deactivate particle (Open, default) else: pos[:, ii] *= 0.0 vel[:, ii] *= 0.0 idx[ii] -= 128 print(N_lost[0, :], N_lost[1, :]) assign_weighting_CIC(pos, idx, Ie, W_elec) return
def uniform_config_reverseradix_velocity(): ''' Creates an N-sampled normal distribution across all particle species within each simulation cell OUTPUT: pos -- 3xN array of particle positions. Pos[0] is uniformly distributed with boundaries depending on its temperature type vel -- 3xN array of particle velocities. Each component initialized as a Gaussian with a scale factor determined by the species perp/para temperature idx -- N array of particle indexes, indicating which species it belongs to. Coded as an 8-bit signed integer, allowing values between +/-128 New function using analytic loadings and reverse-radix shuffling. TO DO: -- Check with particle plots, is this random enough? -- Do a run to see if it fixes boundaries -- At some point, have to load loss cone distribution ''' pos = np.zeros((3, const.N), dtype=np.float64) vel = np.zeros((3, const.N), dtype=np.float64) idx = np.ones(const.N, dtype=np.int8) * -1 for jj in range(const.Nj): half_n = const.N_species[ jj] // 2 # Half particles of species - doubled later st = const.idx_start[jj] en = const.idx_start[jj] + half_n # Set position for kk in range(half_n): pos[0, st + kk] = 2 * const.xmax * (float(kk) / (half_n - 1)) pos[0, st:en] -= const.xmax # Set velocity for half: Randomly Maxwellian arr = np.arange(half_n) R_vr = rkbr_uniform_set(arr + 1, base=2) R_theta = rkbr_uniform_set(arr, base=3) R_vrx = rkbr_uniform_set(arr + 1, base=5) vr = const.vth_perp[jj] * np.sqrt(-2 * np.log(R_vr)) vrx = const.vth_par[jj] * np.sqrt(-2 * np.log(R_vrx)) theta = R_theta * np.pi * 2 vel[0, st:en] = vrx * np.sin(theta) + const.drift_v[jj] vel[1, st:en] = vr * np.sin(theta) vel[2, st:en] = vr * np.cos(theta) idx[st:en] = jj pos[0, en:en + half_n] = pos[0, st:en] # Other half, same position vel[0, en:en + half_n] = vel[0, st:en] * 1.0 # Set parallel vel[1, en:en + half_n] = vel[1, st:en] * -1.0 # Invert perp velocities (v2 = -v1) vel[2, en:en + half_n] = vel[2, st:en] * -1.0 idx[st:const.idx_end[jj]] = jj # Set initial Larmor radius - rL from v_perp, distributed to y,z based on velocity gyroangle print('Initializing particles off-axis') B0x = fields.eval_B0x(pos[0, :en]) v_perp = np.sqrt(vel[1, :en]**2 + vel[2, :en]**2) gyangle = get_gyroangle_array(vel[:, :en]) rL = v_perp / (const.qm_ratios[idx[:en]] * B0x) pos[1, :en] = rL * np.cos(gyangle) pos[2, :en] = rL * np.sin(gyangle) return pos, vel, idx
def uniform_gaussian_distribution_ultra_quiet(): '''Creates an N-sampled normal distribution across all particle species within each simulation cell OUTPUT: pos -- 3xN array of particle positions. Pos[0] is uniformly distributed with boundaries depending on its temperature type vel -- 3xN array of particle velocities. Each component initialized as a Gaussian with a scale factor determined by the species perp/para temperature idx -- N array of particle indexes, indicating which species it belongs to. Coded as an 8-bit signed integer, allowing values between +/-128 Same as UGD_Q() but with 4 particles at each spatial point instead of two. This balances it in vx as well as vy (at least initially) and should allow for flux to be more equal? ''' pos = np.zeros((3, const.N), dtype=np.float64) vel = np.zeros((3, const.N), dtype=np.float64) idx = np.ones( const.N, dtype=np.int8) * -1 # Start all particles as disabled (idx < 0) np.random.seed(const.seed) for jj in range(const.Nj): quart_n = const.nsp_ppc[ jj] // 4 # Quarter of particles per cell - quaded later if const.temp_type[ jj] == 0: # Change how many cells are loaded between cold/warm populations NC_load = const.NX else: if const.rc_hwidth == 0 or const.rc_hwidth > const.NX // 2: # Need to change this to be something like the FWHM or something NC_load = const.NX else: NC_load = 2 * const.rc_hwidth # Load particles in each applicable cell acc = 0 offset = 0 for ii in range(NC_load): # Add particle if last cell (for symmetry) if ii == NC_load - 1: quart_n += 1 offset = 1 # Particle index ranges st = const.idx_start[jj] + acc en = const.idx_start[jj] + acc + quart_n # Set position for half: Analytically uniform for kk in range(quart_n): pos[0, st + kk] = const.dx * (float(kk) / (quart_n - offset) + ii) # Turn [0, NC] distro into +/- NC/2 distro pos[0, st:en] -= NC_load * const.dx / 2 # Set velocity for half: Randomly Maxwellian vel[0, st:en] = np.random.normal(0, const.vth_par[jj], quart_n) + const.drift_v[jj] vel[1, st:en] = np.random.normal(0, const.vth_perp[jj], quart_n) vel[2, st:en] = np.random.normal(0, const.vth_perp[jj], quart_n) idx[st:en] = jj # Turn particle on # Set Loss Cone Distribution: Reinitialize particles in loss cone (move to a function) if const.homogenous == False and const.temp_type[jj] == 1: LCD_by_rejection(pos, vel, st, en, jj) # Quiet start : Initialize second half of v_perp vel[0, en:en + quart_n] = vel[0, st:en] * 1.0 # Set parallel pos[0, en:en + quart_n] = pos[0, st:en] # Other half, same position vel[1, en:en + quart_n] = vel[ 1, st:en] * -1.0 # Invert perp velocities (v2 = -v1) vel[2, en:en + quart_n] = vel[2, st:en] * -1.0 idx[en:en + quart_n] = jj # Turn particle on # Quieter start : Initialize second half of v_para en2 = en + quart_n vel[0, en2:en2 + 2 * quart_n] = vel[0, st:en2] * -1.0 # Set anti-parallel pos[0, en2:en2 + 2 * quart_n] = pos[0, st:en2] # Same positions vel[1, en2:en2 + 2 * quart_n] = vel[1, st:en2] # Same perpendicular velocities vel[2, en2:en2 + 2 * quart_n] = vel[2, st:en2] idx[en2:en2 + 2 * quart_n] = jj # Turn particle on acc += quart_n * 4 # Set initial Larmor radius - rL from v_perp, distributed to y,z based on velocity gyroangle print('Initializing particles off-axis') B0x = fields.eval_B0x(pos[0, :acc]) v_perp = np.sqrt(vel[1, :acc]**2 + vel[2, :acc]**2) gyangle = get_gyroangle_array(vel[:, :acc]) rL = v_perp / (const.qm_ratios[idx[:acc]] * B0x) pos[1, :acc] = rL * np.cos(gyangle) pos[2, :acc] = rL * np.sin(gyangle) return pos, vel, idx
def uniform_config_random_velocity_gaussian_T(): '''Creates an N-sampled normal distribution across all particle species within each simulation cell OUTPUT: pos -- 3xN array of particle positions. Pos[0] is uniformly distributed with boundaries depending on its temperature type vel -- 3xN array of particle velocities. Each component initialized as a Gaussian with a scale factor determined by the species perp/para temperature idx -- N array of particle indexes, indicating which species it belongs to. Coded as an 8-bit signed integer, allowing values between +/-128 This one varies temperature by position as a gaussian - i.e. every particle is loaded from a slightly different normal distribution. Because of this, don't bother loading cellwise. Also, Gaussian only applied to hot component. Cold components remain homogenous and isotropic ''' pos = np.zeros((3, const.N), dtype=np.float64) vel = np.zeros((3, const.N), dtype=np.float64) idx = np.ones( const.N, dtype=np.int8) * -1 # Start all particles as disabled (idx < 0) np.random.seed(const.seed) for jj in range(const.Nj): half_n = const.N_species[ jj] // 2 # Half particles of species - doubled later st = const.idx_start[jj] en = const.idx_start[jj] + half_n # Set position for kk in range(half_n): pos[0, st + kk] = 2 * const.xmax * (float(kk) / (half_n - 1)) pos[0, st:en] -= const.xmax idx[st:en] = jj if const.temp_type[jj] == 1: # Set velocity: Position varying temperature (Gaussian) vth_par_gauss, vth_perp_gauss = get_vth_at_x(pos[0, st:en], jj) mu = np.zeros(const.N_species[jj] // 2) # Set velocity for half: Randomly Maxwellian but with varying vth in space vel[0, st:en] = np.random.normal( mu, vth_par_gauss, const.N_species[jj] // 2) + const.drift_v[jj] vel[1, st:en] = np.random.normal(mu, vth_perp_gauss, const.N_species[jj] // 2) vel[2, st:en] = np.random.normal(mu, vth_perp_gauss, const.N_species[jj] // 2) # Set Loss Cone Distribution: Reinitialize particles in loss cone (move to a function) if const.homogenous == False: LCD_by_rejection_varying_vth(pos, vel, st, en, jj, vth_par_gauss, vth_perp_gauss) else: # Set velocity for half: Randomly Maxwellian, isotropic and homogenous vel[0, st:en] = np.random.normal( 0.0, const.vth_par[jj], const.N_species[jj] // 2) + const.drift_v[jj] vel[1, st:en] = np.random.normal(0.0, const.vth_perp[jj], const.N_species[jj] // 2) vel[2, st:en] = np.random.normal(0.0, const.vth_perp[jj], const.N_species[jj] // 2) pos[0, en:en + half_n] = pos[0, st:en] # Other half, same position vel[0, en:en + half_n] = vel[0, st:en] * 1.0 # Set parallel vel[1, en:en + half_n] = vel[1, st:en] * -1.0 # Invert perp velocities (v2 = -v1) vel[2, en:en + half_n] = vel[2, st:en] * -1.0 idx[st:const.idx_end[jj]] = jj # Set initial Larmor radius - rL from v_perp, distributed to y,z based on velocity gyroangle print('Initializing particles off-axis') B0x = fields.eval_B0x(pos[0, :en]) v_perp = np.sqrt(vel[1, :en]**2 + vel[2, :en]**2) gyangle = get_gyroangle_array(vel[:, :en]) rL = v_perp / (const.qm_ratios[idx[:en]] * B0x) pos[1, :en] = rL * np.cos(gyangle) pos[2, :en] = rL * np.sin(gyangle) return pos, vel, idx
def uniform_config_random_velocity(): '''Creates an N-sampled normal distribution across all particle species within each simulation cell OUTPUT: pos -- 3xN array of particle positions. Pos[0] is uniformly distributed with boundaries depending on its temperature type vel -- 3xN array of particle velocities. Each component initialized as a Gaussian with a scale factor determined by the species perp/para temperature idx -- N array of particle indexes, indicating which species it belongs to. Coded as an 8-bit signed integer, allowing values between +/-128 Note: y,z components of particle positions intialized with identical gyrophases, since only the projection onto the x-axis interacts with the simulation fields. pos y,z are kept ONLY to calculate/track the Larmor radius of each particle. This initial position suffers the same issue as trying to update the radial field using B0r = 0 for the Larmor radius approximation, however because this is only an initial condition, at worst this will just cause a variation in the Larmor radius with position in x, but this will at least be conserved throughout the simulation, and not drift with time. CHECK THIS LATER: BUT ITS ONLY AN INITIAL CONDITION SO IT SHOULD BE OK FOR NOW # Could use temp_type[jj] == 1 for RC LCD only ''' pos = np.zeros((3, const.N), dtype=np.float64) vel = np.zeros((3, const.N), dtype=np.float64) idx = np.ones( const.N, dtype=np.int8) * -1 # Start all particles as disabled (idx < 0) np.random.seed(const.seed) for jj in range(const.Nj): half_n = const.nsp_ppc[ jj] // 2 # Half particles per cell - doubled later if const.temp_type[ jj] == 0: # Change how many cells are loaded between cold/warm populations NC_load = const.NX else: if const.rc_hwidth == 0 or const.rc_hwidth > const.NX // 2: # Need to change this to be something like the FWHM or something NC_load = const.NX else: NC_load = 2 * const.rc_hwidth # Load particles in each applicable cell acc = 0 offset = 0 for ii in range(NC_load): # Add particle if last cell (for symmetry) if ii == NC_load - 1: half_n += 1 offset = 1 # Particle index ranges st = const.idx_start[jj] + acc en = const.idx_start[jj] + acc + half_n # Set position for half: Analytically uniform for kk in range(half_n): pos[0, st + kk] = const.dx * (float(kk) / (half_n - offset) + ii) # Turn [0, NC] distro into +/- NC/2 distro pos[0, st:en] -= NC_load * const.dx / 2 # Set velocity for half: Randomly Maxwellian vel[0, st:en] = np.random.normal(0, const.vth_par[jj], half_n) + const.drift_v[jj] vel[1, st:en] = np.random.normal(0, const.vth_perp[jj], half_n) vel[2, st:en] = np.random.normal(0, const.vth_perp[jj], half_n) idx[st:en] = jj # Turn particle on # Set Loss Cone Distribution: Reinitialize particles in loss cone (move to a function) if const.homogenous == False and const.temp_type[jj] == 1: LCD_by_rejection(pos, vel, st, en, jj) # Quiet start : Initialize second half if const.quiet_start == True: vel[0, en:en + half_n] = vel[0, st:en] * 1.0 # Set parallel else: vel[0, en:en + half_n] = vel[0, st:en] * -1.0 # Set anti-parallel pos[0, en:en + half_n] = pos[0, st:en] # Other half, same position vel[1, en:en + half_n] = vel[ 1, st:en] * -1.0 # Invert perp velocities (v2 = -v1) vel[2, en:en + half_n] = vel[2, st:en] * -1.0 idx[en:en + half_n] = jj # Turn particle on acc += half_n * 2 # Set initial Larmor radius - rL from v_perp, distributed to y,z based on velocity gyroangle print('Initializing particles off-axis') B0x = fields.eval_B0x(pos[0, :en]) v_perp = np.sqrt(vel[1, :en]**2 + vel[2, :en]**2) gyangle = get_gyroangle_array(vel[:, :en]) rL = v_perp / (const.qm_ratios[idx[:en]] * B0x) pos[1, :en] = rL * np.cos(gyangle) pos[2, :en] = rL * np.sin(gyangle) return pos, vel, idx
def uniform_gaussian_distribution_quiet(): '''Creates an N-sampled normal distribution across all particle species within each simulation cell OUTPUT: pos -- 3xN array of particle positions. Pos[0] is uniformly distributed with boundaries depending on its temperature type vel -- 3xN array of particle velocities. Each component initialized as a Gaussian with a scale factor determined by the species perp/para temperature idx -- N array of particle indexes, indicating which species it belongs to. Coded as an 8-bit signed integer, allowing values between +/-128 Note: y,z components of particle positions intialized with identical gyrophases, since only the projection onto the x-axis interacts with the simulation fields. pos y,z are kept ONLY to calculate/track the Larmor radius of each particle. This initial position suffers the same issue as trying to update the radial field using B0r = 0 for the Larmor radius approximation, however because this is only an initial condition, at worst this will just cause a variation in the Larmor radius with position in x, but this will at least be conserved throughout the simulation, and not drift with time. CHECK THIS LATER: BUT ITS ONLY AN INITIAL CONDITION SO IT SHOULD BE OK FOR NOW # Could use temp_type[jj] == 1 for RC LCD only ''' pos = np.zeros((3, N), dtype=np.float64) vel = np.zeros((3, N), dtype=np.float64) idx = np.zeros(N, dtype=np.int8) np.random.seed(seed) for jj in range(Nj): idx[idx_start[jj]:idx_end[jj]] = jj # Set particle idx half_n = nsp_ppc[jj] // 2 # Half particles per cell - doubled later sf_par = np.sqrt(kB * Tpar[jj] / mass[jj]) # Scale factors for velocity initialization sf_per = np.sqrt(kB * Tper[jj] / mass[jj]) if temp_type[ jj] == 0: # Change how many cells are loaded between cold/warm populations NC_load = NX else: if rc_hwidth == 0 or rc_hwidth > NX // 2: NC_load = NX else: NC_load = 2 * rc_hwidth # Load particles in each applicable cell acc = 0 offset = 0 for ii in range(NC_load): # Add particle if last cell (for symmetry) if ii == NC_load - 1: half_n += 1 offset = 1 # Particle index ranges st = idx_start[jj] + acc en = idx_start[jj] + acc + half_n # Set position for half: Analytically uniform for kk in range(half_n): pos[0, st + kk] = dx * (float(kk) / (half_n - offset) + ii) # Turn [0, NC] distro into +/- NC/2 distro pos[0, st:en] -= NC_load * dx / 2 # Set velocity for half: Randomly Maxwellian vel[0, st:en] = np.random.normal(0, sf_par, half_n) + drift_v[jj] vel[1, st:en] = np.random.normal(0, sf_per, half_n) vel[2, st:en] = np.random.normal(0, sf_per, half_n) # Set Loss Cone Distribution: Reinitialize particles in loss cone B0x = fields.eval_B0x(pos[0, st:en]) if const.homogenous == False: N_loss = const.N_species[jj] while N_loss > 0: v_perp = np.sqrt(vel[1, st:en]**2 + vel[2, st:en]**2) N_loss, loss_idx = calc_losses(vel[0, st:en], v_perp, B0x, st=st) # Catch for a particle on the boundary : Set 90 degree pitch angle (gyrophase shouldn't overly matter) if N_loss == 1: if abs(pos[0, loss_idx[0]]) == const.xmax: ww = loss_idx[0] vel[0, loss_idx[0]] = 0. vel[1, loss_idx[0]] = np.sqrt(vel[0, ww]**2 + vel[1, ww]**2 + vel[2, ww]**2) vel[2, loss_idx[0]] = 0. N_loss = 0 if N_loss != 0: vel[0, loss_idx] = np.random.normal(0., sf_par, N_loss) vel[1, loss_idx] = np.random.normal(0., sf_per, N_loss) vel[2, loss_idx] = np.random.normal(0., sf_per, N_loss) else: v_perp = np.sqrt(vel[1, st:en]**2 + vel[2, st:en]**2) vel[0, en:en + half_n] = vel[0, st:en] * -1.0 # Invert velocities (v2 = -v1) vel[1, en:en + half_n] = vel[1, st:en] * -1.0 vel[2, en:en + half_n] = vel[2, st:en] * -1.0 pos[0, en:en + half_n] = pos[0, st:en] # Other half, same position acc += half_n * 2 # Set initial Larmor radius - rL from v_perp, distributed to y,z based on velocity gyroangle print('Initializing particles off-axis') B0x = fields.eval_B0x(pos[0]) v_perp = np.sqrt(vel[1]**2 + vel[2]**2) gyangle = get_gyroangle_from_velocity(vel) rL = v_perp / (qm_ratios[idx] * B0x) pos[1] = rL * np.cos(gyangle) pos[2] = rL * np.sin(gyangle) return pos, vel, idx