Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ionization module #260

Merged
merged 60 commits into from
Aug 21, 2019
Merged

Ionization module #260

merged 60 commits into from
Aug 21, 2019

Conversation

MaxThevenet
Copy link
Member

@MaxThevenet MaxThevenet commented Aug 8, 2019

This PR proposes an implementation for field ionization (using the ADK model) in WarpX. The main points are:

Ionization energies and initialization

The tables for ionization energies were taken from the NIST Atomic Spectra Database. This is handled with:

  • Utils/atomic_data.txt: the text file generated by NIST website.
  • Utils/write_atomic_data_cpp.py: a python script that reads atomic_data.txt and extracts ionization levels into a C++ file (see line below).
  • Utils/IonizationEnergiesTable.H: C++ file with ionization tables data and metadata.
  • PhysicalParticleContainer::InitIonizationModule handles the init of the ionization module. The mapping between ionizable species and their product species is done at the MultiParticleContainer level in mapSpeciesProduct.

Creation of new electrons

As ionization is a two-species process, it is handled at the MultiParticleContainer in MultiParticleContainer::doFieldIonization, in a 2-step approach. Within each tile:

  • An ionization mask is built (1 if particle is ionized, 0 otherwise) in PhysicalParticleContainer::buildIonizationMask.
  • The mapping between indices of ionized ions (source) and the corresponding created electron (product) is done via a prefix sum of the ionization mask (using amrex::Gpu::inclusive_scan for portability). Particle quantities are copied from source to product in createIonizedParticles.

Peripheral changes

Ionization required a number of peripheral changes, listed below:

  • The particle pusher and current deposition needed to be modified to account for ionization level. This is done with a new argument, ion_lev, that is a pointer to the array of ionization levels when ionization is on (nullptr otherwise).
  • The ionization level is the first runtime integer particle attribute in WarpX. This required some small additions in WarpXParticleContainer (GetiAttribs is the equivalent of GetAttribs for int attributes, and the mapping between the names and indices of int attributes is done in particle_icomps, similar to particle_comps for Real attributes).

Caveats

  • In principle, some energy should be extracted from the electromagnetic fields when ionization occurs. This can be done by depositing some current to account for the energy taken by the ionization process, but it is not included in this PR.
  • Only one single-level ionization can occur per ion per time step.
  • The ionization is currently done at the beginning of each time step. For more accuracy, it should be done right after the field gather (and after a half-push in position to have all particle data synchronized).

Tests

This implementation was tested for plasma mirror in 2D and 3D (the image below shows the reflection of a laser pulse on a silicon target with 1 electron ionized per ion initially, in 2d). The same simulation was run on GPU, and gave similar image (and ionization does not trigger CPU-GPU exchanges at each iteration). Other tests were done for laser-plasma acceleration, in the lab frame and in the boosted frame.
Screen Shot 2019-08-08 at 4 00 24 PM

@MaxThevenet
Copy link
Member Author

MaxThevenet commented Aug 12, 2019

Thanks for starting this review @rlehe. I implemented your comments in a bunch of small commits.

I enabled ionization when sub cycling is on, and ran the automated inputs.rt test with:

  • MR, no sub-cycling (3200 iterations)
  • MR, sub-cycling (1600 iterations)

This required changes in the input file:

amr.max_level = 1
warpx.fine_tag_lo = -3.e-6    6.e-6 
warpx.fine_tag_hi =  3.e-6   11.e-6
warpx.do_subcycling = 0

In both cases, the patch occupies half of the plasma and the test is successful.

I can add these to the automated tests if you think it is useful, though they take a little bit of time.

Copy link
Member

@RemiLehe RemiLehe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for this PR.
Here are a few more comments.
Also: did you test the code on GPU (using the Python automatic tests)?

ions.physical_element = N
ions.do_continuous_injection=1

electrons.mass = m_p
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be m_e?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented the changes you suggested in the input files.

ions.ionization_product_species = electrons
ions.physical_element = N

electrons.mass = m_p
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m_e?

particles.nspecies = 2
particles.species_names = electrons ions

ions.mass = m_p
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, this should be ions.mass = 14*m_p (approximate value), this the ions are Nitrogen.

particles.nspecies = 2
particles.species_names = electrons ions

ions.mass = m_p
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14*m_p?

@@ -0,0 +1,79 @@
max_step = 500
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this file be removed?
It does not seem to be used in the automated tests.

