Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Examples/Tests/collision/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ add_warpx_test(
OFF # dependency
)

add_warpx_test(
test_2d_charge_exchange_dsmc # name
2 # dims
1 # nprocs
inputs_test_2d_charge_exchange_dsmc # inputs
"analysis_test_2d_collisions_split_momentum_push.py --path diags/reducedfiles/" # analysis
"analysis_default_regression.py --path diags/diag1/" # checksum
OFF # dependency
)

add_warpx_test(
test_2d_collisions_split_momentum_push_electrostatic # name
2 # dims
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def analyze(args: argparse.Namespace) -> None:
num_particles_per_cell_array = np.array(
[int(nppc) for nppc in num_particles_per_cell_list]
)
num_species = len(input_dict["particles.species_names"])
num_particles_per_cell = np.prod(num_particles_per_cell_array)
electrostatic = (
True
Expand All @@ -52,9 +53,9 @@ def analyze(args: argparse.Namespace) -> None:
else False
)
if electrostatic:
equipartition_value = 1 / (6 * num_particles_per_cell + 1)
equipartition_value = 1 / (3 * num_species * num_particles_per_cell + 1)
else:
equipartition_value = 5 / (6 * num_particles_per_cell + 5)
equipartition_value = 5 / (3 * num_species * num_particles_per_cell + 5)

# normalize the field energy variation
electron_temperature = float(input_dict["my_constants.Te"][0])
Expand Down Expand Up @@ -110,7 +111,7 @@ def analyze(args: argparse.Namespace) -> None:

# verify the total energy conservation
total_energy_error_norm = np.max(np.abs(total_energy_error))
relative_tolerance = 1e-5 if electrostatic else 6e-5
relative_tolerance = 1.1e-5 if electrostatic else 6e-5
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The new test requires the tolerance to be slightly relaxed.

if total_energy_error_norm >= relative_tolerance:
raise ValueError(
f"Total energy conservation failed with a maximum relative error of {total_energy_error_norm}"
Expand Down
125 changes: 125 additions & 0 deletions Examples/Tests/collision/inputs_test_2d_charge_exchange_dsmc
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# 2D charge exchange collision test (DSMC), with electrostatic solver.
# Electrons (neutralizing background) + He+ ions + He neutrals.
# He+ and He undergo DSMC charge exchange using the He resonant cross-section.
Comment thread
RemiLehe marked this conversation as resolved.
Outdated
# The electrostatic solver is active; total energy (field + particle) conservation
# is verified using analysis_test_2d_collisions_split_momentum_push.py.

# algo
algo.field_gathering = energy-conserving
algo.particle_shape = 2

# amr
amr.max_level = 0
amr.n_cell = 16 16

# boundary
boundary.field_hi = periodic periodic
boundary.field_lo = periodic periodic
boundary.particle_hi = periodic periodic
boundary.particle_lo = periodic periodic

# collisions
collisions.collision_names = cx_col
cx_col.type = dsmc
cx_col.scattering_processes = charge_exchange
cx_col.species = Heplus He
cx_col.product_species = He Heplus
cx_col.charge_exchange_cross_section = ../../../../warpx-data/MCC_cross_sections/He/charge_exchange.dat

# diagnostics
diagnostics.diags_names = diag1
diag1.diag_type = Full
diag1.format = openpmd
diag1.intervals = 2000
diag1.fields_to_plot = Ex Ey Ez Bx By Bz jx jy jz rho
diag1.write_species = 1
diag1.species = electrons Heplus He
diag1.electrons.variables = w x z ux uy uz
diag1.Heplus.variables = w x z ux uy uz
diag1.He.variables = w x z ux uy uz

# electrons (neutralizing background for He+)
electrons.charge = -q_e
electrons.density = n0
electrons.injection_style = "NUniformPerCell"
electrons.mass = m_e
electrons.momentum_distribution_type = gaussian
electrons.num_particles_per_cell_each_dim = 8 8
electrons.profile = constant
electrons.species_type = electron
electrons.ux_th = sqrt(Te*q_e/m_e)/clight
electrons.uy_th = sqrt(Te*q_e/m_e)/clight
electrons.uz_th = sqrt(Te*q_e/m_e)/clight
electrons.xmax = 10*de0
electrons.xmin = 0
electrons.zmax = 10*de0
electrons.zmin = 0

# Heplus (He+ ions undergoing charge exchange)
Heplus.charge = q_e
Heplus.density = n0
Heplus.injection_style = "NUniformPerCell"
Heplus.mass = m_He
Heplus.momentum_distribution_type = gaussian
Heplus.num_particles_per_cell_each_dim = 8 8
Heplus.profile = constant
Heplus.ux_th = sqrt(Ti*q_e/m_He)/clight
Heplus.uy_th = sqrt(Ti*q_e/m_He)/clight
Heplus.uz_th = sqrt(Ti*q_e/m_He)/clight
Heplus.xmax = 10*de0
Heplus.xmin = 0
Heplus.zmax = 10*de0
Heplus.zmin = 0

# He (neutral He atoms undergoing charge exchange)
He.charge = 0
He.density = n0
He.injection_style = "NUniformPerCell"
He.mass = m_He
He.momentum_distribution_type = gaussian
He.num_particles_per_cell_each_dim = 8 8
He.profile = constant
He.ux_th = sqrt(Ti*q_e/m_He)/clight
He.uy_th = sqrt(Ti*q_e/m_He)/clight
He.uz_th = sqrt(Ti*q_e/m_He)/clight
He.xmax = 10*de0
He.xmin = 0
He.zmax = 10*de0
He.zmin = 0

# reduced diagnostics
field_energy.intervals = 1
field_energy.type = FieldEnergy
particle_energy.intervals = 1
particle_energy.type = ParticleEnergy

# geometry
geometry.dims = 2
geometry.prob_hi = 10*de0 10*de0
geometry.prob_lo = 0. 0.

# interpolation
interpolation.galerkin_scheme = 1

# max_step
max_step = 2000

# my_constants
my_constants.m_He = 4*m_p # He mass, kg
my_constants.n0 = 1e30 # number density, m^-3
my_constants.Te = 100. # electron temperature, eV
my_constants.Ti = 100. # He ion/neutral temperature, eV
my_constants.wpe = q_e*sqrt(n0/(m_e*epsilon0)) # electron plasma frequency, rad/s
my_constants.de0 = clight/wpe # electron skin depth, m
my_constants.dt = 0.1/wpe # time step (electron timescale), s

# particles
particles.species_names = electrons Heplus He

# warpx
warpx.const_dt = dt
warpx.do_electrostatic = labframe
warpx.reduced_diags_names = field_energy particle_energy
warpx.serialize_initial_conditions = 1
warpx.use_filter = 0
warpx.verbose = 1
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"lev=0": {
"rho": 0.0007701705979872025
"rho": 0.0007712764213131624
},
"H": {
"particle_position_x": 0.0,
"particle_position_y": 0.0,
"particle_position_z": 2530.8479274154333,
"particle_position_z": 2530.062259029084,
"particle_momentum_x": 0.0,
"particle_momentum_y": 0.0,
"particle_momentum_z": 5.948838493809116e-17,
Expand All @@ -23,7 +23,7 @@
"H2plus": {
"particle_position_x": 0.0,
"particle_position_y": 0.0,
"particle_position_z": 1021.6452859116588,
"particle_position_z": 1023.4987864862421,
"particle_momentum_x": 0.0,
"particle_momentum_y": 0.0,
"particle_momentum_z": 9.753884936303964e-28,
Expand All @@ -38,4 +38,4 @@
"particle_momentum_z": 5.434778911325365e-18,
"particle_weight": 1104206441820.484
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"lev=0": {
"Bx": 0.0,
"By": 0.0,
"Bz": 0.0,
"Ex": 14448333080954.348,
"Ey": 0.0,
"Ez": 17169859243066.512,
"jx": 7.803855689663588e+18,
"jy": 7.215854549193545e+18,
"jz": 6.576572210896873e+18,
"rho": 53358037967.6566
},
"He": {
"particle_position_x": 0.000435321087901117,
"particle_position_y": 0.0,
"particle_position_z": 0.00043532495740775497,
"particle_momentum_x": 4.2993604934866694e-18,
"particle_momentum_y": 4.284989545488133e-18,
"particle_momentum_z": 4.295691097769843e-18,
"particle_weight": 2823958725036869.5
},
"Heplus": {
"particle_position_x": 0.00043531587816417113,
"particle_position_y": 0.0,
"particle_position_z": 0.0004353330259468823,
"particle_momentum_x": 4.306240128575128e-18,
"particle_momentum_y": 4.269568445141001e-18,
"particle_momentum_z": 4.2453148876079736e-18,
"particle_weight": 2823958725036869.5
},
"electrons": {
"particle_position_x": 0.00043485561435631804,
"particle_position_y": 0.0,
"particle_position_z": 0.0004366002075857368,
"particle_momentum_x": 4.9302740059601715e-20,
"particle_momentum_y": 5.076441659627337e-20,
"particle_momentum_z": 4.984100225110185e-20,
"particle_weight": 2823958725036869.5
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ public:
[=] AMREX_GPU_DEVICE (index_type i) -> index_type {
return ((mask[i] > 0) &
(mask[i] != int(ScatteringProcessType::IONIZATION)) &
(mask[i] != int(ScatteringProcessType::TWOPRODUCT_REACTION))) ? 1 : 0;
(mask[i] != int(ScatteringProcessType::TWOPRODUCT_REACTION)) &
(mask[i] != int(ScatteringProcessType::CHARGE_EXCHANGE))) ? 1 : 0;
},
[=] AMREX_GPU_DEVICE (index_type i, index_type s) { no_product_offsets_data[i] = s; },
amrex::Scan::Type::exclusive, amrex::Scan::retSum
Expand All @@ -167,7 +168,8 @@ public:
auto const with_product_total = amrex::Scan::PrefixSum<index_type>(n_total_pairs,
[=] AMREX_GPU_DEVICE (index_type i) -> index_type {
return ((mask[i] == int(ScatteringProcessType::IONIZATION)) |
(mask[i] == int(ScatteringProcessType::TWOPRODUCT_REACTION))) ? 1 : 0;
(mask[i] == int(ScatteringProcessType::TWOPRODUCT_REACTION)) |
(mask[i] == int(ScatteringProcessType::CHARGE_EXCHANGE))) ? 1 : 0;
},
[=] AMREX_GPU_DEVICE (index_type i, index_type s) { with_product_offsets_data[i] = s; },
amrex::Scan::Type::exclusive, amrex::Scan::retSum
Expand Down Expand Up @@ -236,7 +238,7 @@ public:
amrex::ParallelForRNG(n_total_pairs,
[=] AMREX_GPU_DEVICE (int i, amrex::RandomEngine const& engine) noexcept
{
if ((mask[i] > 0) & (mask[i] != int(ScatteringProcessType::IONIZATION)) & (mask[i] != int(ScatteringProcessType::TWOPRODUCT_REACTION)))
if ((mask[i] > 0) & (mask[i] != int(ScatteringProcessType::IONIZATION)) & (mask[i] != int(ScatteringProcessType::TWOPRODUCT_REACTION)) & (mask[i] != int(ScatteringProcessType::CHARGE_EXCHANGE)))
{
const auto slot0_idx = products_np_data[0] + no_product_offsets_data[i];
// Make a copy of the particle from the first reactant species
Expand Down Expand Up @@ -336,7 +338,8 @@ public:
}

// Next perform all product-producing collisions
else if (mask[i] == int(ScatteringProcessType::TWOPRODUCT_REACTION))
else if ((mask[i] == int(ScatteringProcessType::TWOPRODUCT_REACTION)) ||
(mask[i] == int(ScatteringProcessType::CHARGE_EXCHANGE)))
Comment thread
roelof-groenewald marked this conversation as resolved.
{
// create a copy of the first product species at the location of the first reactant species
const auto src0_idx = static_cast<int>(p_pair_indices_0[i]);
Expand All @@ -354,6 +357,16 @@ public:
// Set the weight of the new particle to p_pair_reaction_weight[i]
soa_products_data[3].m_rdata[PIdx::w][slot3_idx] = p_pair_reaction_weight[i];

// For charge exchange, swap positions: each product appears at the position
// of the *other* reactant, reflecting the physical transfer of charge across space.
Comment thread
RemiLehe marked this conversation as resolved.
Outdated
// Position components occupy indices 0 through PIdx::w-1 in m_rdata.
if (mask[i] == int(ScatteringProcessType::CHARGE_EXCHANGE)) {
for (int idim = 0; idim < PIdx::w; ++idim) {
soa_products_data[2].m_rdata[idim][slot2_idx] = soa_1.m_rdata[idim][src1_idx];
soa_products_data[3].m_rdata[idim][slot3_idx] = soa_0.m_rdata[idim][src0_idx];
}
}

Copy link
Copy Markdown
Member

@roelof-groenewald roelof-groenewald Apr 28, 2026

Choose a reason for hiding this comment

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

What do you guys think about making this more future proof by using the GetPositionAsStored and SetPositionAsStored structs rather than relying on the position attributes being first in the order?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In retrospect, since the particle iterators are not themselves easily accessible from this location, I think reverting to this original approach is better (more efficient and more readable). Sorry for the flip-flopping!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh man, @oshapoval, I'm so sorry but I just realized there might be a problem with this approach due to the fact that theta is actually not before w in the attribute list for RZ, RCYLINDER, and RSPHERE - see

static constexpr auto names = {"r", "z", "w", "ux", "uy", "uz", "theta"};
.

I think the balance of things then is to just have some code duplication here that will keep things easy to read. So making this something like:

soa_products_data[2].m_rdata[PIdx::z][slot2_idx] = soa_1.m_rdata[PIdx::z][src1_idx];
soa_products_data[3].m_rdata[PIdx::z][slot3_idx] = soa_0.m_rdata[PIdx::z][src0_idx];
#if defined(WARPX_DIM_RZ)
    soa_products_data[2].m_rdata[PIdx::x][slot2_idx] = soa_1.m_rdata[PIdx::x][src1_idx];
    soa_products_data[2].m_rdata[PIdx::theta][slot2_idx] = soa_1.m_rdata[PIdx::theta][src1_idx];
    soa_products_data[3].m_rdata[PIdx::x][slot3_idx] = soa_0.m_rdata[PIdx::x][src0_idx];
    soa_products_data[3].m_rdata[PIdx::theta][slot3_idx] = soa_0.m_rdata[PIdx::theta][src0_idx];
#elif defined(WARPX_DIM_RCYLINDER)
    soa_products_data[2].m_rdata[PIdx::theta][slot2_idx] = soa_1.m_rdata[PIdx::theta][src1_idx];
    soa_products_data[3].m_rdata[PIdx::theta][slot3_idx] = soa_0.m_rdata[PIdx::theta][src0_idx];
#elif defined(WARPX_DIM_RSPHERE)
    soa_products_data[2].m_rdata[PIdx::theta][slot2_idx] = soa_1.m_rdata[PIdx::theta][src1_idx];
    soa_products_data[3].m_rdata[PIdx::theta][slot3_idx] = soa_0.m_rdata[PIdx::theta][src0_idx];
    soa_products_data[2].m_rdata[PIdx::phi][slot2_idx] = soa_1.m_rdata[PIdx::phi][src1_idx];
    soa_products_data[3].m_rdata[PIdx::phi][slot3_idx] = soa_0.m_rdata[PIdx::phi][src0_idx];
#elif defined(WARPX_DIM_3D)
    soa_products_data[2].m_rdata[PIdx::x][slot2_idx] = soa_1.m_rdata[PIdx::x][src1_idx];
    soa_products_data[2].m_rdata[PIdx::y][slot2_idx] = soa_1.m_rdata[PIdx::y][src1_idx];
    soa_products_data[3].m_rdata[PIdx::x][slot3_idx] = soa_0.m_rdata[PIdx::x][src0_idx];
    soa_products_data[3].m_rdata[PIdx::y][slot3_idx] = soa_0.m_rdata[PIdx::y][src0_idx];
#elif defined(WARPX_DIM_XZ)
    soa_products_data[2].m_rdata[PIdx::x][slot2_idx] = soa_1.m_rdata[PIdx::x][src1_idx];
    soa_products_data[3].m_rdata[PIdx::x][slot3_idx] = soa_0.m_rdata[PIdx::x][src0_idx];
#endif

Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe I don't know what is happening here, but could this be done using SmartCopy?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@roelof-groenewald Good point, thanks for catching this.

Copy link
Copy Markdown
Member

@roelof-groenewald roelof-groenewald Apr 30, 2026

Choose a reason for hiding this comment

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

@dpgrote SmartCopy is being used here to create two new particles (soa_products_data[2] particle at slot2_idx based on soa_0 parent particle at src0_idx and soa_products_data[3] particle at slot3_idx based on parent particle sao_1 particle at src1_idx). However, in order to keep charge conservation during the charge exchange process the location of the two new particles is being swapped so that their locations depend on the other one's parent particle.

// Update the momenta of the products
TwoProductComputeProductMomenta(
soa_0.m_rdata[PIdx::ux][src0_idx],
Expand Down
1 change: 1 addition & 0 deletions Source/Particles/Collision/ScatteringProcess.H
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum class ScatteringProcessType {
ELASTIC,
BACK,
TWOPRODUCT_REACTION,
CHARGE_EXCHANGE,
EXCITATION,
IONIZATION,
FORWARD,
Expand Down
5 changes: 3 additions & 2 deletions Source/Particles/Collision/ScatteringProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ ScatteringProcess::parseProcessType(const std::string& scattering_process)
return ScatteringProcessType::ELASTIC;
} else if (scattering_process == "back") {
return ScatteringProcessType::BACK;
} else if ((scattering_process == "charge_exchange") ||
(scattering_process == "two_product_reaction")) {
} else if (scattering_process == "charge_exchange") {
return ScatteringProcessType::CHARGE_EXCHANGE;
} else if (scattering_process == "two_product_reaction") {
return ScatteringProcessType::TWOPRODUCT_REACTION;
} else if (scattering_process == "ionization") {
return ScatteringProcessType::IONIZATION;
Expand Down
Loading