Skip to content

Commit 8938d09

Browse files
authored
Add new time step size limiters (#6840)
This add time step limiters based on the plasma frequency and cyclotron frequency. This is in addition to the existing limiter based on the particle speed and controlled by the parameter `warpx.dt_update_interval`. This adds two new input parameters `warpx.max_omegap_dt` and `warpx.max_omegac_dt` for the plasma and cyclotron frequency limits. For this PR, when calculating the cyclotron frequency, only the B-field MultiFabs and the constant external fields are used when finding the maximum B-field. This ignores the rest of the external fields due to the complication or cost required to include them. If required, a later PR could include them, perhaps by fetching the B-field for the particles (which could be expensive) or by calculating the external B-fields at the grid locations, which would be complicated. Also, currently, for the initial time steps the `warpx.max_dt` or `warpx.cfl` with speed `c` is used. It would be better to apply the limiters initially rather that requiring the user to specify the initial step. This can be done in this PR or a separate PR. Todo: - [x] Add CI test - [x] Add documentation
1 parent bc65310 commit 8938d09

10 files changed

Lines changed: 354 additions & 24 deletions

File tree

Docs/source/usage/parameters.rst

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3184,17 +3184,40 @@ Time step
31843184

31853185
.. pp:param:: warpx.dt_update_interval
31863186
:type: ``string``
3187-
:default: ``-1``
31883187
:optional:
31893188

3190-
How many iterations pass between timestep adaptations when using the electrostatic solver.
3191-
Must be greater than ``0`` to use adaptive timestepping, or else :pp:param:`warpx.const_dt` must be specified.
3189+
This controls adaptive timestepping, where the time step size is updated based on the conditions of the simulation, and only applies when using the explicit electrostatic or theta-implicit solvers.
3190+
This specifies time step intervals when the time step size is updated.
3191+
The value must be greater than ``0``.
3192+
When specified, :pp:param:`warpx.const_dt` must not also be specified.
3193+
The time step size is updated using the limits specified by :pp:param:`warpx.cfl`, :pp:param:`warpx.max_omegap_dt`, and :pp:param:`warpx.max_omegac_dt`.
3194+
3195+
.. pp:param:: warpx.dt_update_diagnostic_file
3196+
:type: ``string``
3197+
:optional:
3198+
3199+
When adaptive timestepping is activated, information about the new time step and the simulation conditions are output to the file specified by this parameter.
3200+
3201+
.. pp:param:: warpx.max_omegap_dt
3202+
:type: ``float``
3203+
:optional:
3204+
3205+
With adaptive timestepping, the time step size is limited to be less than or equal to the value specified divided by the global plasma frequency.
3206+
The application of this limit is controlled by :pp:param:`warpx.dt_update_interval`, and is only applied when using the explicit electrostatic or theta-implicit solver..
3207+
3208+
.. pp:param:: warpx.max_omegac_dt
3209+
:type: ``float``
3210+
:optional:
3211+
3212+
With adaptive timestepping, the time step size is limited to be less than or equal to the value specified divided by the maximum cyclotron frequency.
3213+
Note that the maximum B-field is calculated from using only the constant applied B field (as set by :pp:param:`particles.B_external_particle`) and the B-field grid data.
3214+
The application of this limit is controlled by :pp:param:`warpx.dt_update_interval`, and is only applied when using the explicit electrostatic or theta-implicit solver..
31923215

31933216
.. pp:param:: warpx.max_dt
31943217
:type: ``float``
31953218
:optional:
31963219

3197-
The maximum timestep permitted for the electrostatic solver, when using adaptive timestepping.
3220+
The maximum timestep permitted when using adaptive timestepping.
31983221
If supplied, also sets the initial timestep for these simulations, before the first timestep update.
31993222

32003223
Filtering

Examples/Tests/electrostatic_sphere/inputs_test_3d_electrostatic_sphere_adaptive

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
stop_time = 60e-6
22
warpx.cfl = 0.2
33
warpx.dt_update_interval = 10
4+
warpx.dt_update_diagnostic_file = diags/reducedfiles/dt_updates.txt
5+
warpx.max_omegap_dt = 0.2
6+
warpx.max_omegac_dt = 0.2
47
warpx.max_dt = 1.5e-6
58
amr.n_cell = 64 64 64
69
amr.max_level = 0

Source/Evolve/WarpXComputeDt.cpp

Lines changed: 214 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@
1515
# include "FieldSolver/FiniteDifferenceSolver/FiniteDifferenceAlgorithms/CartesianNodalAlgorithm.H"
1616
# include "FieldSolver/FiniteDifferenceSolver/FiniteDifferenceAlgorithms/CartesianYeeAlgorithm.H"
1717
#endif
18+
#include "Fields.H"
1819
#include "Particles/MultiParticleContainer.H"
1920
#include "Utils/TextMsg.H"
2021
#include "Utils/WarpXAlgorithmSelection.H"
2122
#include "Utils/WarpXConst.H"
23+
#include "Utils/Parser/ParserUtils.H"
24+
25+
#include <ablastr/coarsen/sample.H>
2226

2327
#include <AMReX.H>
2428
#include <AMReX_Geometry.H>
2529
#include <AMReX_IntVect.H>
30+
#include <AMReX_MFIter.H>
2631
#include <AMReX_Print.H>
2732
#include <AMReX_REAL.H>
2833
#include <AMReX_Vector.H>
2934

3035
#include <algorithm>
3136
#include <memory>
37+
#include <filesystem>
3238

3339
/**
3440
* Compute the minimum of array x, where x has dimension AMREX_SPACEDIM
@@ -108,35 +114,229 @@ WarpX::ComputeDt ()
108114
}
109115

110116
/**
111-
* Determine the simulation timestep from the maximum speed of all particles
112-
* Sets timestep so that a particle can only cross cfl*dx cells per timestep.
117+
* Used to determine the simulation timestep from the maximum speed of all particles
118+
* Timestep will be set so that a particle can cross at most cfl*dx cells per timestep.
113119
*/
114-
void
115-
WarpX::UpdateDtFromParticleSpeeds ()
120+
amrex::Real
121+
WarpX::ParticleGridSpeedMax ()
116122
{
117123
const amrex::Real* dx = geom[max_level].CellSize();
118124
const amrex::Real dx_min = minDim(dx);
119125

120126
const amrex::ParticleReal max_v = mypc->maxParticleVelocity();
121-
amrex::Real deltat_new = 0.;
122127

123-
// Protections from overly-large timesteps
124-
if (max_v == 0) {
125-
WARPX_ALWAYS_ASSERT_WITH_MESSAGE(m_max_dt.has_value(), "Particles at rest and no constant or maximum timestep specified. Aborting.");
126-
deltat_new = m_max_dt.value();
127-
} else {
128-
deltat_new = cfl * dx_min / max_v;
128+
return max_v/dx_min;
129+
}
130+
131+
amrex::Real
132+
WarpX::GlobalPlasmaFrequencyMax ()
133+
{
134+
const std::unique_ptr<amrex::MultiFab> global_plasma_frequency = mypc->GetGlobalPlasmaFrequency(0);
135+
const amrex::Real global_plasma_frequency_max = global_plasma_frequency->max(0);
136+
return global_plasma_frequency_max;
137+
}
138+
139+
amrex::Real
140+
WarpX::GlobalCyclotronFrequencyMax ()
141+
{
142+
using ablastr::fields::Direction;
143+
using warpx::fields::FieldType;
144+
145+
const amrex::ParmParse pp_particles("particles");
146+
amrex::Vector<amrex::Real> B_external_particle(3, 0.);
147+
utils::parser::queryArrWithParser(pp_particles, "B_external_particle", B_external_particle);
148+
149+
const amrex::Real Bx_external = B_external_particle[0];
150+
const amrex::Real By_external = B_external_particle[1];
151+
const amrex::Real Bz_external = B_external_particle[2];
152+
153+
amrex::Real B_max = 0.;
154+
155+
// loop over refinement levels
156+
for (int lev = 0; lev <= finestLevel(); ++lev)
157+
{
158+
// get MultiFab data at lev
159+
const amrex::MultiFab & Bx = *m_fields.get(FieldType::Bfield_aux, Direction{0}, lev);
160+
const amrex::MultiFab & By = *m_fields.get(FieldType::Bfield_aux, Direction{1}, lev);
161+
const amrex::MultiFab & Bz = *m_fields.get(FieldType::Bfield_aux, Direction{2}, lev);
162+
163+
// Prepare interpolation of field components to cell center
164+
// The arrays below store the index type (staggering) of each MultiFab, with the third
165+
// component set to zero in the two-dimensional case.
166+
auto Bxtype = amrex::GpuArray<int,3>{0, 0, 0};
167+
auto Bytype = amrex::GpuArray<int,3>{0, 0, 0};
168+
auto Bztype = amrex::GpuArray<int,3>{0, 0, 0};
169+
for (int i = 0; i < AMREX_SPACEDIM; ++i){
170+
Bxtype[i] = Bx.ixType()[i];
171+
Bytype[i] = By.ixType()[i];
172+
Bztype[i] = Bz.ixType()[i];
173+
}
174+
175+
// General preparation of interpolation and reduction operations
176+
const amrex::GpuArray<int,3> cellCenteredtype{0,0,0};
177+
const amrex::GpuArray<int,3> reduction_coarsening_ratio{1,1,1};
178+
constexpr int reduction_comp = 0;
179+
180+
amrex::ReduceOps<amrex::ReduceOpMax> reduce_op;
181+
amrex::ReduceData<amrex::Real> reduce_data(reduce_op);
182+
using ReduceTuple = typename decltype(reduce_data)::Type;
183+
184+
// MFIter loop to interpolate fields to cell center and get maximum value
185+
#ifdef AMREX_USE_OMP
186+
#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
187+
#endif
188+
for (amrex::MFIter mfi(Bx, amrex::TilingIfNotGPU()); mfi.isValid(); ++mfi)
189+
{
190+
// Make the box cell centered in preparation for the interpolation (and to avoid
191+
// including ghost cells in the calculation)
192+
const amrex::Box & box = enclosedCells(mfi.nodaltilebox());
193+
const auto& arrBx = Bx[mfi].array();
194+
const auto& arrBy = By[mfi].array();
195+
const auto& arrBz = Bz[mfi].array();
196+
197+
reduce_op.eval(box, reduce_data,
198+
[=] AMREX_GPU_DEVICE (int i, int j, int k) -> ReduceTuple
199+
{
200+
const amrex::Real Bx_interp = ablastr::coarsen::sample::Interp(arrBx, Bxtype, cellCenteredtype,
201+
reduction_coarsening_ratio, i, j, k, reduction_comp);
202+
const amrex::Real By_interp = ablastr::coarsen::sample::Interp(arrBy, Bytype, cellCenteredtype,
203+
reduction_coarsening_ratio, i, j, k, reduction_comp);
204+
const amrex::Real Bz_interp = ablastr::coarsen::sample::Interp(arrBz, Bztype, cellCenteredtype,
205+
reduction_coarsening_ratio, i, j, k, reduction_comp);
206+
return {amrex::Math::powi<2>(Bx_interp + Bx_external) +
207+
amrex::Math::powi<2>(By_interp + By_external) +
208+
amrex::Math::powi<2>(Bz_interp + Bz_external)};
209+
});
210+
}
211+
212+
const amrex::Real hv_Bsq = amrex::get<0>(reduce_data.value()); // highest value of |B|**2
213+
214+
B_max = std::max(B_max, std::sqrt(hv_Bsq));
215+
216+
}
217+
218+
// MPI reduce
219+
amrex::ParallelDescriptor::ReduceRealMax({B_max});
220+
221+
amrex::Real omegac_max = 0.;
222+
223+
const int n_containers = mypc->nContainers();
224+
for (int i = 0; i < n_containers; i++)
225+
{
226+
const WarpXParticleContainer& pc = mypc->GetParticleContainer(i);
227+
if (pc.getMass() > 0.) {
228+
const amrex::Real pc_omegac = pc.getCharge()*B_max/pc.getMass();
229+
omegac_max = std::max(omegac_max, pc_omegac);
230+
}
231+
}
232+
233+
return omegac_max;
234+
}
235+
236+
void
237+
WarpX::ApplyDtLimiters ()
238+
{
239+
using namespace amrex::literals;
240+
241+
// Calculate limiting values from the simulation conditions
242+
const amrex::Real vmax_o_dx = ParticleGridSpeedMax();
243+
const amrex::Real omegap_max = m_max_omegap_dt.has_value() ? GlobalPlasmaFrequencyMax() : 0._rt;
244+
const amrex::Real omegac_max = m_max_omegac_dt.has_value() ? GlobalCyclotronFrequencyMax() : 0._rt;
245+
246+
// Ensure that a valid time step value exists, either from the simulation conditions or from max_dt
247+
if (vmax_o_dx == 0._rt &&
248+
(!m_max_omegap_dt.has_value() || omegap_max == 0._rt) &&
249+
(!m_max_omegac_dt.has_value() || omegac_max == 0._rt)) {
250+
WARPX_ALWAYS_ASSERT_WITH_MESSAGE(m_max_dt.has_value(),
251+
"No valid time step size limit found, warpx.max_dt must be specified");
252+
}
253+
254+
amrex::Real dt_new = std::numeric_limits<amrex::Real>::max();
255+
256+
if (vmax_o_dx > 0._rt) {
257+
dt_new = std::min(dt_new, cfl/vmax_o_dx);
258+
}
259+
if (m_max_omegap_dt.has_value() && omegap_max > 0._rt) {
260+
dt_new = std::min(dt_new, m_max_omegap_dt.value()/omegap_max);
261+
}
262+
if (m_max_omegac_dt.has_value() && omegac_max > 0._rt) {
263+
dt_new = std::min(dt_new, m_max_omegac_dt.value()/omegac_max);
129264
}
130265

131-
// Restrict to be less than user-specified maximum timestep, if present
132266
if (m_max_dt.has_value()) {
133-
deltat_new = std::min(deltat_new, m_max_dt.value());
267+
dt_new = std::min(dt_new, m_max_dt.value());
134268
}
135269

136270
// Update dt
137-
dt[max_level] = deltat_new;
271+
dt[max_level] = dt_new;
138272

139273
for (int lev = max_level-1; lev >= 0; --lev) {
140274
dt[lev] = dt[lev+1] * refRatio(lev)[0];
141275
}
276+
277+
// Write diagnostics if requested
278+
if (amrex::ParallelDescriptor::IOProcessor()
279+
&& !m_dt_update_diagnostic_file.empty()
280+
&& !amrex::FileExists(m_dt_update_diagnostic_file)) {
281+
282+
std::filesystem::path const diagnostic_path(m_dt_update_diagnostic_file);
283+
std::filesystem::path const diagnostic_dir = diagnostic_path.parent_path();
284+
if (!diagnostic_dir.empty()) {
285+
std::filesystem::create_directories(diagnostic_dir);
286+
}
287+
288+
std::ofstream diagnostic_file{m_dt_update_diagnostic_file, std::ofstream::out | std::ofstream::trunc};
289+
if (!diagnostic_file.is_open()) {
290+
amrex::Abort("Failed to open file: " + m_dt_update_diagnostic_file);
291+
}
292+
293+
int c = 0;
294+
diagnostic_file << "#";
295+
diagnostic_file << "[" << c++ << "]step()";
296+
diagnostic_file << " ";
297+
diagnostic_file << "[" << c++ << "]time(s)";
298+
diagnostic_file << " ";
299+
diagnostic_file << "[" << c++ << "]new_dt";
300+
diagnostic_file << " ";
301+
diagnostic_file << "[" << c++ << "]vmax_dt";
302+
if (m_max_omegap_dt.has_value()) {
303+
diagnostic_file << " ";
304+
diagnostic_file << "[" << c++ << "]omegap_dt";
305+
}
306+
if (m_max_omegac_dt.has_value()) {
307+
diagnostic_file << " ";
308+
diagnostic_file << "[" << c++ << "]omegac_dt";
309+
}
310+
diagnostic_file << "\n";
311+
diagnostic_file.close();
312+
}
313+
314+
if (amrex::ParallelDescriptor::IOProcessor()
315+
&& !m_dt_update_diagnostic_file.empty()) {
316+
317+
std::ofstream diagnostic_file{m_dt_update_diagnostic_file, std::ofstream::out | std::ofstream::app};
318+
if (!diagnostic_file.is_open()) {
319+
amrex::Abort("Failed to open file: " + m_dt_update_diagnostic_file);
320+
}
321+
322+
diagnostic_file << std::setprecision(14);
323+
diagnostic_file << istep[0] + 1;
324+
diagnostic_file << " ";
325+
diagnostic_file << t_new[0];
326+
diagnostic_file << " ";
327+
diagnostic_file << dt_new;
328+
diagnostic_file << " ";
329+
diagnostic_file << vmax_o_dx*dt_new;
330+
331+
if (m_max_omegap_dt.has_value()) {
332+
diagnostic_file << " ";
333+
diagnostic_file << omegap_max*dt_new;
334+
}
335+
if (m_max_omegac_dt.has_value()) {
336+
diagnostic_file << " ";
337+
diagnostic_file << omegac_max*dt_new;
338+
}
339+
diagnostic_file << "\n";
340+
diagnostic_file.close();
341+
}
142342
}

Source/Evolve/WarpXEvolve.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,11 @@ WarpX::Evolve (int numsteps)
195195
// Update the timestep for solvers that support adaptive timestepping
196196
// (electrostatic and theta-implicit EM), provided const_dt is not specified.
197197
if (m_dt_update_interval.contains(step+1)) {
198+
SynchronizeVelocityWithPosition();
199+
ApplyDtLimiters();
198200
if (verbose_step) {
199-
amrex::Print() << Utils::TextMsg::Info("updating timestep");
201+
amrex::Print() << Utils::TextMsg::Info("updating timestep to dt = " + std::to_string(dt[0]));
200202
}
201-
SynchronizeVelocityWithPosition();
202-
UpdateDtFromParticleSpeeds();
203203
}
204204

205205
// If position and velocity are synchronized, push velocity backward one half step

Source/Particles/MultiParticleContainer.H

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ public:
196196

197197
std::unique_ptr<amrex::MultiFab> GetChargeDensity(int lev, bool local = false);
198198

199+
std::unique_ptr<amrex::MultiFab> GetGlobalPlasmaFrequency (int lev);
199200
void GenerateGlobalDebyeLength ();
200201

201202
void doFieldIonization (int lev,

Source/Particles/MultiParticleContainer.cpp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,47 @@ MultiParticleContainer::GetChargeDensity (int lev, bool local)
693693
return rho;
694694
}
695695

696+
std::unique_ptr<amrex::MultiFab>
697+
MultiParticleContainer::GetGlobalPlasmaFrequency (int lev)
698+
{
699+
const WarpX & warpx = WarpX::GetInstance();
700+
701+
amrex::BoxArray const & ba = warpx.boxArray(lev);
702+
amrex::DistributionMapping const & dmap = warpx.DistributionMap(lev);
703+
int const ncomps = 1;
704+
const amrex::IntVect ng = amrex::IntVect::TheZeroVector();
705+
auto global_plasma_frequency = std::make_unique<amrex::MultiFab>(ba, dmap, ncomps, ng);
706+
global_plasma_frequency->setVal(amrex::Real(0.0));
707+
708+
for (auto& pc : allcontainers) {
709+
710+
if (pc->getMass() == 0. || pc->getCharge() == 0.) {
711+
continue;
712+
}
713+
714+
std::unique_ptr<amrex::MultiFab> plasma_frequency = pc->GetPlasmaFrequency(lev);
715+
716+
#ifdef AMREX_USE_OMP
717+
#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
718+
#endif
719+
for (amrex::MFIter mfi(*global_plasma_frequency, TilingIfNotGPU()); mfi.isValid(); ++mfi )
720+
{
721+
const amrex::Box box = mfi.tilebox();
722+
723+
amrex::Array4<amrex::Real> const& omegap_array = plasma_frequency->array(mfi);
724+
amrex::Array4<amrex::Real> const& global_omegap_array = global_plasma_frequency->array(mfi);
725+
726+
amrex::ParallelFor(box,
727+
[=] AMREX_GPU_DEVICE (int i, int j, int k) {
728+
amrex::Real const omegap = omegap_array(i,j,k);
729+
amrex::Real const global_omegap = global_omegap_array(i,j,k);
730+
global_omegap_array(i,j,k) = std::sqrt(global_omegap*global_omegap + omegap*omegap);
731+
});
732+
}
733+
}
734+
return global_plasma_frequency;
735+
}
736+
696737
void
697738
MultiParticleContainer::GenerateGlobalDebyeLength ()
698739
{

0 commit comments

Comments
 (0)