@@ -26,6 +26,7 @@ void doDepositionShapeN(const amrex::Real * const xp,
const amrex::Real * const uxp,
const amrex::Real * const uyp,
const amrex::Real * const uzp,
const int * const ion_lev,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add the documentation from ion_lev in the above Doxygen description?
In particular, could you mention that:

  • This is needed in order to have the charge of each macroparticle (since q is a scalar here).
  • For non-ionizable species, ion_lev is a null pointer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments added.

@@ -36,6 +37,7 @@ void doDepositionShapeN(const amrex::Real * const xp,
const amrex::Real stagger_shift,
const amrex::Real q)
{
const bool do_ionization = ion_lev;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment saying that this line checks whether ion_lev is a null pointer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments added.

amrex::Real wq = q*wp[ip];
if (do_ionization){
wq *= ion_lev[ip];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is maybe a bit dangerous, because here, the charge q is directly the charge that the user enters in the input script. Now for ionizable species, users might be a bit confused as to what charge should be entered (e.g. they might think that <species>.charge should be 0, if the ionizable species is initially neutral).
For this reason, it might be better if wq is independent of the user-entered charge, in the case of an ionizable species:

             if (do_ionization){
                wq = wp[ip] * ion_lev[ip] * PhysConst::q_e;
            } else {
                wq = wp[ip] * q;
            }

An alternative would be to explicitly check that the user-entered charge is q_e for an ionizable species.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented the second option: if the user specifies a charge that is not q_e, the charge is over-written and the code raises a Warning.

{
// For particle i in mfi, if is_ionized[i]=1, copy particle
// particle i from container pc_source into pc_product
static void createIonizedParticles (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be static void and in a namespace?
Could this go into a separate file, for ionization?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the static keyword

@RemiLehe
Copy link
Member

Also: could you add the modified ChargeDeposition (taking into account the ion_lev) to this PR?

amrex::Real wq = q*wp[ip];
if (do_ionization){
wq *= ion_lev[ip];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See remark below, in the Esirkepov kernel.

@@ -179,6 +184,7 @@ void doEsirkepovDepositionShapeN (const amrex::Real * const xp,
const amrex::Real * const uxp,
const amrex::Real * const uyp,
const amrex::Real * const uzp,
const int * ion_lev,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark as before: could you add the doc for ion_lev?

const int tile_id = mfi.LocalTileIndex();
pc_product->GetParticles(lev)[std::make_pair(grid_id,tile_id)];
pc_product->DefineAndReturnParticleTile(lev, grid_id, tile_id);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaxThevenet Could you add a comment in the above code, explaining why you need to touch each tile, and briefly explaining what DefineAndReturnParticleTile does?

[=] AMREX_GPU_DEVICE (long i) {
Real qp = q;
if (ion_lev){ qp *= ion_lev[i]; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark as for the current deposition:
maybe it is safer to do

if (ion_lev) {
    qp = ion_lev[i] * PhysConst::q_e;
} else {
    qp = q;
}

[=] AMREX_GPU_DEVICE (long i) {
Real qp = q;
if (ion_lev){ qp *= ion_lev[i]; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same remark

pp.query("ionization_initial_level", ionization_initial_level);
pp.get("ionization_product_species", ionization_product_name);
pp.get("physical_element", physical_element);
// Add Real component for ionization level
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this comment be Add integer component for ionization level ?

pp.get("physical_element", physical_element);
// Add Real component for ionization level
AddIntComp("ionization_level");
plot_flags.resize(PIdx::nattribs + 1, 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the above line really needed? It seems to me that there is already a similar line in the ParticleIO.cpp file...

@@ -0,0 +1,67 @@
import re, os
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a string at the beginning of this file, explaining its purpose?
(e.g. this could be copy/pasted from this PR's description)

// The advantage of inclusive_scan is that the sum of is_ionized
// is in the last element, so no other reduction is required to get
// number of particles.
amrex::Gpu::inclusive_scan(is_ionized.begin(), is_ionized.end(), i_product.begin());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment here, saying that Gpu::inclusive_scan runs on the current GPU stream, and synchronizes with the CPU, so that the next line (executed by the CPU) has the updated values of i_product?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added.

@MaxThevenet
Copy link
Member Author

@RemiLehe Thanks for your remarks, I implemented the changes. A few comments:

  • For now, let's keep createIonizesParticles in MultiParticleContainer.cpp, and we'll update it after @atmyers makes changes to get rid of runtime components.
  • The code throws a warning if the user does not use charge=q_e for ionizable species, and the charge is overwritten
  • an error is raised if ionization is on when compiled for GPU, due to conflict between recent Redistribute and runtime particle components.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants