Adding a new physics package#
This guide will cover the files which need to be modified if you’re adding a new physics package. Small snippets of code will be included with the instructions to tell you where the new code needs to go. Once completed, you should be in a position to start writing functions which actually perform your new package.
physics_package.h#
Firstly, we will need a new alias for the physics package. The physics_package
class will be used whichever package is active, but it will only call functions
based on its type
variable. We can add a new type by extending the enum class
phys
:
// Possible physics packages
enum class phys {
bremsstrahlung,
bethe_heitler_ee,
bethe_heitler_muon,
nuclear_trident,
Before we start the PIC loop, the physics package manager will call setup
for
each physics package in the list. This function switches to the setup function
which matches the type
variable. We need a corresponding setup function for
our new package:
void setup_bremsstrahlung(SimulationData<ndims> &data);
void setup_bh_ee(SimulationData<ndims> &data);
void setup_bh_mu(SimulationData<ndims> &data);
During the PIC loop, the physics package manager goes through each physics
package, and calls the run
command. Again, this switches to run the package
which
matches the type
. We need to declare a run
function for the new package in
this file:
// Run packages
void run_bremsstrahlung(physics_package_manager<ndims> *manager,
SimulationData<ndims> &data);
void run_bh_ee(physics_package_manager<ndims> *manager,
SimulationData<ndims> &data);
void run_bh_mu(physics_package_manager<ndims> *manager,
SimulationData<ndims> &data);
Standard, non-collisional physics packages call two functions when they are run. A sampling function calculates the process rate, and determines whether it triggers for the current particle in the current time-step. A performing function actually modifies triggers macro-particles. We will need to declare functions for these, if appropriate
void sample_bremsstrahlung(Species<ndims> *part,
bool &event_triggered,
physics_package_manager<ndims> *manager,
SimulationData<ndims> &data);
void sample_bh_ee(Species<ndims> *part,
bool &event_triggered,
physics_package_manager<ndims> *manager,
SimulationData<ndims> &data);
void sample_bh_mu(Species<ndims> *part,
bool &event_triggered,
physics_package_manager<ndims> *manager,
SimulationData<ndims> &data);
void perform_bremsstrahlung(Species<ndims> *part,
physics_package_manager<ndims> *manager,
SimulationData<ndims> &);
void perform_bh_ee(Species<ndims> *part,
physics_package_manager<ndims> *,
SimulationData<ndims> &);
void perform_bh_mu(Species<ndims> *part,
physics_package_manager<ndims> *,
SimulationData<ndims> &);
If the process is collisional, then the sample/perform framework is inappropriate. Instead, sampling and performing must be done in the same function. The run function will loop over all cells, and a perform function must be declared for performing the process in the current cell
void perform_coll_ir(int ix,
int iy,
int iz,
SimulationData<ndims> &,
physics_package_manager<ndims> *manager);
setup.cpp#
Now that we have a new physics package alias in the phys
enum class, we need
to tell the code when to use it. We need to add a new case to the
setup_physics_packages
function. This creates a new physics package for each
physics_package
block in the input deck. Here, the “val” string corresponds
to the value given to the process
key in the physics_package
block. We need
to link that input deck parameter to the type
variable in the
physics_package
class
if (val == "bremsstrahlung") {
interaction->set_type(phys::bremsstrahlung);
} else if (val == "bethe_heitler_ee") {
interaction->set_type(phys::bethe_heitler_ee);
} else if (val == "bethe_heitler_mu") {
interaction->set_type(phys::bethe_heitler_muon);
physics_package.cpp#
Our physics_package
class now has an alias for our new package. When the
physics package manager calls a setup or run command for this class, we need to
setup or run the corresponding package function. Hence, we need to add new cases
to physics_package<ndims>::setup
switch (type) {
case phys::bremsstrahlung:
setup_bremsstrahlung(data);
break;
case phys::bethe_heitler_ee:
setup_bh_ee(data);
break;
case phys::bethe_heitler_muon:
setup_bh_mu(data);
break;
and physics_package<ndims>::run
switch (type) {
case phys::bremsstrahlung:
run_bremsstrahlung(manager, data);
break;
case phys::bethe_heitler_ee:
run_bh_ee(manager, data);
break;
case phys::bethe_heitler_muon:
run_bh_mu(manager, data);
break;
If your new package can create secondary particles, then there are additional
functions which need to be modified. For each secondary species present, you
will need to provide three pieces of information. Firstly, you must give the
corresponding keyname for the species, as it appears in the input deck
physics_package
block. This is done in
physics_package<ndims>::get_secondary_keynames
switch (type) {
case phys::bremsstrahlung:
secondary_keynames.push_back("photon_species");
break;
case phys::bethe_heitler_ee:
secondary_keynames.push_back("electron_species");
secondary_keynames.push_back("positron_species");
break;
case phys::bethe_heitler_muon:
secondary_keynames.push_back("muon_species");
secondary_keynames.push_back("antimuon_species");
break;
If the user doesn’t specify the secondary species in the input deck, the code
will create a new species for this package. To do this, we need to provide the
code with a default species name, given in the function
physics_package<ndims>::get_new_spec_names
:
switch (type) {
case phys::bremsstrahlung:
new_species_names.push_back("bremsstrahlung_photon");
break;
case phys::bethe_heitler_ee:
new_species_names.push_back("bh_electron");
new_species_names.push_back("bh_positron");
break;
case phys::bethe_heitler_muon:
new_species_names.push_back("bh_muon");
new_species_names.push_back("bh_antimuon");
break;
Finally, if a new species must be created, we must provide information about
the type of species. A new case must therefore be added to
physics_package<ndims>::get_new_spec_types
.
case phys::bremsstrahlung:
new_species_types.push_back(part_type::photon);
break;
case phys::bethe_heitler_ee:
new_species_types.push_back(part_type::electron);
new_species_types.push_back(part_type::positron);
break;
case phys::bethe_heitler_muon:
new_species_types.push_back(part_type::muon);
new_species_types.push_back(part_type::antimuon);
break;
Species.h#
Add any new phys_var
variables needed. You should create a new optical depth
variable to track when the new process triggers. The format currently in use
is to add the od_
prefix for optical depth variables. If there are other
permanent varibles needed, this is where to add them.
// Permanent MPI-aware particle variables
friend class phys_var<ndims>;
phys_var<ndims> od_bremsstrahlung;
phys_var<ndims> od_bh_ee;
physics_package_manager.cpp#
Any new permanent particle variables we have added for the new package will
need initialising. This is currently done in the function
physics_package_manager<ndims>::initialise_shared_perm_vars
. Make sure to
add a new case for each new variable you have created.
if (species_list[i]->od_bremsstrahlung.in_use()) {
species_list[i]->od_bremsstrahlung.resize_array();
for (auto part : *species_list[i]) {
part->od_bremsstrahlung.set(-log(rng.random()));
}
}
For optical depth variables, we sample these from the emission probability distribution: \(\tau_d = -\ln\left(x_r\right)\), where \(x_r\) is a uniformly distributed random number. Note that this function will only initialise physics package variables which are “in use”. This is switched on by the packages themselves.
species.cpp#
Our new permanent variables need to be moved along with the rest of the particle
data, whenever particle data is moved. Currently, this is done when
macro-particles pass MPI boundaries, or when package-owned secondary particles
are added to the main species list. Permanent physics package particle variables
are packed for transfer in the function
Species<ndims>::get_all_phys_variables
, and we will need a new case for our
new variables
if (od_bremsstrahlung.in_use()) {
phys_vars.push_back(od_bremsstrahlung.array_ptr());
}
if (od_bh_ee.in_use()) {
phys_vars.push_back(od_bh_ee.array_ptr());
}
if (od_bh_mu.in_use()) {
phys_vars.push_back(od_bh_mu.array_ptr());
}
We also need to make that when packages create new particles, we initialise all
the required permanent variables - including
variables which needed for other processes. To do this, we pass each
package-owned secondary species a list of
all the active variables for the corresponding main-list species, using
Species<ndims>::copy_phys_bools
. Our newly added variables need
to appear in this function too.
if (other_spec->od_bremsstrahlung.in_use()) {
od_bremsstrahlung.activate(*this);
}
if (other_spec->od_bh_ee.in_use()) {
od_bh_ee.activate(*this);
}
if (other_spec->od_bh_mu.in_use()) {
od_bh_mu.activate(*this);
}
New package file#
For code neatness, it is good to create a new source-code file for each physics
package. An easy way to implement this would be to find the test_process_*.cpp
file which most closely resembles your new package, and to simply copy this
file. Then, change the names of any setup, run, perform or sample functions to
match those you defined in Species.h
.
Once you reach this stage, you’ll have a new basic physics package which matches one of the test processes. From here, you can start modifying the test package functions to achieve the desired behaviour. This is how the real physics packages already implemented in the code were created. For details on more complicated implementations, try looking to the existing physics packages for guidance.
I’m aware elastic collisions are yet to be implemented. This may work differently to the current packages - look to the FORTRAN version of the code for help.
CMakeLists.txt#
All the source-code files appear in the SOURCES
list of this file. For
consistency, if you add a new package file, be sure to reference it here.
# Library sources exclude main.cpp, which contains main()
set(SOURCES
Driver.cpp
Fields.cpp
Simulation.cpp
Species.cpp
# Including headers in add_library calls, as this information is used by
# various IDEs.
set(HEADERS
"${PROJECT_BINARY_DIR}/include/commit_info.h"
"${PROJECT_SOURCE_DIR}/include/StaticArray1d.h"
"${PROJECT_SOURCE_DIR}/include/StaticArray2d.h"
"${PROJECT_SOURCE_DIR}/include/Array3d.h"