Marking and transformations#

Introduction#

A common use case when dealing with NESO-Particles ParticleGroup objects is selecting particles based on some condition(s) and applying transformations to them. For example, assuming a ParticleGroup contains many different species of particles, one might want to select all particles of a given species that also have weight below some threshold, and then merge them to avoid tracking many small particles.

In order to accommodate the above, VANTAGE-Reactions offers a uniform interface for MarkingStrategy, TransformationStrategy, and TransformationWrapper objects.

Marking Strategies#

A marking strategy is the abstract wrapper class for the creation of NESO-Particles particle subgroups. The below strategies are the two currently implemented.

Example of several marking strategies#
inline void marking_strategy_example(ParticleGroupSharedPtr particle_group) {

  // Marking strategies take in ParticleSubgroup shared pointers.
  // We trivially produce a whole group subgroup pointer:
  auto input_subgroup = std::make_shared<ParticleSubGroup>(particle_group);

  // To make a marking strategy that will mark all particles with the
  // real-valued particle dat "WEIGHT" < 1e-6 we use the ComparisonMarkerSingle
  // strategy, comparing a single REAL value with a less-than comparison
  // function (LessThanComp is a simple device-safe wrapper)
  //
  // The make_marking_strategy helper function casts marking strategies into
  // a std::shared_ptr<MarkingStrategy>.
  //
  // The first argument in the case of the ComparisonMarkersSingle strategy is
  // the single ParticleDat name (as a NESO-Particles Sym) and the second is the
  // fixed value these dats will be compared against.
  auto mark_low_weight =
      make_marking_strategy<ComparisonMarkerSingle<REAL, LessThanComp>>(
          Sym<REAL>("WEIGHT"), 1e-6);

  // Here we make a new marking strategy, which will mark all particles with
  // ID=0
  auto mark_id_zero =
      make_marking_strategy<ComparisonMarkerSingle<INT, EqualsComp>>(
          Sym<INT>("ID"), 0);

  // The resulting subgroup will have only particles with ID=0
  auto subgroup_only_id_zero =
      mark_id_zero->make_marker_subgroup(input_subgroup);

  // The following subgroup will have only particles with ID=0 and WEIGHT<1e-6
  auto subgroup_id_zero_and_low_weight =
      mark_low_weight->make_marker_subgroup(subgroup_only_id_zero);

  return;
}

As demonstrated above, marking strategies are composed by applying them one after the other to get particle subgroups where particles respect all conditions.

Transformation Strategies#

Once a suitable subgroup is constructed, we use TransformationStrategy objects to apply any required transformation to those particles. All transformation strategies have the transform method which is applied to particle subgroups. Examples of the various built-in strategies are given below.

Particle Removal Strategy#

Often we wish to remove some particles from the simulation. For example, this might be due to their weight being too low to track. The following code will remove all particles with weight below a given threshold using marking and transformation strategies.

Example of using a removal strategy#
inline void removal_strategy_example(ParticleGroupSharedPtr particle_group) {

  // Marking strategies take in ParticleSubgroup shared pointers.
  // We trivially produce a whole group subgroup pointer:
  auto input_subgroup = std::make_shared<ParticleSubGroup>(particle_group);

  // As in the marking strategy example we mark low weight particles by first
  // creating the marking strategy
  auto mark_low_weight =
      make_marking_strategy<ComparisonMarkerSingle<REAL, LessThanComp>>(
          Sym<REAL>("WEIGHT"), 1e-6);

  // And then applying it
  auto subgroup_low_weight =
      mark_low_weight->make_marker_subgroup(input_subgroup);

  // The make_transformation_strategy helper function casts concrete
  // transformation strategies into std::shared_ptr<TransformationStrategy>.
  //
  // The simple removal strategy below requires no inputs, and just deletes
  // all particles in the passed subgroup
  auto removal_strategy =
      make_transformation_strategy<SimpleRemovalTransformationStrategy>();

  removal_strategy->transform(subgroup_low_weight);

  return;
}

ParticleDatZeroer#

VANTAGE-Reactions assumes that particles carry information of their contribution to various sources that need to be projected onto the grid. A common requirement in these situations is to reset the values of sources on the particles after projection. The library offers the ParticleDatZeroer transformation strategy that allows the user to accomplish this.

