def add_applied_field(self, applied_field): # Call method of parent class PICMI_Simulation.add_applied_field(self, applied_field) if type(applied_field) == PICMI_Mirror: assert applied_field.z_front_location is not None mirror = Mirror(z_lab=applied_field.z_front_location, n_cells=applied_field.number_of_cells, gamma_boost=self.fbpic_sim.boost.gamma0) self.fbpic_sim.mirrors.append(mirror) elif type(applied_field) == PICMI_ConstantAppliedField: # TODO: Handle bounds for field_name in ['Ex', 'Ey', 'Ez', 'Bx', 'By', 'Bz']: field_value = getattr(applied_field, field_name) if field_value is None: continue def field_func(F, x, y, z, t, amplitude, length_scale): return (F + amplitude * field_value) # Pass it to FBPIC self.fbpic_sim.external_fields.append( ExternalField(field_func, field_name, 1., 0.)) elif type(applied_field) == PICMI_AnalyticAppliedField: # TODO: Handle bounds for field_name in ['Ex', 'Ey', 'Ez', 'Bx', 'By', 'Bz']: # Extract expression and execute it inside a function definition expression = getattr(applied_field, field_name + '_expression') if expression is None: continue fieldfunc = None define_function_code = \ """def fieldfunc( F, x, y, z, t, amplitude, length_scale ):\n return( F + amplitude * %s )""" %expression # Take into account user-defined variables for k in applied_field.user_defined_kw: define_function_code = \ "%s = %s\n" %(k,applied_field.user_defined_kw[k]) \ + define_function_code exec(define_function_code, globals()) # Pass it to FBPIC self.fbpic_sim.external_fields.append( ExternalField(fieldfunc, field_name, 1., 0.)) else: raise ValueError("Unrecognized `applied_field` type.")
def run_simulation(gamma_boost, use_separate_electron_species): """ Run a simulation with a laser pulse going through a gas jet of ionizable N5+ atoms, and check the fraction of atoms that are in the N5+ state. Parameters ---------- gamma_boost: float The Lorentz factor of the frame in which the simulation is carried out. use_separate_electron_species: bool Whether to use separate electron species for each level, or a single electron species for all levels. """ # The simulation box zmax_lab = 20.e-6 # Length of the box along z (meters) zmin_lab = 0.e-6 Nr = 3 # Number of gridpoints along r rmax = 10.e-6 # Length of the box along r (meters) Nm = 2 # Number of modes used # The particles of the plasma p_zmin = 5.e-6 # Position of the beginning of the plasma (meters) p_zmax = 15.e-6 p_rmin = 0. # Minimal radial position of the plasma (meters) p_rmax = 100.e-6 # Maximal radial position of the plasma (meters) n_atoms = 0.2 # The atomic density is chosen very low, # to avoid collective effects p_nz = 2 # Number of particles per cell along z p_nr = 1 # Number of particles per cell along r p_nt = 4 # Number of particles per cell along theta # Boosted frame boost = BoostConverter(gamma_boost) # Boost the different quantities beta_boost = np.sqrt(1. - 1. / gamma_boost**2) zmin, zmax = boost.static_length([zmin_lab, zmax_lab]) p_zmin, p_zmax = boost.static_length([p_zmin, p_zmax]) n_atoms, = boost.static_density([n_atoms]) # Increase the number of particles per cell in order to keep sufficient # statistics for the evaluation of the ionization fraction if gamma_boost > 1: p_nz = int(2 * gamma_boost * (1 + beta_boost) * p_nz) # The laser a0 = 1.8 # Laser amplitude lambda0_lab = 0.8e-6 # Laser wavelength # Boost the laser wavelength before calculating the laser amplitude lambda0, = boost.copropag_length([lambda0_lab], beta_object=1.) # Duration and initial position of the laser ctau = 10. * lambda0 z0 = -2 * ctau # Calculate laser amplitude omega = 2 * np.pi * c / lambda0 E0 = a0 * m_e * c * omega / e B0 = E0 / c def laser_func(F, x, y, z, t, amplitude, length_scale): """ Function that describes a Gaussian laser with infinite waist """ return( F + amplitude * math.cos( 2*np.pi*(z-c*t)/lambda0 ) * \ math.exp( - (z - c*t - z0)**2/ctau**2 ) ) # Resolution and number of timesteps dz = lambda0 / 16. dt = dz / c Nz = int((zmax - zmin) / dz) + 1 N_step = int( (2. * 40. * lambda0 + zmax - zmin) / (dz * (1 + beta_boost))) + 1 # Get the speed of the plasma uz_m, = boost.longitudinal_momentum([0.]) v_plasma, = boost.velocity([0.]) # The diagnostics diag_period = N_step - 1 # Period of the diagnostics in number of timesteps # Initial ionization level of the Nitrogen atoms level_start = 2 # Initialize the simulation object, with the neutralizing electrons # No particles are created because we do not pass the density sim = Simulation(Nz, zmax, Nr, rmax, Nm, dt, zmin=zmin, v_comoving=v_plasma, use_galilean=False, boundaries='open', use_cuda=use_cuda) # Add the charge-neutralizing electrons elec = sim.add_new_species(q=-e, m=m_e, n=level_start * n_atoms, p_nz=p_nz, p_nr=p_nr, p_nt=p_nt, p_zmin=p_zmin, p_zmax=p_zmax, p_rmin=p_rmin, p_rmax=p_rmax, continuous_injection=False, uz_m=uz_m) # Add the N atoms ions = sim.add_new_species(q=0, m=14. * m_p, n=n_atoms, p_nz=p_nz, p_nr=p_nr, p_nt=p_nt, p_zmin=p_zmin, p_zmax=p_zmax, p_rmin=p_rmin, p_rmax=p_rmax, continuous_injection=False, uz_m=uz_m) # Add the target electrons if use_separate_electron_species: # Use a dictionary of electron species: one per ionizable level target_species = {} level_max = 6 # N can go up to N7+, but here we stop at N6+ for i_level in range(level_start, level_max): target_species[i_level] = sim.add_new_species(q=-e, m=m_e) else: # Use the pre-existing, charge-neutralizing electrons target_species = elec level_max = None # Default is going up to N7+ # Define ionization ions.make_ionizable(element='N', level_start=level_start, level_max=level_max, target_species=target_species) # Set the moving window sim.set_moving_window(v=v_plasma) # Add a laser to the fields of the simulation (external fields) sim.external_fields = [ ExternalField(laser_func, 'Ex', E0, 0.), ExternalField(laser_func, 'By', B0, 0.) ] # Add a particle diagnostic sim.diags = [ ParticleDiagnostic( diag_period, {"ions": ions}, particle_data=["position", "gamma", "weighting", "E", "B"], # Test output of fields and gamma for standard # (non-boosted) particle diagnostics write_dir='tests/diags', comm=sim.comm) ] if gamma_boost > 1: T_sim_lab = (2. * 40. * lambda0_lab + zmax_lab - zmin_lab) / c sim.diags.append( BackTransformedParticleDiagnostic(zmin_lab, zmax_lab, v_lab=0., dt_snapshots_lab=T_sim_lab / 2., Ntot_snapshots_lab=3, gamma_boost=gamma_boost, period=diag_period, fldobject=sim.fld, species={"ions": ions}, comm=sim.comm, write_dir='tests/lab_diags')) # Run the simulation sim.step(N_step, use_true_rho=True) # Check the fraction of N5+ ions at the end of the simulation w = ions.w ioniz_level = ions.ionizer.ionization_level # Get the total number of N atoms/ions (all ionization levels together) ntot = w.sum() # Get the total number of N5+ ions n_N5 = w[ioniz_level == 5].sum() # Get the fraction of N5+ ions, and check that it is close to 0.32 N5_fraction = n_N5 / ntot print('N5+ fraction: %.4f' % N5_fraction) assert ((N5_fraction > 0.30) and (N5_fraction < 0.34)) # When different electron species are created, check the fraction of # each electron species if use_separate_electron_species: for i_level in range(level_start, level_max): n_N = w[ioniz_level == i_level].sum() assert np.allclose(target_species[i_level].w.sum(), n_N) # Check consistency in the regular openPMD diagnostics ts = OpenPMDTimeSeries('./tests/diags/hdf5/') last_iteration = ts.iterations[-1] w, q = ts.get_particle(['w', 'charge'], species="ions", iteration=last_iteration) # Check that the openPMD file contains the same number of N5+ ions n_N5_openpmd = np.sum(w[(4.5 * e < q) & (q < 5.5 * e)]) assert np.isclose(n_N5_openpmd, n_N5) # Remove openPMD files shutil.rmtree('./tests/diags/') # Check consistency of the back-transformed openPMD diagnostics if gamma_boost > 1.: ts = OpenPMDTimeSeries('./tests/lab_diags/hdf5/') last_iteration = ts.iterations[-1] w, q = ts.get_particle(['w', 'charge'], species="ions", iteration=last_iteration) # Check that the openPMD file contains the same number of N5+ ions n_N5_openpmd = np.sum(w[(4.5 * e < q) & (q < 5.5 * e)]) assert np.isclose(n_N5_openpmd, n_N5) # Remove openPMD files shutil.rmtree('./tests/lab_diags/')
def test_external_laser_field(show=False): "Function that is run by py.test, when doing `python setup.py test`" # Initialize the simulation sim = Simulation(Nz, zmax, Nr, rmax, Nm, dt, p_zmin, p_zmax, 0, p_rmax, p_nz, p_nr, p_nt, n, initialize_ions=False, zmin=zmin, use_cuda=use_cuda, boundaries='periodic') # Add the external fields sim.external_fields = [ ExternalField(laser_func, 'Ex', a0 * m_e * c**2 * k0 / e, lambda0), ExternalField(laser_func, 'By', a0 * m_e * c * k0 / e, lambda0) ] # Prepare the arrays for the time history of the pusher Nptcl = sim.ptcl[0].Ntot x = np.zeros((N_step, Nptcl)) y = np.zeros((N_step, Nptcl)) z = np.zeros((N_step, Nptcl)) ux = np.zeros((N_step, Nptcl)) uz = np.zeros((N_step, Nptcl)) uy = np.zeros((N_step, Nptcl)) # Prepare the particles with proper transverse and longitudinal momentum sim.ptcl[0].ux = a0 * np.sin(k0 * sim.ptcl[0].z) sim.ptcl[0].uz[:] = 0.5 * sim.ptcl[0].ux**2 # Push the particles over N_step and record the corresponding history for i in range(N_step): # Record the history x[i, :] = sim.ptcl[0].x[:] y[i, :] = sim.ptcl[0].y[:] z[i, :] = sim.ptcl[0].z[:] ux[i, :] = sim.ptcl[0].ux[:] uy[i, :] = sim.ptcl[0].uy[:] uz[i, :] = sim.ptcl[0].uz[:] # Take a simulation step sim.step(1) # Compute the analytical solution t = dt * np.arange(N_step) # Conservation of ux ux_analytical = np.zeros((N_step, Nptcl)) uz_analytical = np.zeros((N_step, Nptcl)) for i in range(N_step): ux_analytical[i, :] = a0 * np.sin(k0 * (z[i, :] - c * t[i])) uz_analytical[i, :] = 0.5 * ux_analytical[i, :]**2 # Show the results if show: import matplotlib.pyplot as plt plt.figure(figsize=(10, 5)) plt.subplot(211) plt.plot(t, ux_analytical, '--') plt.plot(t, ux, 'o') plt.xlabel('t') plt.ylabel('ux') plt.subplot(212) plt.plot(t, uz_analytical, '--') plt.plot(t, uz, 'o') plt.xlabel('t') plt.ylabel('uz') plt.show() else: assert np.allclose(ux, ux_analytical, atol=5.e-2) assert np.allclose(uz, uz_analytical, atol=5.e-2)
def run_external_laser_field_simulation(show, gamma_boost=None): """ Runs a simulation with a set of particles whose motion corresponds to that of a particle that is initially at rest (in the lab frame) before being reached by a plane wave (propagating to the right) In the lab frame, the motion is given by ux = a0 sin ( k0(z-ct) ) uz = ux^2 / 2 (from the conservation of gamma - uz) In the boosted frame, the motion is given by ux = a0 sin ( k0 gamma0 (1-beta0) (z-ct) ) uz = - gamma0 beta0 + gamma0 (1-beta0) ux^2 / 2 """ # Time parameters dt = lambda0 / c / 200 # 200 points per laser period N_step = 400 # Two laser periods # Initialize BoostConverter object if gamma_boost is None: boost = BoostConverter(gamma0=1.) else: boost = BoostConverter(gamma_boost) # Reduce time resolution, for the case of a boosted simulation if gamma_boost is not None: dt = dt * (1. + boost.beta0) / boost.gamma0 # Initialize the simulation sim = Simulation(Nz, zmax, Nr, rmax, Nm, dt, initialize_ions=False, zmin=zmin, use_cuda=use_cuda, boundaries='periodic', gamma_boost=gamma_boost) # Add electrons sim.ptcl = [] sim.add_new_species(-e, m_e, n=n, p_rmax=p_rmax, p_nz=p_nz, p_nr=p_nr, p_nt=p_nt) # Add the external fields sim.external_fields = [ ExternalField(laser_func, 'Ex', a0 * m_e * c**2 * k0 / e, lambda0, gamma_boost=gamma_boost), ExternalField(laser_func, 'By', a0 * m_e * c * k0 / e, lambda0, gamma_boost=gamma_boost) ] # Prepare the arrays for the time history of the pusher Nptcl = sim.ptcl[0].Ntot x = np.zeros((N_step, Nptcl)) y = np.zeros((N_step, Nptcl)) z = np.zeros((N_step, Nptcl)) ux = np.zeros((N_step, Nptcl)) uy = np.zeros((N_step, Nptcl)) uz = np.zeros((N_step, Nptcl)) # Prepare the particles with proper transverse and longitudinal momentum, # at t=0 in the simulation frame k0p = k0 * boost.gamma0 * (1. - boost.beta0) sim.ptcl[0].ux = a0 * np.sin(k0p * sim.ptcl[0].z) sim.ptcl[0].uz[:] = -boost.gamma0*boost.beta0 \ + boost.gamma0*(1-boost.beta0)*0.5*sim.ptcl[0].ux**2 # Push the particles over N_step and record the corresponding history for i in range(N_step): # Record the history x[i, :] = sim.ptcl[0].x[:] y[i, :] = sim.ptcl[0].y[:] z[i, :] = sim.ptcl[0].z[:] ux[i, :] = sim.ptcl[0].ux[:] uy[i, :] = sim.ptcl[0].uy[:] uz[i, :] = sim.ptcl[0].uz[:] # Take a simulation step sim.step(1) # Compute the analytical solution t = sim.dt * np.arange(N_step) # Conservation of ux ux_analytical = np.zeros((N_step, Nptcl)) uz_analytical = np.zeros((N_step, Nptcl)) for i in range(N_step): ux_analytical[i, :] = a0 * np.sin(k0p * (z[i, :] - c * t[i])) uz_analytical[i,:] = -boost.gamma0*boost.beta0 \ + boost.gamma0*(1-boost.beta0)*0.5*ux_analytical[i,:]**2 # Show the results if show: import matplotlib.pyplot as plt plt.figure(figsize=(10, 5)) plt.subplot(211) plt.plot(t, ux_analytical, '--') plt.plot(t, ux, 'o') plt.xlabel('t') plt.ylabel('ux') plt.subplot(212) plt.plot(t, uz_analytical, '--') plt.plot(t, uz, 'o') plt.xlabel('t') plt.ylabel('uz') plt.show() else: assert np.allclose(ux, ux_analytical, atol=5.e-2) assert np.allclose(uz, uz_analytical, atol=5.e-2)
def run_simulation(gamma_boost): """ Run a simulation with a laser pulse going through a gas jet of ionizable N5+ atoms, and check the fraction of atoms that are in the N5+ state. Parameters ---------- gamma_boost: float The Lorentz factor of the frame in which the simulation is carried out. """ # The simulation box zmax_lab = 20.e-6 # Length of the box along z (meters) zmin_lab = 0.e-6 Nr = 3 # Number of gridpoints along r rmax = 10.e-6 # Length of the box along r (meters) Nm = 2 # Number of modes used # The particles of the plasma p_zmin = 5.e-6 # Position of the beginning of the plasma (meters) p_zmax = 15.e-6 p_rmin = 0. # Minimal radial position of the plasma (meters) p_rmax = 100.e-6 # Maximal radial position of the plasma (meters) n_e = 1. # The plasma density is chosen very low, # to avoid collective effects p_nz = 2 # Number of particles per cell along z p_nr = 1 # Number of particles per cell along r p_nt = 4 # Number of particles per cell along theta # Boosted frame boost = BoostConverter(gamma_boost) # Boost the different quantities beta_boost = np.sqrt(1. - 1. / gamma_boost**2) zmin, zmax = boost.static_length([zmin_lab, zmax_lab]) p_zmin, p_zmax = boost.static_length([p_zmin, p_zmax]) n_e, = boost.static_density([n_e]) # Increase the number of particles per cell in order to keep sufficient # statistics for the evaluation of the ionization fraction if gamma_boost > 1: p_nz = int(2 * gamma_boost * (1 + beta_boost) * p_nz) # The laser a0 = 1.8 # Laser amplitude lambda0_lab = 0.8e-6 # Laser wavelength # Boost the laser wavelength before calculating the laser amplitude lambda0, = boost.copropag_length([lambda0_lab], beta_object=1.) # Duration and initial position of the laser ctau = 10. * lambda0 z0 = -2 * ctau # Calculate laser amplitude omega = 2 * np.pi * c / lambda0 E0 = a0 * m_e * c * omega / e B0 = E0 / c def laser_func(F, x, y, z, t, amplitude, length_scale): """ Function that describes a Gaussian laser with infinite waist """ return( F + amplitude * math.cos( 2*np.pi*(z-c*t)/lambda0 ) * \ math.exp( - (z - c*t - z0)**2/ctau**2 ) ) # Resolution and number of timesteps dz = lambda0 / 16. dt = dz / c Nz = int((zmax - zmin) / dz) + 1 N_step = int( (2. * 40. * lambda0 + zmax - zmin) / (dz * (1 + beta_boost))) + 1 # Get the speed of the plasma uz_m, = boost.longitudinal_momentum([0.]) v_plasma, = boost.velocity([0.]) # The diagnostics diag_period = N_step - 1 # Period of the diagnostics in number of timesteps # Initialize the simulation object sim = Simulation( Nz, zmax, Nr, rmax, Nm, dt, p_zmax, p_zmax, # No electrons get created because we pass p_zmin=p_zmax p_rmin, p_rmax, p_nz, p_nr, p_nt, n_e, zmin=zmin, initialize_ions=False, v_comoving=v_plasma, use_galilean=False, boundaries='open', use_cuda=use_cuda) sim.set_moving_window(v=v_plasma) # Add the N atoms p_zmin, p_zmax, Npz = adapt_to_grid(sim.fld.interp[0].z, p_zmin, p_zmax, p_nz) p_rmin, p_rmax, Npr = adapt_to_grid(sim.fld.interp[0].r, p_rmin, p_rmax, p_nr) sim.ptcl.append( Particles(q=e, m=14. * m_p, n=0.2 * n_e, Npz=Npz, zmin=p_zmin, zmax=p_zmax, Npr=Npr, rmin=p_rmin, rmax=p_rmax, Nptheta=p_nt, dt=dt, use_cuda=use_cuda, uz_m=uz_m, grid_shape=sim.fld.interp[0].Ez.shape, continuous_injection=False)) sim.ptcl[1].make_ionizable(element='N', level_start=0, target_species=sim.ptcl[0]) # Add a laser to the fields of the simulation (external fields) sim.external_fields = [ ExternalField(laser_func, 'Ex', E0, 0.), ExternalField(laser_func, 'By', B0, 0.) ] # Add a field diagnostic sim.diags = [ ParticleDiagnostic(diag_period, {"ions": sim.ptcl[1]}, write_dir='tests/diags', comm=sim.comm) ] if gamma_boost > 1: T_sim_lab = (2. * 40. * lambda0_lab + zmax_lab - zmin_lab) / c sim.diags.append( BoostedParticleDiagnostic(zmin_lab, zmax_lab, v_lab=0., dt_snapshots_lab=T_sim_lab / 2., Ntot_snapshots_lab=3, gamma_boost=gamma_boost, period=diag_period, fldobject=sim.fld, species={"ions": sim.ptcl[1]}, comm=sim.comm, write_dir='tests/lab_diags')) # Run the simulation sim.step(N_step, use_true_rho=True) # Check the fraction of N5+ ions at the end of the simulation w = sim.ptcl[1].w ioniz_level = sim.ptcl[1].ionizer.ionization_level # Get the total number of N atoms/ions (all ionization levels together) ntot = w.sum() # Get the total number of N5+ ions n_N5 = w[ioniz_level == 5].sum() # Get the fraction of N5+ ions, and check that it is close to 0.32 N5_fraction = n_N5 / ntot print('N5+ fraction: %.4f' % N5_fraction) assert ((N5_fraction > 0.30) and (N5_fraction < 0.34)) # Check consistency in the regular openPMD diagnostics ts = OpenPMDTimeSeries('./tests/diags/hdf5/') last_iteration = ts.iterations[-1] w, q = ts.get_particle(['w', 'charge'], species="ions", iteration=last_iteration) # Check that the openPMD file contains the same number of N5+ ions n_N5_openpmd = np.sum(w[(4.5 * e < q) & (q < 5.5 * e)]) assert np.isclose(n_N5_openpmd, n_N5) # Remove openPMD files shutil.rmtree('./tests/diags/') # Check consistency of the back-transformed openPMD diagnostics if gamma_boost > 1.: ts = OpenPMDTimeSeries('./tests/lab_diags/hdf5/') last_iteration = ts.iterations[-1] w, q = ts.get_particle(['w', 'charge'], species="ions", iteration=last_iteration) # Check that the openPMD file contains the same number of N5+ ions n_N5_openpmd = np.sum(w[(4.5 * e < q) & (q < 5.5 * e)]) assert np.isclose(n_N5_openpmd, n_N5) # Remove openPMD files shutil.rmtree('./tests/lab_diags/')