# Order of the stencil for z derivatives in the Maxwell solver. # Use -1 for infinite order, i.e. for exact dispersion relation in # all direction (adviced for single-GPU/single-CPU simulation). # Use a positive number (and multiple of 2) for a finite-order stencil # (required for multi-GPU/multi-CPU with MPI). A large `n_order` leads # to more overhead in MPI communications, but also to a more accurate # dispersion relation for electromagnetic waves. (Typically, # `n_order = 32` is a good trade-off.) # See https://arxiv.org/abs/1611.05712 for more information. n_order = -1 # Boosted frame gamma_boost = 10. # Boosted frame converter boost = BoostConverter(gamma_boost) # The simulation box Nz = 600 # Number of gridpoints along z zmax = 0.e-6 # Length of the box along z (meters) zmin = -30.e-6 Nr = 75 # Number of gridpoints along r rmax = 150.e-6 # Length of the box along r (meters) Nm = 2 # Number of modes used # The simulation timestep # (See the section Advanced use > Running boosted-frame simulation # of the FBPIC documentation for an explanation of the calculation of dt) dt = min(rmax / (2 * boost.gamma0 * Nr) / c, (zmax - zmin) / Nz / c) # Timestep (seconds)
# (increase this number for a real simulation) # Order of the stencil for z derivatives in the Maxwell solver. # Use -1 for infinite order, i.e. for exact dispersion relation in # all direction (adviced for single-GPU/single-CPU simulation). # Use a positive number (and multiple of 2) for a finite-order stencil # (required for multi-GPU/multi-CPU with MPI). A large `n_order` leads # to more overhead in MPI communications, but also to a more accurate # dispersion relation for electromagnetic waves. (Typically, # `n_order = 32` is a good trade-off.) # See https://arxiv.org/abs/1611.05712 for more information. n_order = -1 # Boosted frame gamma_boost = 15. boost = BoostConverter(gamma_boost) # The laser (conversion to boosted frame is done inside 'add_laser') a0 = 2. # Laser amplitude w0 = 50.e-6 # Laser waist ctau = 5.e-6 # Laser duration z0 = -10.e-6 # Laser centroid zfoc = 0.e-6 # Focal position lambda0 = 0.8e-6 # Laser wavelength # The density profile w_matched = 50.e-6 ramp_up = 5.e-3 plateau = 8.e-2 ramp_down = 5.e-3
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 check_fields(interp1_complex, z, r, info_in_real_part, z0, gamma_b, forward_propagating, show_difference=False): """ Check the real and imaginary part of the interpolation grid agree with the theory by: - Checking that the part (real or imaginary) that does not carry information is zero - Extracting the a0 from the other part and comparing it to the predicted value - Using the extracted value of a0 to compare the simulated profile with a gaussian profile """ # Extract the part that has information if info_in_real_part: interp1 = interp1_complex.real zero_part = interp1_complex.imag else: interp1 = interp1_complex.imag zero_part = interp1_complex.real # Control that the part that has no information is 0 assert np.allclose(0., zero_part, atol=1.e-6 * interp1.max()) # Get the predicted properties of the laser in the boosted frame if gamma_b is None: boost = BoostConverter(1.) else: boost = BoostConverter(gamma_b) ctau_b, lambda0_b, Lprop_b, z0_b = \ boost.copropag_length([ctau, 0.8e-6, Lprop, z0]) # Take into account whether the pulse is propagating forward or backward if not forward_propagating: Lprop_b = -Lprop_b # Fit the on-axis profile to extract a0 def fit_function(z, a0, z0_phase): return (gaussian_laser(z, r[0], a0, z0_phase, z0_b + Lprop_b, ctau_b, lambda0_b)) fit_result = curve_fit(fit_function, z, interp1[:, 0], p0=np.array([a0, z0_b + Lprop_b])) a0_fit, z0_fit = fit_result[0] # Check that the a0 agrees within 5% of the predicted value assert abs(abs(a0_fit) - a0) / a0 < 0.05 # Calculate predicted fields r2d, z2d = np.meshgrid(r, z) # Factor 0.5 due to the definition of the interpolation grid interp1_predicted = gaussian_laser(z2d, r2d, a0_fit, z0_fit, z0_b + Lprop_b, ctau_b, lambda0_b) # Plot the difference if show_difference: import matplotlib.pyplot as plt plt.subplot(311) plt.imshow(interp1.T) plt.colorbar() plt.subplot(312) plt.imshow(interp1_predicted.T) plt.colorbar() plt.subplot(313) plt.imshow((interp1_predicted - interp1).T) plt.colorbar() plt.show() # Control the values (with a precision of 3%) assert np.allclose(interp1_predicted, interp1, atol=3.e-2 * interp1.max())
Nz = 400 # Number of gridpoints along z zmax = 0.e-6 # Length of the box along z (meters) zmin = -20.e-6 Nr = 75 # Number of gridpoints along r rmax = 150.e-6 # Length of the box along r (meters) Nm = 2 # Number of modes used n_guard = 40 # Number of guard cells exchange_period = 10 # The simulation timestep dt = (zmax - zmin) / Nz / c # Timestep (seconds) N_step = 101 # Number of iterations to perform # (increase this number for a real simulation) # Boosted frame gamma_boost = 15. boost = BoostConverter(gamma_boost) # The laser (conversion to boosted frame is done inside 'add_laser') a0 = 2. # Laser amplitude w0 = 50.e-6 # Laser waist ctau = 5.e-6 # Laser duration z0 = -10.e-6 # Laser centroid zfoc = 0.e-6 # Focal position lambda0 = 0.8e-6 # Laser wavelength # The density profile w_matched = 50.e-6 ramp_up = 5.e-3 plateau = 8.e-2 ramp_down = 5.e-3
def add_laser_pulse(sim, laser_profile, gamma_boost=None, method='direct', z0_antenna=None, v_antenna=0.): """ Introduce a laser pulse in the simulation. The laser is either added directly to the interpolation grid initially (method= ``direct``) or it is progressively emitted by an antenna (method= ``antenna``). Parameters ---------- sim: a Simulation object The structure that contains the simulation. laser_profile: a valid laser profile object Laser profiles can be imported from ``fbpic.lpa_utils.laser`` gamma_boost: float, optional When initializing the laser in a boosted frame, set the value of ``gamma_boost`` to the corresponding Lorentz factor. In this case, ``laser_profile`` should be initialized with all its physical quantities (wavelength, etc...) given in the lab frame, and the function ``add_laser_pulse`` will automatically convert these properties to their boosted-frame value. method: string, optional Whether to initialize the laser directly in the box (method= ``direct``) or through a laser antenna (method= ``antenna``). Default: ``direct`` z0_antenna: float, optional (meters) Required for the ``antenna`` method: initial position (in the lab frame) of the antenna. v_antenna: float, optional (meters per second) Only used for the ``antenna`` method: velocity of the antenna (in the lab frame) Example ------- In order to initialize a Laguerre-Gauss profile with a waist of 5 microns and a duration of 30 femtoseconds, centered at :math:`z=3` microns initially: :: from fbpic.lpa_utils.laser import add_laser_pulse, LaguerreGaussLaser profile = LaguerreGaussLaser(a0=0.5, waist=5.e-6, tau=30.e-15, z0=3.e-6, p=1, m=0) add_laser_pulse( sim, profile ) """ # Prepare the boosted frame converter if (gamma_boost is not None) and (gamma_boost != 1.): if laser_profile.propag_direction == 1: boost = BoostConverter(gamma_boost) else: raise ValueError('For now, backward-propagating lasers ' 'cannot be used in the boosted-frame.') else: boost = None # Handle the introduction method of the laser if method == 'direct': # Directly add the laser to the interpolation object add_laser_direct(sim, laser_profile, boost) elif method == 'antenna': # Add a laser antenna to the simulation object if z0_antenna is None: raise ValueError('You need to provide `z0_antenna`.') dr = sim.fld.interp[0].dr Nr = sim.fld.interp[0].Nr Nm = sim.fld.Nm sim.laser_antennas.append( LaserAntenna(laser_profile, z0_antenna, v_antenna, dr, Nr, Nm, boost, use_cuda=sim.use_cuda)) else: raise ValueError('Unknown laser method: %s' % method)
def add_laser(sim, a0, w0, ctau, z0, zf=None, lambda0=0.8e-6, cep_phase=0., phi2_chirp=0., theta_pol=0., gamma_boost=None, method='direct', fw_propagating=True, update_spectral=True, z0_antenna=None): """ Introduce a linearly-polarized, Gaussian laser in the simulation The laser is either added directly to the interpolation grid initially (method=`direct`) or it is progressively emitted by an antenna (method=`antenna`) Parameters ---------- sim: a Simulation object The structure that contains the simulation. a0: float (unitless) The a0 of the pulse at focus (in the lab-frame). w0: float (in meters) The waist of the pulse at focus. ctau: float (in meters) The pulse length (in the lab-frame), defined as: E_laser ~ exp( - (z-ct)**2 / ctau**2 ) z0: float (in meters) The position of the laser centroid relative to z=0 (in the lab-frame). zf: float (in meters), optional The position of the laser focus relative to z=0 (in the lab-frame). Default: the laser focus is at z0. lambda0: float (in meters), optional The central wavelength of the laser (in the lab-frame). Default: 0.8 microns (Ti:Sapph laser). cep_phase: float (rad) Carrier Enveloppe Phase (CEP), i.e. the phase of the laser oscillations, at the position where the laser enveloppe is maximum. phi2_chirp: float (in second^2) The amount of temporal chirp, at focus *in the lab frame* Namely, a wave packet centered on the frequency (w0 + dw) will reach its peak intensity at a time z(dw) = z0 - c*phi2*dw. Thus, a positive phi2 corresponds to positive chirp, i.e. red part of the spectrum in the front of the pulse and blue part of the spectrum in the back. theta_pol: float (in radians), optional The angle of polarization with respect to the x axis. Default: 0 rad. gamma_boost: float, optional When initializing the laser in a boosted frame, set the value of `gamma_boost` to the corresponding Lorentz factor. All the other quantities (ctau, zf, etc.) are to be given in the lab frame. method: string, optional Whether to initialize the laser directly in the box (method=`direct`) or through a laser antenna (method=`antenna`) fw_propagating: bool, optional Only for the `direct` method: Wether the laser is propagating in the forward or backward direction. update_spectral: bool, optional Only for the `direct` method: Wether to update the fields in spectral space after modifying the fields on the interpolation grid. z0_antenna: float, optional (meters) Only for the `antenna` method: initial position (in the lab frame) of the antenna. If not provided, then the z0_antenna is set to zf. """ # Set a number of parameters for the laser k0 = 2 * np.pi / lambda0 E0 = a0 * m_e * c**2 * k0 / e # Amplitude at focus # Set default focusing position and laser antenna position if zf is None: zf = z0 if z0_antenna is None: z0_antenna = z0 # Prepare the boosted frame converter if (gamma_boost is not None) and (fw_propagating == True): boost = BoostConverter(gamma_boost) else: boost = None # Handle the introduction method of the laser if method == 'direct': # Directly add the laser to the interpolation object add_laser_direct(sim.fld, E0, w0, ctau, z0, zf, k0, cep_phase, phi2_chirp, theta_pol, fw_propagating, update_spectral, boost) elif method == 'antenna': dr = sim.fld.interp[0].dr Nr = sim.fld.interp[0].Nr Nm = sim.fld.Nm # Add a laser antenna to the simulation object sim.laser_antennas.append( LaserAntenna(E0, w0, ctau, z0, zf, k0, cep_phase, phi2_chirp, theta_pol, z0_antenna, dr, Nr, Nm, boost=boost)) else: raise ValueError('Unknown laser method: %s' % method)
def run_simulation(gamma_boost, show): """ Run a simulation with a relativistic electron bunch crosses a laser Parameters ---------- gamma_boost: float The Lorentz factor of the frame in which the simulation is carried out. show: bool Whether to show a plot of the angular distribution """ # Boosted frame boost = BoostConverter(gamma_boost) # The simulation timestep diag_period = 100 N_step = 101 # Number of iterations to perform # Calculate timestep to resolve the interaction with enough points laser_duration_boosted, = boost.copropag_length([laser_duration], beta_object=-1) bunch_sigma_z_boosted, = boost.copropag_length([bunch_sigma_z], beta_object=1) dt = (4 * laser_duration_boosted + bunch_sigma_z_boosted / c) / N_step # Initialize the simulation object zmax, zmin = boost.copropag_length([zmax_lab, zmin_lab], beta_object=1.) sim = Simulation(Nz, zmax, Nr, rmax, Nm, dt, p_zmin=0, p_zmax=0, p_rmin=0, p_rmax=0, p_nz=1, p_nr=1, p_nt=1, n_e=1, dens_func=None, zmin=zmin, boundaries='periodic', use_cuda=use_cuda) # Remove particles that were previously created sim.ptcl = [] print('Initialized simulation') # Add electron bunch (automatically converted to boosted-frame) add_elec_bunch_gaussian(sim, sig_r=1.e-6, sig_z=bunch_sigma_z, n_emit=0., gamma0=gamma_bunch_mean, sig_gamma=gamma_bunch_rms, Q=Q_bunch, N=N_bunch, tf=0.0, zf=0.5 * (zmax + zmin), boost=boost) elec = sim.ptcl[0] print('Initialized electron bunch') # Add a photon species photons = Particles(q=0, m=0, n=0, Npz=1, zmin=0, zmax=0, Npr=1, rmin=0, rmax=0, Nptheta=1, dt=sim.dt, ux_m=0., uy_m=0., uz_m=0., ux_th=0., uy_th=0., uz_th=0., dens_func=None, continuous_injection=False, grid_shape=sim.fld.interp[0].Ez.shape, particle_shape='linear', use_cuda=sim.use_cuda) sim.ptcl.append(photons) print('Initialized photons') # Activate Compton scattering for electrons of the bunch elec.activate_compton(target_species=photons, laser_energy=laser_energy, laser_wavelength=laser_wavelength, laser_waist=laser_waist, laser_ctau=laser_ctau, laser_initial_z0=laser_initial_z0, ratio_w_electron_photon=50, boost=boost) print('Activated Compton') # Add diagnostics if write_hdf5: sim.diags = [ ParticleDiagnostic(diag_period, species={ 'electrons': elec, 'photons': photons }, comm=sim.comm) ] # Get initial total momentum initial_total_elec_px = (elec.w * elec.ux).sum() * m_e * c initial_total_elec_py = (elec.w * elec.uy).sum() * m_e * c initial_total_elec_pz = (elec.w * elec.uz).sum() * m_e * c ### Run the simulation for species in sim.ptcl: species.send_particles_to_gpu() for i_step in range(N_step): for species in sim.ptcl: species.halfpush_x() elec.handle_elementary_processes(sim.time + 0.5 * sim.dt) for species in sim.ptcl: species.halfpush_x() # Increment time and run diagnostics sim.time += sim.dt sim.iteration += 1 for diag in sim.diags: diag.write(sim.iteration) # Print fraction of photons produced if i_step % 10 == 0: for species in sim.ptcl: species.receive_particles_from_gpu() simulated_frac = photons.w.sum() / elec.w.sum() for species in sim.ptcl: species.send_particles_to_gpu() print( 'Iteration %d: Photon fraction per electron = %f' \ %(i_step, simulated_frac) ) for species in sim.ptcl: species.receive_particles_from_gpu() # Check estimation of photon fraction check_photon_fraction(simulated_frac) # Check conservation of momentum (is only conserved ) if elec.compton_scatterer.ratio_w_electron_photon == 1: check_momentum_conservation(gamma_boost, photons, elec, initial_total_elec_px, initial_total_elec_py, initial_total_elec_pz) # Transform the photon momenta back into the lab frame photon_u = 1. / photons.inv_gamma photon_lab_pz = boost.gamma0 * (photons.uz + boost.beta0 * photon_u) photon_lab_p = boost.gamma0 * (photon_u + boost.beta0 * photons.uz) # Plot the scaled angle and frequency if show: import matplotlib.pyplot as plt # Bin the photons on a grid in frequency and angle freq_min = 0.5 freq_max = 1.2 N_freq = 500 gammatheta_min = 0. gammatheta_max = 1. N_gammatheta = 100 hist_range = [[freq_min, freq_max], [gammatheta_min, gammatheta_max]] extent = [freq_min, freq_max, gammatheta_min, gammatheta_max] fundamental_frequency = 4 * gamma_bunch_mean**2 * c / laser_wavelength photon_scaled_freq = photon_lab_p * c / (h * fundamental_frequency) gamma_theta = gamma_bunch_mean * np.arccos( photon_lab_pz / photon_lab_p) grid, freq_bins, gammatheta_bins = np.histogram2d( photon_scaled_freq, gamma_theta, weights=photons.w, range=hist_range, bins=[N_freq, N_gammatheta]) # Normalize by solid angle, frequency and number of photons dw = (freq_bins[1] - freq_bins[0]) * 2 * np.pi * fundamental_frequency dtheta = (gammatheta_bins[1] - gammatheta_bins[0]) / gamma_bunch_mean domega = 2. * np.pi * np.sin( gammatheta_bins / gamma_bunch_mean) * dtheta grid /= dw * domega[np.newaxis, 1:] * elec.w.sum() grid = np.where(grid == 0, np.nan, grid) plt.imshow(grid.T, origin='lower', extent=extent, cmap='gist_earth', aspect='auto', vmax=1.8e-16) plt.title('Particles, $d^2N/d\omega \,d\Omega$') plt.xlabel('Scaled energy ($\omega/4\gamma^2\omega_\ell$)') plt.ylabel(r'$\gamma \theta$') plt.colorbar() # Plot theory plt.plot(1. / (1 + gammatheta_bins**2), gammatheta_bins, color='r') plt.show() plt.clf()
def test_boosted_output(gamma_boost=10.): """ # TODO Parameters ---------- gamma_boost: float The Lorentz factor of the frame in which the simulation is carried out. """ # The simulation box Nz = 500 # Number of gridpoints along z zmax_lab = 0.e-6 # Length of the box along z (meters) zmin_lab = -20.e-6 Nr = 10 # Number of gridpoints along r rmax = 10.e-6 # Length of the box along r (meters) Nm = 2 # Number of modes used # Number of timesteps N_steps = 500 diag_period = 20 # Period of the diagnostics in number of timesteps dt_lab = (zmax_lab - zmin_lab) / Nz * 1. / c T_sim_lab = N_steps * dt_lab # Move into directory `tests` os.chdir('./tests') # Initialize the simulation object sim = Simulation( Nz, zmax_lab, Nr, rmax, Nm, dt_lab, 0, 0, # No electrons get created because we pass p_zmin=p_zmax=0 0, rmax, 1, 1, 4, n_e=0, zmin=zmin_lab, initialize_ions=False, gamma_boost=gamma_boost, v_comoving=-0.9999 * c, boundaries='open', use_cuda=use_cuda) sim.set_moving_window(v=c) # Remove the electron species sim.ptcl = [] # Add a Gaussian electron bunch # Note: the total charge is 0 so all fields should remain 0 # throughout the simulation. As a consequence, the motion of the beam # is a mere translation. N_particles = 3000 add_elec_bunch_gaussian(sim, sig_r=1.e-6, sig_z=1.e-6, n_emit=0., gamma0=100, sig_gamma=0., Q=0., N=N_particles, zf=0.5 * (zmax_lab + zmin_lab), boost=BoostConverter(gamma_boost)) sim.ptcl[0].track(sim.comm) # openPMD diagnostics sim.diags = [ BackTransformedParticleDiagnostic(zmin_lab, zmax_lab, v_lab=c, dt_snapshots_lab=T_sim_lab / 3., Ntot_snapshots_lab=3, gamma_boost=gamma_boost, period=diag_period, fldobject=sim.fld, species={"bunch": sim.ptcl[0]}, comm=sim.comm) ] # Run the simulation sim.step(N_steps) # Check consistency of the back-transformed openPMD diagnostics: # Make sure that all the particles were retrived by checking particle IDs ts = OpenPMDTimeSeries('./lab_diags/hdf5/') ref_pid = np.sort(sim.ptcl[0].tracker.id) for iteration in ts.iterations: pid, = ts.get_particle(['id'], iteration=iteration) pid = np.sort(pid) assert len(pid) == N_particles assert np.all(ref_pid == pid) # Remove openPMD files shutil.rmtree('./lab_diags/') os.chdir('../')
Nr = 100 # Number of gridpoints along r rmax = 100.e-6 # Length of the box along r (meters) Nm = 2 # Number of modes used n_order = -1 # The order of the stencil in z # The simulation timestep dt = (zmax - zmin) / Nz / c # Timestep (seconds) N_step = 800 # Number of iterations to perform N_show = 40 # Number of timestep between every plot show_fields = False l_boost = False if l_boost: # Boosted frame gamma_boost = 15. boost = BoostConverter(gamma_boost) else: gamma_boost = 1. boost = None # Initialize the simulation object sim = Simulation(Nz, zmax, Nr, rmax, Nm, dt, -1.e-6, 0., -1.e-6, 0.,
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)
use_cuda = True # The simulation box Nz = 100 zmax = 0.e-6 zmin = -20.e-6 Nr = 200 rmax = 20.e-6 Nm = 1 # The simulation timestep dt = (zmax - zmin) / Nz / c N_step = 101 # Boosted frame gamma_boost = 15. boost = BoostConverter(gamma_boost) # The bunch sigma_r = 1.e-6 sigma_z = 3.e-6 Q = 200.e-12 gamma0 = 100. sigma_gamma = 0. n_emit = 0.1e-6 z_focus = 2000.e-6 z0 = -10.e-6 N = 40000 # The diagnostics diag_period = 5 Ntot_snapshot_lab = 21
def __init__(self, interp, comm, ptcl, v, p_nz, time, ux_m=0., uy_m=0., uz_m=0., ux_th=0., uy_th=0., uz_th=0., gamma_boost=None): """ Initializes a moving window object. Parameters ---------- interp: a list of Interpolation objects Contains the positions of the boundaries comm: a BoundaryCommunicator object Contains information about the MPI decomposition and about the longitudinal boundaries ptcl: a list of Particle objects Needed in order to infer the position of injection of the particles by the moving window. v: float (meters per seconds), optional The speed of the moving window p_nz: int Number of macroparticles per cell along the z direction time: float (seconds) The time (in the simulation) at which the moving window was initialized ux_m, uy_m, uz_m: floats (dimensionless) Normalized mean momenta of the injected particles in each direction ux_th, uy_th, uz_th: floats (dimensionless) Normalized thermal momenta in each direction gamma_boost : float, optional When initializing the laser in a boosted frame, set the value of `gamma_boost` to the corresponding Lorentz factor. (uz_m is to be given in the lab frame ; for the moment, this will not work if any of ux_th, uy_th, uz_th, ux_m, uy_m is nonzero) """ # Check that the boundaries are open if ((comm.rank == comm.size-1) and (comm.right_proc is not None)) \ or ((comm.rank == 0) and (comm.left_proc is not None)): raise ValueError( 'The simulation is using a moving window, but ' 'the boundaries are periodic.\n Please select open ' 'boundaries when initializing the Simulation object.') # Momenta parameters self.ux_m = ux_m self.uy_m = uy_m self.uz_m = uz_m self.ux_th = ux_th self.uy_th = uy_th self.uz_th = uz_th # When running the simulation in boosted frame, convert the arguments if gamma_boost is not None: boost = BoostConverter(gamma_boost) self.uz_m, = boost.longitudinal_momentum([self.uz_m]) # Attach moving window speed and period self.v = v self.exchange_period = comm.exchange_period # Attach reference position of moving window (only for the first proc) # (Determines by how many cells the window should be moved) if comm.rank == 0: self.zmin = interp[0].zmin # Attach injection position and speed (only for the last proc) if comm.rank == comm.size - 1: ng = comm.n_guard self.z_inject = interp[0].zmax - ng / 2 * interp[0].dz # Try to detect the position of the end of the plasma: # Find the maximal position of the particles which are # continously injected. self.z_end_plasma = None for species in ptcl: if species.continuous_injection and species.Ntot != 0: # Add half of the spacing between particles (the injection # function itself will add a half-spacing again) self.z_end_plasma = species.z.max( ) + 0.5 * interp[0].dz / p_nz break # Default value in the absence of continuously-injected particles if self.z_end_plasma is None: self.z_end_plasma = self.z_inject self.v_end_plasma = \ c * self.uz_m / np.sqrt(1 + ux_m**2 + uy_m**2 + self.uz_m**2) self.nz_inject = 0 self.p_nz = p_nz # Attach time of last move self.t_last_move = time
def __init__(self, interp, comm, dt, ptcl, v, p_nz, time, ux_m=0., uy_m=0., uz_m=0., ux_th=0., uy_th=0., uz_th=0., gamma_boost=None): """ Initializes a moving window object. Parameters ---------- interp: a list of Interpolation objects Contains the positions of the boundaries comm: a BoundaryCommunicator object Contains information about the MPI decomposition and about the longitudinal boundaries dt: float The timestep of the simulation. ptcl: a list of Particle objects Needed in order to infer the position of injection of the particles by the moving window. v: float (meters per seconds), optional The speed of the moving window p_nz: int Number of macroparticles per cell along the z direction time: float (seconds) The time (in the simulation) at which the moving window was initialized ux_m, uy_m, uz_m: floats (dimensionless) Normalized mean momenta of the injected particles in each direction ux_th, uy_th, uz_th: floats (dimensionless) Normalized thermal momenta in each direction gamma_boost : float, optional When initializing the laser in a boosted frame, set the value of `gamma_boost` to the corresponding Lorentz factor. (uz_m is to be given in the lab frame ; for the moment, this will not work if any of ux_th, uy_th, uz_th, ux_m, uy_m is nonzero) """ # Check that the boundaries are open if ((comm.rank == comm.size-1) and (comm.right_proc is not None)) \ or ((comm.rank == 0) and (comm.left_proc is not None)): raise ValueError( 'The simulation is using a moving window, but ' 'the boundaries are periodic.\n Please select open ' 'boundaries when initializing the Simulation object.') # Momenta parameters self.ux_m = ux_m self.uy_m = uy_m self.uz_m = uz_m self.ux_th = ux_th self.uy_th = uy_th self.uz_th = uz_th # When running the simulation in boosted frame, convert the arguments if gamma_boost is not None: boost = BoostConverter(gamma_boost) self.uz_m, = boost.longitudinal_momentum([self.uz_m]) # Attach moving window speed and period self.v = v # Get the positions of the global physical domain zmin_global_domain, zmax_global_domain = comm.get_zmin_zmax( local=False, with_damp=False, with_guard=False) # Attach reference position of moving window (only for the first proc) # (Determines by how many cells the window should be moved) if comm.rank == 0: self.zmin = zmin_global_domain # Attach injection position and speed (only for the last proc) if comm.rank == comm.size - 1: self.v_end_plasma = \ c * self.uz_m / np.sqrt(1 + ux_m**2 + uy_m**2 + self.uz_m**2) # Initialize plasma *ahead* of the right *physical* boundary of # the box so, after `exchange_period` iterations # (without adding new plasma), there will still be plasma # inside the physical domain. ( +3 takes into account that 3 more # cells need to be filled w.r.t the left edge of the physical box # such that the last cell inside the box is always correct for # 1st and 3rd order shape factor particles after the moving window # shifted by exchange_period cells. ) self.z_inject = zmax_global_domain + 3*comm.dz + \ comm.exchange_period * (v-self.v_end_plasma) * dt # Try to detect the position of the end of the plasma: # Find the maximal position of the particles which are # continously injected. self.z_end_plasma = None for species in ptcl: if species.continuous_injection and species.Ntot != 0: # Add half of the spacing between particles (the injection # function itself will add a half-spacing again) self.z_end_plasma = species.z.max() + 0.5 * comm.dz / p_nz break # Default value in the absence of continuously-injected particles if self.z_end_plasma is None: self.z_end_plasma = zmax_global_domain self.nz_inject = 0 self.p_nz = p_nz # Attach time of last move self.t_last_move = time - dt
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/')