Example of zeroing out real-valued particle data#
inline void zeroer_strategy_example(ParticleGroupSharedPtr particle_group) {

  auto input_subgroup = std::make_shared<ParticleSubGroup>(particle_group);

  // A ParticleDatZeroer zeroes INT or REAL particle dats and is constructed
  // by passing a vestor of strings with the dat names
  auto zeroer = make_transformation_strategy<ParticleDatZeroer<REAL>>(
      std::vector<std::string>{"ELECTRON_SOURCE_DENSITY",
                               "ION_SOURCE_DENSITY"});

  zeroer->transform(input_subgroup);

  return;
}

Accumulator Strategies#

Another common requirement is the accumulation of particle properties cellwise. This is a requirement for finite volume methods (projection of sources) as well as general particle data analysis (weighted averages of quantities). Two classes of transformation strategies are provided for this use.

CellwiseAccumulator and WeightedCellwiseAccumulator example#
inline void
accumulator_strategy_example(ParticleGroupSharedPtr particle_group) {

  auto input_subgroup = std::make_shared<ParticleSubGroup>(particle_group);

  // CellwiseAccumulators need to access some general data about the particle
  // group, so it needs to be available on construction
  //
  // Similarly to zeroers, accumulators take in the names of the particle dats
  // that they should accumulate values for cellwise. Here we use make_shared
  // instead of make_transformation_strategy in order to be able to call
  // accumulator-specific methods
  auto accumulator = std::make_shared<CellwiseAccumulator<REAL>>(
      particle_group, std::vector<std::string>{"ELECTRON_SOURCE_DENSITY",
                                               "ION_SOURCE_DENSITY"});

  // The accumulator, unlike other transforms, does not modify the particle
  // group. Instead, it modifies its own internal state.
  accumulator->transform(input_subgroup);

  // Upon accumulation, the accumulated data is stored in NESO-Particle
  // CellDatConst objects and can be retrieved easily
  auto accumulated_electron_source =
      accumulator->get_cell_data("ELECTRON_SOURCE_DENSITY");

  // Individual cell data buffers can be zeroed
  accumulator->zero_buffer("ELECTRON_SOURCE_DENSITY");

  // Or all buffers can be zeroed
  accumulator->zero_all_buffers();

  // A weighted accumulator transform is also available, taking in the REAL
  // particle dat to be used as the weight (should be a PartilceDat with 1
  // component) in the constructor
  auto weighted_accumulator =
      std::make_shared<WeightedCellwiseAccumulator<REAL>>(
          particle_group, std::vector<std::string>{"VELOCITY", "POSITION"},
          "WEIGHT");

  weighted_accumulator->transform(input_subgroup);

  auto weighted_velocities = weighted_accumulator->get_cell_data("VELOCITY");
  weighted_accumulator->zero_buffer("VELOCITY");

  // In addition to the standard accumulator methods, the weighted accumulator
  // also offers access to the accumulated weight dat
  auto accumulated_weight = weighted_accumulator->get_weight_cell_data();

  return;
}

Composite Strategy#

Sometimes multuple strategies need to be applied in order. It is possible to compose transformation strategies by adding them to a composite strategy, allowing all of them to be applied with one transform call. This is particularly useful in the construction of TransformationWrapper objects (see below), where there is a hook left for a single transformation strategy.

Applying accumulator and zeroer strategies as part of a composite strategy#
inline void composite_strategy_example(ParticleGroupSharedPtr particle_group) {

  auto input_subgroup = std::make_shared<ParticleSubGroup>(particle_group);

  // We wish to compose an accumulator and a particle dat zeroer to accumulate
  // some sources and then to reset the particle data that stored them
  //
  // The sources we wish to accumulate
  auto source_names =
      std::vector<std::string>{"ELECTRON_SOURCE_DENSITY", "ION_SOURCE_DENSITY"};

  // In order to have later access to the accumulator, we construct it using
  // make_shared
  auto accumulator =
      std::make_shared<CellwiseAccumulator<REAL>>(particle_group, source_names);

  // A composite transform can be constructed by passing a vector of
  // TransformationStrategy objects, so if we wish to include the accumulator,
  // it must be dynamically cast
  auto composite = std::make_shared<CompositeTransform>(
      std::vector<std::shared_ptr<TransformationStrategy>>{
          std::dynamic_pointer_cast<TransformationStrategy>(accumulator)});

  // We can also directly add TransformationStrategy objects to the composite to
  // be applied in sequence (in order of addition)

  composite->add_transformation(
      make_transformation_strategy<ParticleDatZeroer<REAL>>(source_names));

  // The composite can then be applied as one transformation
  composite->transform(input_subgroup);

  // Since the accumulator was added to the composite via a pointer
  // its interface is still accessible

  auto accumulated_electron_source =
      accumulator->get_cell_data("ELECTRON_SOURCE_DENSITY");

  accumulator->zero_all_buffers();

  return;
}

