Skip to content

Commit a25faff

Browse files
n01rRevathiJambunathanpre-commit-ci[bot]EZoni
authored
Add Time-Averaged Field Diagnostics (#5285)
This PR adds time-averaged field diagnostics to the WarpX output. To-do: - [x] code - [x] docs - [x] tests - [x] example Follow-up PRs: - meta-data - make compatible with adaptive time stepping This PR is based on work performed during the *2024 WarpX Refactoring Hackathon* and was created together with @RevathiJambunathan. Successfully merging this pull request may close #5165. --------- Co-authored-by: RevathiJambunathan <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Edoardo Zoni <[email protected]> Co-authored-by: Edoardo Zoni <[email protected]>
1 parent d34cc6c commit a25faff

21 files changed

+507
-49
lines changed

Docs/source/usage/parameters.rst

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2633,10 +2633,11 @@ Diagnostics and output
26332633
In-situ visualization
26342634
^^^^^^^^^^^^^^^^^^^^^
26352635

2636-
WarpX has four types of diagnostics:
2637-
``FullDiagnostics`` consist in dumps of fields and particles at given iterations,
2638-
``BackTransformedDiagnostics`` are used when running a simulation in a boosted frame, to reconstruct output data to the lab frame,
2639-
``BoundaryScrapingDiagnostics`` are used to collect the particles that are absorbed at the boundary, throughout the simulation, and
2636+
WarpX has five types of diagnostics:
2637+
``Full`` diagnostics consist in dumps of fields and particles at given iterations,
2638+
``TimeAveraged`` diagnostics only allow field data, which they output after averaging over a period of time,
2639+
``BackTransformed`` diagnostics are used when running a simulation in a boosted frame, to reconstruct output data to the lab frame,
2640+
``BoundaryScraping`` diagnostics are used to collect the particles that are absorbed at the boundary, throughout the simulation, and
26402641
``ReducedDiags`` allow the user to compute some reduced quantity (particle temperature, max of a field) and write a small amount of data to text files.
26412642
Similar to what is done for physical species, WarpX has a class Diagnostics that allows users to initialize different diagnostics, each of them with different fields, resolution and period.
26422643
This currently applies to standard diagnostics, but should be extended to back-transformed diagnostics and reduced diagnostics (and others) in a near future.
@@ -2882,12 +2883,58 @@ In-situ capabilities can be used by turning on Sensei or Ascent (provided they a
28822883
* ``warpx.mffile_nstreams`` (`int`) optional (default `4`)
28832884
Limit the number of concurrent readers per file.
28842885

2886+
2887+
.. _running-cpp-parameters-diagnostics-timeavg:
2888+
2889+
Time-Averaged Diagnostics
2890+
^^^^^^^^^^^^^^^^^^^^^^^^^
2891+
2892+
``TimeAveraged`` diagnostics are a special type of ``Full`` diagnostics that allows for the output of time-averaged field data.
2893+
This type of diagnostics can be created using ``<diag_name>.diag_type = TimeAveraged``.
2894+
We support only field data and related options from the list at `Full Diagnostics`_.
2895+
2896+
.. note::
2897+
2898+
As with ``Full`` diagnostics, ``TimeAveraged`` diagnostics output the initial **instantaneous** conditions of the selected fields on step 0 (unless more specific output intervals exclude output for step 0).
2899+
2900+
In addition, ``TimeAveraged`` diagnostic options include:
2901+
2902+
* ``<diag_name>.time_average_mode`` (`string`, default `none`)
2903+
Describes the operating mode for time averaged field output.
2904+
2905+
* ``none`` for no averaging (instantaneous fields)
2906+
2907+
* ``fixed_start`` for a diagnostic that averages all fields between the current output step and a fixed point in time
2908+
2909+
* ``dynamic_start`` for a constant averaging period and output at different points in time (non-overlapping)
2910+
2911+
.. note::
2912+
2913+
To enable time-averaged field output with intervals tightly spaced enough for overlapping averaging periods,
2914+
please create additional instances of ``TimeAveraged`` diagnostics.
2915+
2916+
* ``<diag_name>.average_period_steps`` (`int`)
2917+
Configures the number of time steps in an averaging period.
2918+
Set this only in the ``dynamic_start`` mode and only if ``average_period_time`` has not already been set.
2919+
Will be ignored in the ``fixed_start`` mode (with warning).
2920+
2921+
* ``<diag_name>.average_period_time`` (`float`, in seconds)
2922+
Configures the time (SI units) in an averaging period.
2923+
Set this only in the ``dynamic_start`` mode and only if ``average_period_steps`` has not already been set.
2924+
Will be ignored in the ``fixed_start`` mode (with warning).
2925+
2926+
* ``<diag_name>.average_start_step`` (`int`)
2927+
Configures the time step at which time-averaging begins.
2928+
Set this only in the ``fixed_start`` mode.
2929+
Will be ignored in the ``dynamic_start`` mode (with warning).
2930+
28852931
.. _running-cpp-parameters-diagnostics-btd:
28862932

28872933
BackTransformed Diagnostics
28882934
^^^^^^^^^^^^^^^^^^^^^^^^^^^
28892935

28902936
``BackTransformed`` diag type are used when running a simulation in a boosted frame, to reconstruct output data to the lab frame. This option can be set using ``<diag_name>.diag_type = BackTransformed``. We support the following list of options from `Full Diagnostics`_
2937+
28912938
``<diag_name>.format``, ``<diag_name>.openpmd_backend``, ``<diag_name>.dump_rz_modes``, ``<diag_name>.file_prefix``, ``<diag_name>.diag_lo``, ``<diag_name>.diag_hi``, ``<diag_name>.write_species``, ``<diag_name>.species``.
28922939

28932940
Additional options for this diagnostic include:

Docs/source/usage/python.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ Diagnostics
114114

115115
.. autoclass:: pywarpx.picmi.FieldDiagnostic
116116

117+
.. autoclass:: pywarpx.picmi.TimeAveragedFieldDiagnostic
118+
117119
.. autoclass:: pywarpx.picmi.ElectrostaticFieldDiagnostic
118120

119121
.. autoclass:: pywarpx.picmi.Checkpoint

Examples/Physics_applications/laser_ion/CMakeLists.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ add_warpx_test(
66
2 # dims
77
2 # nprocs
88
inputs_test_2d_laser_ion_acc # inputs
9-
analysis_default_openpmd_regression.py # analysis
10-
diags/diag1/ # output
9+
analysis_test_laser_ion.py # analysis
10+
diags/diagInst/ # output
1111
OFF # dependency
1212
)
1313

@@ -16,7 +16,7 @@ add_warpx_test(
1616
2 # dims
1717
2 # nprocs
1818
inputs_test_2d_laser_ion_acc_picmi.py # inputs
19-
analysis_default_openpmd_regression.py # analysis
20-
diags/diag1/ # output
19+
analysis_test_laser_ion.py # analysis
20+
diags/diagInst/ # output
2121
OFF # dependency
2222
)

Examples/Physics_applications/laser_ion/README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Visualize
8787
:alt: Particle densities for electrons (top), protons (middle), and electrons again in logarithmic scale (bottom).
8888
:width: 80%
8989

90-
Particle densities for electrons (top), protons (middle), and electrons again in logarithmic scale (bottom).
90+
Particle densities for electrons (top), protons (middle), and electrons again in logarithmic scale (bottom).
9191

9292
Particle density output illustrates the evolution of the target in time and space.
9393
Logarithmic scales can help to identify where the target becomes transparent for the laser pulse (bottom panel in :numref:`fig-tnsa-densities` ).

Examples/Physics_applications/laser_ion/analysis_default_openpmd_regression.py

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
6+
import numpy as np
7+
import openpmd_api as io
8+
9+
sys.path.insert(1, "../../../../warpx/Regression/Checksum/")
10+
from checksumAPI import evaluate_checksum
11+
12+
13+
def load_field_from_iteration(
14+
series, iteration: int, field: str, coord: str = None
15+
) -> np.ndarray:
16+
"""Load iteration of field data from file."""
17+
18+
it = series.iterations[iteration]
19+
field_obj = it.meshes[f"{field}"]
20+
21+
if field_obj.scalar:
22+
field_data = field_obj[io.Mesh_Record_Component.SCALAR].load_chunk()
23+
elif coord in [item[0] for item in list(field_obj.items())]:
24+
field_data = field_obj[coord].load_chunk()
25+
else:
26+
raise Exception(
27+
f"Specified coordinate: f{coord} is not available for field: f{field}."
28+
)
29+
series.flush()
30+
31+
return field_data
32+
33+
34+
def compare_time_avg_with_instantaneous_diags(dir_inst: str, dir_avg: str):
35+
"""Compare instantaneous data (multiple iterations averaged in post-processing) with in-situ averaged data."""
36+
37+
field = "E"
38+
coord = "z"
39+
avg_period_steps = 5
40+
avg_output_step = 100
41+
42+
path_tpl_inst = f"{dir_inst}/openpmd_%T.h5"
43+
path_tpl_avg = f"{dir_avg}/openpmd_%T.h5"
44+
45+
si = io.Series(path_tpl_inst, io.Access.read_only)
46+
sa = io.Series(path_tpl_avg, io.Access.read_only)
47+
48+
ii0 = si.iterations[0]
49+
fi0 = ii0.meshes[field][coord]
50+
shape = fi0.shape
51+
52+
data_inst = np.zeros(shape)
53+
54+
for i in np.arange(avg_output_step - avg_period_steps + 1, avg_output_step + 1):
55+
data_inst += load_field_from_iteration(si, i, field, coord)
56+
57+
data_inst = data_inst / avg_period_steps
58+
59+
data_avg = load_field_from_iteration(sa, avg_output_step, field, coord)
60+
61+
# Compare the data
62+
if np.allclose(data_inst, data_avg, rtol=1e-12):
63+
print("Test passed: actual data is close to expected data.")
64+
else:
65+
print("Test failed: actual data is not close to expected data.")
66+
sys.exit(1)
67+
68+
69+
if __name__ == "__main__":
70+
# NOTE: works only in the example directory due to relative path import
71+
# compare checksums
72+
evaluate_checksum(
73+
test_name=os.path.split(os.getcwd())[1],
74+
output_file=sys.argv[1],
75+
output_format="openpmd",
76+
)
77+
78+
# TODO: implement intervals parser for PICMI that allows more complex output periods
79+
test_name = os.path.split(os.getcwd())[1]
80+
if "picmi" not in test_name:
81+
# Functionality test for TimeAveragedDiagnostics
82+
compare_time_avg_with_instantaneous_diags(
83+
dir_inst=sys.argv[1],
84+
dir_avg="diags/diagTimeAvg/",
85+
)

Examples/Physics_applications/laser_ion/inputs_test_2d_laser_ion_acc

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -200,18 +200,32 @@ laser1.profile_focal_distance = 4.0e-6 # focal distance from the antenna [m]
200200
#################################
201201
# Diagnostics
202202
#
203-
diagnostics.diags_names = diag1 openPMDfw openPMDbw
203+
diagnostics.diags_names = diagInst diagTimeAvg openPMDfw openPMDbw
204204

205-
diag1.intervals = 100
206-
diag1.diag_type = Full
207-
diag1.fields_to_plot = Ex Ey Ez Bx By Bz jx jy jz rho rho_electrons rho_hydrogen
205+
# instantaneous field and particle diagnostic
206+
diagInst.intervals = 100,96:100 # second interval only for CI testing the time-averaged diags
207+
diagInst.diag_type = Full
208+
diagInst.fields_to_plot = Ex Ey Ez Bx By Bz jx jy jz rho rho_electrons rho_hydrogen
208209
# reduce resolution of output fields
209-
diag1.coarsening_ratio = 4 4
210+
diagInst.coarsening_ratio = 4 4
210211
# demonstration of a spatial and momentum filter
211-
diag1.electrons.plot_filter_function(t,x,y,z,ux,uy,uz) = (uz>=0) * (x<1.0e-6) * (x>-1.0e-6)
212-
diag1.hydrogen.plot_filter_function(t,x,y,z,ux,uy,uz) = (uz>=0) * (x<1.0e-6) * (x>-1.0e-6)
213-
diag1.format = openpmd
214-
diag1.openpmd_backend = h5
212+
diagInst.electrons.plot_filter_function(t,x,y,z,ux,uy,uz) = (uz>=0) * (x<1.0e-6) * (x>-1.0e-6)
213+
diagInst.hydrogen.plot_filter_function(t,x,y,z,ux,uy,uz) = (uz>=0) * (x<1.0e-6) * (x>-1.0e-6)
214+
diagInst.format = openpmd
215+
diagInst.openpmd_backend = h5
216+
217+
# time-averaged particle and field diagnostic
218+
diagTimeAvg.intervals = 100
219+
diagTimeAvg.diag_type = TimeAveraged
220+
diagTimeAvg.time_average_mode = dynamic_start
221+
#diagTimeAvg.average_period_time = 2.67e-15 # period of 800 nm light waves
222+
diagTimeAvg.average_period_steps = 5 # use only either `time` or `steps`
223+
diagTimeAvg.write_species = 0
224+
diagTimeAvg.fields_to_plot = Ex Ey Ez Bx By Bz jx jy jz rho rho_electrons rho_hydrogen
225+
# reduce resolution of output fields
226+
diagTimeAvg.coarsening_ratio = 4 4
227+
diagTimeAvg.format = openpmd
228+
diagTimeAvg.openpmd_backend = h5
215229

216230
openPMDfw.intervals = 100
217231
openPMDfw.diag_type = Full

Examples/Physics_applications/laser_ion/inputs_test_2d_laser_ion_acc_picmi.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140

141141
# Diagnostics
142142
particle_diag = picmi.ParticleDiagnostic(
143-
name="diag1",
143+
name="diagInst",
144144
period=100,
145145
warpx_format="openpmd",
146146
warpx_openpmd_backend="h5",
@@ -153,7 +153,7 @@
153153
for ncell_comp, cr in zip([nx, nz], coarsening_ratio):
154154
ncell_field.append(int(ncell_comp / cr))
155155
field_diag = picmi.FieldDiagnostic(
156-
name="diag1",
156+
name="diagInst",
157157
grid=grid,
158158
period=100,
159159
number_of_cells=ncell_field,
@@ -162,6 +162,18 @@
162162
warpx_openpmd_backend="h5",
163163
)
164164

165+
field_time_avg_diag = picmi.TimeAveragedFieldDiagnostic(
166+
name="diagTimeAvg",
167+
grid=grid,
168+
period=100,
169+
number_of_cells=ncell_field,
170+
data_list=["B", "E", "J", "rho", "rho_electrons", "rho_hydrogen"],
171+
warpx_format="openpmd",
172+
warpx_openpmd_backend="h5",
173+
warpx_time_average_mode="dynamic_start",
174+
warpx_average_period_time=2.67e-15,
175+
)
176+
165177
particle_fw_diag = picmi.ParticleDiagnostic(
166178
name="openPMDfw",
167179
period=100,
@@ -292,6 +304,7 @@
292304
# Add full diagnostics
293305
sim.add_diagnostic(particle_diag)
294306
sim.add_diagnostic(field_diag)
307+
sim.add_diagnostic(field_time_avg_diag)
295308
sim.add_diagnostic(particle_fw_diag)
296309
sim.add_diagnostic(particle_bw_diag)
297310
# Add reduced diagnostics

Examples/Physics_applications/laser_ion/plot_2d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def visualize_particle_histogram_iteration(
259259
"-d",
260260
"--diag_dir",
261261
type=str,
262-
default="./diags/diag1",
262+
default="./diags/diagInst",
263263
help="Directory containing density and field diagnostics",
264264
)
265265
parser.add_argument(

Python/pywarpx/picmi.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3271,6 +3271,57 @@ def diagnostic_initialize_inputs(self):
32713271
ElectrostaticFieldDiagnostic = FieldDiagnostic
32723272

32733273

3274+
class TimeAveragedFieldDiagnostic(FieldDiagnostic):
3275+
"""
3276+
See `Input Parameters <https://warpx.readthedocs.io/en/latest/usage/parameters.html>`__ for more information.
3277+
3278+
Parameters
3279+
----------
3280+
warpx_time_average_mode: str
3281+
Type of time averaging diagnostic
3282+
Supported values include ``"none"``, ``"fixed_start"``, and ``"dynamic_start"``
3283+
3284+
* ``"none"`` for no averaging (instantaneous fields)
3285+
* ``"fixed_start"`` for a diagnostic that averages all fields between the current output step and a fixed point in time
3286+
* ``"dynamic_start"`` for a constant averaging period and output at different points in time (non-overlapping)
3287+
3288+
warpx_average_period_steps: int, optional
3289+
Configures the number of time steps in an averaging period.
3290+
Set this only in the ``"dynamic_start"`` mode and only if ``warpx_average_period_time`` has not already been set.
3291+
Will be ignored in the ``"fixed_start"`` mode (with warning).
3292+
3293+
warpx_average_period_time: float, optional
3294+
Configures the time (SI units) in an averaging period.
3295+
Set this only in the ``"dynamic_start"`` mode and only if ``average_period_steps`` has not already been set.
3296+
Will be ignored in the ``"fixed_start"`` mode (with warning).
3297+
3298+
warpx_average_start_steps: int, optional
3299+
Configures the time step at which time-averaging begins.
3300+
Set this only in the ``"fixed_start"`` mode.
3301+
Will be ignored in the ``"dynamic_start"`` mode (with warning).
3302+
"""
3303+
3304+
def init(self, kw):
3305+
super().init(kw)
3306+
self.time_average_mode = kw.pop("warpx_time_average_mode", None)
3307+
self.average_period_steps = kw.pop("warpx_average_period_steps", None)
3308+
self.average_period_time = kw.pop("warpx_average_period_time", None)
3309+
self.average_start_step = kw.pop("warpx_average_start_step", None)
3310+
3311+
def diagnostic_initialize_inputs(self):
3312+
super().diagnostic_initialize_inputs()
3313+
3314+
self.diagnostic.set_or_replace_attr("diag_type", "TimeAveraged")
3315+
3316+
if "write_species" not in self.diagnostic.argvattrs:
3317+
self.diagnostic.write_species = False
3318+
3319+
self.diagnostic.time_average_mode = self.time_average_mode
3320+
self.diagnostic.average_period_steps = self.average_period_steps
3321+
self.diagnostic.average_period_time = self.average_period_time
3322+
self.diagnostic.average_start_step = self.average_start_step
3323+
3324+
32743325
class Checkpoint(picmistandard.base._ClassWithInit, WarpXDiagnosticBase):
32753326
"""
32763327
Sets up checkpointing of the simulation, allowing for later restarts

0 commit comments

Comments
 (0)