Particle Merging Strategy#

VANTAGE-Reaction implements a simplified merging algorithim from [VRANIC2015]. It assumes that all particles being merged are of the same species (i.e. have the same mass) and that they are non-relativistic. In the original paper, the authors merge particles within momentum space cells, while we merge all particles in the subgroup passed to the transformation strategy (and we use the momentum space bounding box in 3D to determine the plane in which the merged particle momenta lie).

Particles are merged cell-wise into 2 particles. The properties modified by the merging algorithm are the positions, weights, and momenta/velocities. Other properties are taken from 2 other particles in the passed subgroups, i.e. properties like cell and species IDs should be copied consistently, but all other properties should be considered undefined. As such, merging should only be invoked once all particle properties have been used for their respective purposes.

The implementation of MergeTransformationStrategy is available in 2D and 3D, and, given the above considerations, is easily used.

An example of constructing a merging strategy in 2D#
inline void merging_strategy_example(ParticleGroupSharedPtr particle_group) {

  auto input_subgroup = std::make_shared<ParticleSubGroup>(particle_group);

  auto merging_strat =
      make_transformation_strategy<MergeTransformationStrategy<2>>();

  merging_strat->transform(input_subgroup);

  return;
}

Transformation Wrappers#

Often we wish to encapsulate both some marking conditions as well as the transformation we wish to perform into a single object. A common example is performing merging on multiple different species, but only on particles where weight is less than some threshold. In other words, the transformation we wish to perform is fixed, but only some of the marking conditions are fixed, while others vary. In this case the library offers the TransformationWrapper class, which wraps marking conditions and a transformation strategy, and applies them to a ParticleGroup. We can then fix the instruction “Merge all particles with weight < threshold”, and can extend it with other conditions, such as “and with species ID = 1”.

An example of a TransformationWrapper being used to remove particles with low weights for two different ID values#
inline void
transformation_wrapper_example(ParticleGroupSharedPtr particle_group) {

  // A transformation wrapper can be constructed with a vector of marking
  // strategies or they can be added later.
  //
  // However, it always requires a transformation strategy at construction
  //
  // The wrapper below encapsulates the instruction "remove all particles with
  // weights < 1e6"
  auto wrapper = std::make_shared<TransformationWrapper>(
      std::vector<std::shared_ptr<MarkingStrategy>>{
          make_marking_strategy<ComparisonMarkerSingle<REAL, LessThanComp>>(
              Sym<REAL>("WEIGHT"), 1e-6)},
      make_transformation_strategy<SimpleRemovalTransformationStrategy>());

  // Wrappers can be copied and/or extended
  //
  // For example, maybe we want to remove both particles with ID=0 and ID=1
  //
  // For this we create 2 wrappers using the above as a base

  auto wrapper_ID0 = std::make_shared<TransformationWrapper>(*wrapper);
  auto wrapper_ID1 = std::make_shared<TransformationWrapper>(*wrapper);

  // We can then add different further marking conditions to them

  wrapper_ID0->add_marking_strategy(
      make_marking_strategy<ComparisonMarkerSingle<INT, EqualsComp>>(
          Sym<INT>("ID"), 0));

  wrapper_ID1->add_marking_strategy(
      make_marking_strategy<ComparisonMarkerSingle<INT, EqualsComp>>(
          Sym<INT>("ID"), 1));

  // Wrappers, unlike strategies, act on the whole group
  //
  // Subselection is assumed to be performed by the successive
  // application of MarkingStrategies
  wrapper_ID0->transform(particle_group);
  wrapper_ID1->transform(particle_group);

  return;
}
[VRANIC2015]

Vranic et al. - Particle merging algorithm for PIC codes https://www.sciencedirect.com/science/article/pii/S0010465515000405