7. Developers Guide for PION¶
7.1. Coding style and guides¶
PION is written in C++ and developed using git as the version control system.
The PION development branch is on a DIAS gitlab repo https://git.dias.ie/compastro/pion, and the name of the development branch is devel
.
All active development of new features takes place on devel
.
Once a set of changes are complete and a new version of PION is to be released, devel
will be merged/rebased back to the master
branch and tagged as a release.
The new master
branch is then pushed to the public PION repository at https://git.dias.ie/massive-stars-software/pion.
7.1.1. Developing a new feature¶
Feature branches can be created off devel
as follows, using the gitlab web interface:
Create an issue with a short title describing the feature you want to work on.
Open the issue page, and there is a dropdown menu for
create merge request
. Click the down arrow and choose a name for the new branch (the number automatically chosen is the number of the issue you have created) and select that the source branch should bedevel
. Then clickcreate merge request
.This brings you to a page for the merge request which is by default marked as
Draft
. You don’t need to do anything here, just note the name of the branch, e.g.2-test-stuff
.Pull the server updates to your local copy of the repo, and checkout the new branch:
$ git checkout 2-test-stuff
.Then you can work on this branch until you are finished with the new feature and it is time to merge the branch back into devel. There are at least two checks to make before merging:
- run
$ ./tools/run_format.sh
to autoformat your changes according to the PION conventions. Then commit and push changes. - Pushing to a branch off
devel
should trigger the pipeline to run. This is a series of jobs that are sent to a server and run in a docker container somewhere. The first checks that the source code is formatted. The next 4 check that the PION executables compile without errors, and the last runs the Double Mach Reflection test and checks that the result has an L1 error norm of \(<10^{-10}\) compared with a reference solution (i.e. it checks for consistency with older code versions).
- run
Once these steps are completed successfully, navigate to the merge request page on your browser and click
Mark as ready
at the top right of the page.Beside the (hopefully green)Merge
button you should selectdelete source branch
andsquash commits
. You can add an informative squash commit message that explains what you did on this branch with all the commits. Then clickMerge
.Check that the merged changes now on
devel
pass the pipeline checks (this can take up to 20 mins).
7.1.2. Coding conventions¶
- Set your code editor to use spaces not tabs for indentation (or run
$ ./tools/run_format.sh
before each commit). - Each indendation block is 2 spaces
- Try to add comments to your code to explain what it does, including references to papers if possible.
- Header files should have class and function descriptions in comment blocks denoted with
///
so that automatic documentation generators (like doxygen) can parse the comments.
7.1.3. Generating documentation from source code¶
PION has plenty of in-place documentation that can be generated with doxygen. Here is how to compile the docs:
- On debian/ubuntu systems you can get all the functionality by installing the packages
doxygen doxygen-latex graphviz
. - Add
-DPION_BUILD_DOCUMENTATION=ON
to the list of options in your cmake command (maybe in the build script). - When you build PION this should generate documentation in the directory
build/doc/doc
, the HTML docs in a subdirectory calledhtml
and the LaTeX docs inlatex
. - Navigate to this directory and run e.g.
$ firefox html/index.html
- Use the dropdown menus to explore the class hierarchy and the source code.
- To generate a PDF you can navigate to the
latex
subdirectory of the generated docs and run$ make
; this should generate a PDF calledrefman.pdf
.
7.2. Computational Grid¶
The PION computational grid is a uniform rectilinear grid in 1, 2 or 3 spatial dimensions. PION can be run in “uniform-grid” mode or “nested-grid” mode, depending on whether static mesh-refinement is needed. The nested-grid mode sets up a hierarchy of grids with different levels of refinement, each refined grid fully enclosed by the coarser grid one level up in the hierarchy. The uniform-grid mode sets up a single grid and there is no mesh refinement. Grid cells are all the same diameter for a given refinement level, and they are squares (2D) or cubes (3D) in the space that is modelled.
- Terminology used in this guide:
- the terms cell and zone are used interchangeably and mean the same thing.
- ghost cells and boundary cells have the same meaning.
- static mesh-refinement and nested-grid are both used to denote the mesh-refinement strategy used in PION, sometimes interchangeably.
- The six directions in 3D space are denoted \(\{\pm\hat{x},\pm\hat{y},\pm\hat{z}\}\)
7.2.1. Grid Geometry¶
There are 5 options for the combinations of coordinate system and dimensionality that have been implemented:
- Cartesian geometry, 1D simulations: this is a line in the \(\pm\hat{x}\) direction that represents an infinite slab (gradients of all quantities in the two perpenicular directions are set to zero).
- Spherical geometry 1D simulations: a spherically symmetric configuration is modelled, again with gradients in the angular directions set to zero. In this case geometric source terms are added to the fluid equations because each cell is a spherical shell and its volume increases with radius.
- Cartesian geometry, 2D simulations: each point on the 2D plane represents an infinite line in the 3rd dimension (gradients perpendicular to the plane are zero), and the divergence of a quantity is linear with distance rather than quadratic. This is of limited use for modelling physical systems, but is useful for development work and exploring parameter space.
- Cylindrical geometry, so-called 2.5D simulations: the \((R,z)\) plane is simulated assuming rotational symmetry about the axis \(R=0\). Again for cylindrical coordinates a geometric source term arises in the radial direction because each cell is a ring (of square cross section) in 3D space and its volume increases with cylindrical radius. Rotation and toroidal magnetic fields are possible in this configuration, but gradients in the angular directions are obviously zero.
- Cartesian geometry, 3D simulations: the 3D domain is represented by a block of cubic cells.
7.2.2. Mesh refinement¶
Static mesh-refinement (i.e., a multiply nested-grid structure) is implemented for the case where each refinement level has the same shape and number of cells as the coarser level above it, but the spatial resolution is a factor of 2 higher and so the refined grid covers 1/2 of the domain of the coarser grid in each dimension. The focus of the nested grid can be at the centre of the domain, the negative boundary or the positive boundary for each dimension. The number of refinement levels is in principle unlimited, but snapshots can only be written every coarse timestep and this imposes a practical limit: for 10 grid levels there are 512 finest-level timesteps per coarsest-level step, and one probably wants to save a snapshot at least this often. The coarsest level is denoted level 0, and each refined level has a higher level number.
7.2.3. Serial and Parallel grid setup¶
PION can be run in serial mode, in which case there is a single process that calculates everything (i.e., no parallel execution). In this case, the grid corresponding to the full domain is set up as a single block of data for each refinement level. PION can also be run in parallel mode, so that \(N_p\) MPI processes are started and these calculate in parallel. On each refinement level the full domain is split into \(N_p\) sub-domains and each MPI process sets up a grid of cells corresponding only to its sub-domain (plus some boundary data for synchronisation). Each MPI process has exactly 1 sub-domain on each refinement level.
7.2.4. Location of source files¶
The source files controlling the grid are located in source/grid/
, particularly uniform_grid.cpp
(single-domain mode) and uniform_grid_pllel.cpp
(MPI parallel mode with multiple sub-domains).
Classes for setting up grids are as follows:
- Uniform grid, single level, single domain:
setup_fixed_grid.cpp
- Uniform grid, single level, MPI-parallel, multiple domains:
setup_fixed_grid_MPI.cpp
- Static mesh-refinement, single domain:
setup_NG_grid.cpp
- Static mesh-refinement, MPI-parallel, multiple domains:
setup_grid_NG_MPI.cpp
7.3. Control Flow¶
The different versions of PION have slightly different control flow, depending on whether PION is run in single-core or parallel mode, and nested-grid or uniform-grid mode. The time-integration scheme is based on that of Falle (1991) and Falle et al. (1998); the uniform-grid implementation is described in some detail in Mackey (2012), and the nested-grid implementation in Mackey et al. (2021) (section 2.4). You are encouraged to read these papers to gain some understanding of what each step does. Here the most complicated routine will be described: the parallel implementation of the nested-grid method.
7.3.1. Outer loop¶
The overall control of PION comes from the main()
function defined in the following files in the source
directory of PION:
+ uniform grid, serial mode: main.cpp
+ uniform grid, parallel mode: main_MPI.cpp
+ nested grid, serial mode: main_NG.cpp
+ nested grid, parallel mode: main_NG_MPI.cpp
This code should rarely need to be updated, however, because it just checks that the command-line options are OK, then in turn calls Init()
, Time_Int()
and Finalise()
functions from the relevant simulation-control class.
Everything in Init()
and Finalise()
is only called once per simulation run and so efficiency is not crucial here.
The function Time_Int()
is responsible for integrating the initial conditions forward in time to a final state after a (usually large) number of timesteps.
The versions of Time_Int()
can be found here:
- uniform grid, serial mode:
sim_control/sim_control.cpp
- uniform grid, parallel mode:
sim_control/sim_control_MPI.cpp
- nested grid, serial mode:
sim_control/sim_control_NG.cpp
- nested grid, parallel mode:
sim_control/sim_control_NG_MPI.cpp
This routine is responsible for
- setting the timestep for the next step;
- saving snapshots periodically according to the chosen criteria;
- stepping forward in time repeatedly until
- the execution is stopped when the criterion is reached for ending the simulation.
Each timestep is taken by the function advance_time()
.
- The uniform-grid version of this (serial and parallel) is found in
sim_control/time_integrator.cpp
, and is split into a 1st-order step and a 2nd-order step. This refers to the order of accuracy of the integration scheme: the 1st-order scheme is basically forward Euler integration with piecewise-constant data, whereas the 2nd-order scheme involves two sub-steps to achieve better accuracy. Initially a 1st-order step is taken for half of the requested timestep, to obtain a time-centred intermediate state. A piecewise-linear interpolation of the data is then applied to this state, and the fluxes across cell boundaries are calculated. These fluxes are used to step the initial state forward by the full timestep (i.e. the intermediate state is discarded once the fluxes have been calculated). This step is accurate to 2nd order in space and time (see Falle et al. (1998)). - The nested-grid version (serial and parallel) is found in
sim_control/sim_control_NG.cpp
, and calls eitheradvance_step_OA1()
for a 1st-order step oradvance_step_OA2()
for a 2nd-order step.
Note that the 1st-order scheme is only used for testing and development purposes, and all scientific applications should use the 2nd-order scheme because it is always more accurate for a given computational expense.
7.3.2. 2nd-order timestep for nested-grid, parallel run¶
The most complicated (and most useful) timestepping routine in PION is advance_step_OA2()
for the nested-grid, parallel execution, found in sim_control/sim_control_NG_MPI.cpp
.
This is a recursive function, first called on the coarsest grid level 0, which then calls itself repeatedly until the finest level is reached.
In this way, calling the function a single time on level 0 will advance all levels one step on level 0 (2 steps on level 1, 4 steps on level 1, \(2^n\) steps on level \(n\)).
- The first task is to ensure that data on all levels are consistent:
- Receive any external boundary data from a coarser level, if required:
BC_update_COARSE_TO_FINE_RECV()
. This can involve MPI communication if the required data is on another MPI process. - Update external boundary conditions, including boundary data between sub-domains on the grid level associated with different MPI processes:
TimeUpdateExternalBCs()
. This usually involves MPI communication. - Send any boundary data from this level to the outer boundary of the next finer level, if required:
BC_update_COARSE_TO_FINE_SEND()
. Also usually involves MPI communication, if the destination grid is on another MPI process.
- Receive any external boundary data from a coarser level, if required:
- Next take a timestep on the next finer level, if it exists (i.e. call the
advance_step_OA2()
function on the next finer level). - Then calculate the 1st-order half-step on this level (none of this involves MPI communication):
- Calculate changes associated with radiative heating and cooling, and chemical kinetics:
calc_microphysics_dU()
- Calculate changes from (magneto-)hydrodynamical fluxes across cell boundaries:
calc_dynamics_dU()
- Calculate changes due to thermal conduction of internal energy (not really working):
calc_thermal_conduction_dU()
- Calculate the half-step intermediate state based on the above changes:
grid_update_state_vector()
- Calculate changes associated with radiative heating and cooling, and chemical kinetics:
- Now update various quantities to the intermediate half-step values:
- update internal boundaries (i.e. stellar winds):
TimeUpdateInternalBCs()
- Receive data from finer levels to replace calculated data (assuming finer level is more accurate):
BC_update_FINE_TO_COARSE_RECV()
. This generally involves MPI communication. - Update external boundaries, including boundary data between sub-domains on the grid level associated with different MPI processes:
TimeUpdateExternalBCs()
. This generally involves MPI communication. - Upate properties of radiation sources (if present) and do raytracing on this grid:
update_evolving_RT_sources()
anddo_ongrid_raytracing()
. This generally involves MPI communication.
- update internal boundaries (i.e. stellar winds):
- Calculate the full-step 2nd-order update, again microphysics, dynamics, (thermal conduction), but this time make some corrections before finalising the update:
- If not the coarsest level, save fluxes crossing the level boundary
save_fine_fluxes()
- If not the finest level, save fluxes crossing cell boundaries corresponding to the level boundary of the next finer level
save_coarse_fluxes()
- If level has data for outer boundary of finer level, send the data
BC_update_COARSE_TO_FINE_SEND()
. This generally involves MPI communication.
- If not the coarsest level, save fluxes crossing the level boundary
- Calculate a second step on the finer level by recursively calling
advance_step_OA2()
- Receive flux values from level boundary of finer level, and correct fluxes on this level accordingly:
recv_BC89_fluxes_F2C()
. This generally involves MPI communication. - Update state on this level for the full step:
grid_update_state_vector()
, and advance the current time. - Update internal boundaries (i.e. stellar winds):
TimeUpdateInternalBCs()
- Replace data with value averaged from finer grids:
BC_update_FINE_TO_COARSE_RECV()
. This generally involves MPI communication. - Update properties of radiation sources (if present) to end of timestep and do raytracing on this grid:
update_evolving_RT_sources()
anddo_ongrid_raytracing()
. This generally involves MPI communication. - Send fluxes crossing level boundary to the next coarser level (for correcting fluxes at that level):
send_BC89_fluxes_F2C()
. This generally involves MPI communication. - Send level data to coarser level, to replace what was calculated there:
BC_update_FINE_TO_COARSE_SEND()
. This generally involves MPI communication. - Return so that the next step can take place.
Without the nested grid levels, the steps are much simpler and the following function calls are omitted:
BC_update_COARSE_TO_FINE_RECV()
, BC_update_COARSE_TO_FINE_SEND()
, BC_update_FINE_TO_COARSE_RECV()
, save_fine_fluxes()
, save_coarse_fluxes()
, BC_update_COARSE_TO_FINE_SEND()
, recv_BC89_fluxes_F2C()
, BC_update_FINE_TO_COARSE_RECV()
, send_BC89_fluxes_F2C()
, BC_update_FINE_TO_COARSE_SEND()
.
The only functions requiring MPI communication are TimeUpdateExternalBCs()
and do_ongrid_raytracing()
.
7.4. Parallel communication¶
Parallel execution in PION is implemented using the Message Passing Interface (MPI) library with a multi-core and multiple domain model. This is based on domain splitting, where each level of the computational grid is recursively bisected \(i\) times along the longest dimension until there are \(N_P = 2^i\) domains, one for each of the \(N_P\) MPI processes. For nested-grid simulations the grid has the same shape and size (in terms of numbers of cells) on all levels, and so the same decomposition is performed at each level, and each MPI process has exactly 1 sub-domain (or patch) on each level.
Interprocess communication is in PION is coded such that the implementation and library used is completely encapsulated in a class, so that calls to MPI functions are not scattered throughout the code.
The motivation for this is so that a different communication library could be used in future without significant modification of the PION source code.
There is an abstract base class, comms_base
, defined in source/comms/comms.h
, which contains the API for interprocess communication.
This contains function declarations for sending and receiving data between processes, for the Silo parallel I/O interface, and various other common tasks such as broadcasting and calculating miminum or maximum across sub-domains.
There are two comms libraries implemented:
- The class
comm_files
insource/comms/comm_files.h/.cpp
is purely for code testing and debugging parallel communication. It is a very simple communication library that uses disk I/O to send and receive data between processes (and hence it is very slow). The advantage is that the communicated data can be parsed outside PION if an error occurs, helping with debugging. This library is, however, poorly maintained, and may no longer work as intended. - The class
comm_mpi
insource/comms/comm_mpi.h/.cpp
is used in all other cases. This uses the system MPI library to call various MPI functions, enabling interprocess communication. It also uses the Silo PMPIO library for parallel I/O. The rest of this section describes the MPI implementation in more detail.
Whether one uses the file-comms or MPI is a compile-time decision, and a (global) pointer to the communications class called COMM
is initialised with the correct class at run-time.
7.4.1. Domain decomposition¶
The decomposition of the domain is controlled by the “Multi-Core, Multiple-Domain (MCMD)” class in decomposition/MCMD_control.cpp
.
The function decomposeDomain()
is called by Init()
from either sim_control/sim_control_MPI.cpp
(uniform grid, called on a single level) or sim_control/sim_control_NG_MPI.cpp
(nested grid, called once for each level).
This is called before each process sets up the grid for its sub-domain, and the function sets the extents and numbers of cells for each sub-domain based on the MPI rank of the process.
It also sets the ranks of the neighbouring MPI processes, if applicable.
7.4.2. Calculating timestep¶
PION uses an explicit time-integration scheme for the ideal-MHD or Euler equations, which is stable as long as no signal can cross a full grid cell in a single timestep. The function calculate_timestep()
in source/sim_control/calc_timestep.h/.cpp
provides the single-process implementation, and this is re-implemented as sim_control_pllel::calculate_timestep()
for multi-process simulations in source/sim_control/sim_control_MPI.h/.cpp
.
The only difference compared with the single-process version is that the global minimum timestep is calculated across all sub-domains, via calls to the COMM
class pointer.
7.4.3. Exchange of boundary data on sub-domains¶
Layers of boundary (or ghost) cells are added to pad each sub-domain, constituting duplicated data that is calculated on the neighbouring sub-domains. The layers are 4 cells deep for boundaries between sub-domains, and 6 cells deep for the outer boundaries of each level’s domain.
for a 3D simulation the \(\pm\hat{x}\) boundaries have the smallest extent (extending to \([y_\mathrm{min},y_\mathrm{max}]\) and \([z_\mathrm{min},z_\mathrm{max}]\)) and \(\pm\hat{z}\) boundaries are the largest, extending to include the edge and corner ghost cells in the \(\pm\hat{x}\) and \(\pm\hat{y}\) directions. The layout of the boundaries is plotted in Fig. 7.1, showing how the shape of the sub-domain plus boundaries cuboid is the same as that of the sub-domain itself, with extra layers of cells on all sides.
- There are two boundary exchanges that use the boundaries of the sub-domains:
- Data are exchanged with neighbouring sub-domains each timestep for data on the same level, to ensure that the solution remains consistent across all sub-domains (and identical to a calculation run on a single process with a single domain). These boundaries are called “MPI” boundaries in PION, defined in
boundaries/MCMD_boundaries.h/.cpp
, called byTimeUpdateExternalBCs()
inboundaries/assign_update_bcs_MPI.cpp
at every timestep. - Raytracing to calculate photoionization and photoheating rates must be calculated when radiation sources are present. This involves communication of attenuation factors across sub-domains, with send and receive functions defined in
boundaries/RT_MPI_boundaries.h/.cpp
, called byraytracing/raytracer_SC_pllel.cpp
every time a raytracing is required.
- Data are exchanged with neighbouring sub-domains each timestep for data on the same level, to ensure that the solution remains consistent across all sub-domains (and identical to a calculation run on a single process with a single domain). These boundaries are called “MPI” boundaries in PION, defined in
7.4.4. Exchange of data between levels of the nested grid¶
There are three further boundary communications needed between levels of a nested grid:
Restriction: If a grid has a more refined grid covering part of its sub-domain, then at each timestep the refined grid averages the data from blocks of \(2^3\) cells and sends this to the coarser grid. The received data overwrites anything calculated by the coarse grid. This is called restriction, and is referred to in the PION source code as “FINE2COARSE”.
- It is implemented in the classes
NG_fine_to_coarse_bc
(serial) andNG_MPI_fine_to_coarse_bc
(parallel) that are defined in the filesboundaries/NG_fine_to_coarse_boundaries.h/.cpp
andboundaries/NG_MPI_fine_to_coarse_boundaries.h/.cpp
, respectively.- the function
setup_grid_NG_MPI::setup_boundary_structs()
ingrid/setup_grid_NG_MPI.cpp
addsFINE_TO_COARSE_RECV
and/orFINE_TO_COARSE_SEND
boundaries to the list of boundaries if this sub-domain needs to receive smoothed data from a finer-level sub-domain or needs to send smoothed data to a coarser-level sub-domain.- There are functions to assign the boundary data to be sent/received. These are called only once, in the function
assign_update_bcs_NG_MPI::assign_boundary_data()
defined inboundaries/assign_update_bcs_NG_MPI.cpp
.- There are function to update the boundary data, i.e. exchange data between levels, possibly on another MPI process. These are called every timestep by the functions
Time_Int()
andadvance_step_OA1()
oradvance_step_OA2()
insim_control/sim_control_NG_MPI.cpp
.Prolongation: All refined grids are enclosed by coarser grids, and the outer boundary of a refined grid therefore needs to be updated by interpolating the solution from the coarse grid onto the external ghost cells of the refined grid. This process is known as prolongation. It is trickier and more computationally intensive than restriction, because of the interpolation and the requirement to conserve mass, momentum and energy. PION calculates slopes in the primitive variables in each direction for each coarse cell, and sends the primitive vector and the 3 slope vectors to the refined grid. The refined grid then interpolates onto the cell-centres of its cells using these slopes, and applies a correction to ensure conservation is maintained. In PION source code this process is denoted “COARSE2FINE”.
- It is implemented in the classes
NG_coarse_to_fine_bc
(serial) andNG_MPI_coarse_to_fine_bc
(parallel) in the filesboundaries/NG_MPI_coarse_to_fine_boundaries.h
andboundaries/NG_MPI_coarse_to_fine_boundaries.cpp
, respectively.- the function
setup_grid_NG_MPI::setup_boundary_structs()
ingrid/setup_grid_NG_MPI.cpp
sets external sub-domain boundaries to beCOARSE_TO_FINE_RECV
type if needed. It also addsCOARSE_TO_FINE_SEND
boundaries to the list of boundaries if finer-level sub-domains below this sub-domain has level boundaries.- Again there are functions to assign the boundary data to be sent/received, called only once, in the function
assign_update_bcs_NG_MPI::assign_boundary_data()
defined inboundaries/assign_update_bcs_NG_MPI.cpp
.- There are function to update the boundary data, i.e. exchange data between levels, possibly on another MPI process. These are called every timestep by the functions
Time_Int()
andadvance_step_OA1()
oradvance_step_OA2()
insim_control/sim_control_NG_MPI.cpp
.Flux Conservation: To maintain conservation, the flux crossing the boundaries of refined grids must be kept consistent with the flux crossing the same surface in the coarser grids. This process was pioneered by Berger & Colella (1989), hence in PION the methods are referred to as “BC89”. The cells on the fine and coarse grids that have a face on the outer boundary of the fine grid are identified and an extra flux vector F is initialised only on these cells. Corner cells may have up to three of these flux vectors, so they are put in a vector of arrays, but most face cells only have one. Time-averaged fluxes over two fine-grid timesteps are sent to the coarse grid and these are used to correct the coarse-grid fluxes to be consistent with the fine grid (it is assumed that the fine grid is more accurate).
- This is implemented in the class
NG_BC89flux
(serial) andNG_MPI_BC89flux
(parallel), defined inboundaries/NG_BC89flux.cpp
andboundaries/NG_MPI_BC89flux.h/.cpp
, respectively.- The setup functions make a list of cells that are on a level boundary and need flux correction (both to send to coarser grid, and to receive from finer grid). The function
NG_BC89flux::setup_flux_vectors()
inboundaries/NG_BC89flux.cpp
(serial) andboundaries/NG_MPI_BC89flux.cpp
(parallel) is called bysetup_grid_NG_MPI::setup_grid()
ingrid/setup_grid_NG_MPI.cpp
. The parallel implementation decides whether the grid with the BC89 boundary that we need to exchange data with is on the same MPI process (call serial function) or on a different process (call the MPI-parallel function).- The update functions,
recv_BC89_fluxes_F2C()
andsend_BC89_fluxes_F2C()
, exchange data between levels, possibly on another MPI process. These are called every timestep by the functionsTime_Int()
andadvance_step_OA1()
oradvance_step_OA2()
insim_control/sim_control_NG_MPI.cpp
.
7.4.5. Parallel input/output (read from/write to disk)¶
The only parallel I/O implementation that is optimised for speed is using the Silo library and its PMPIO interface. This takes a round-robin approach to reading and writing data, where \(N\) MPI processes (about 8 or 16) can work simultaneously. Each MPI process is assigned to one of \(N\) groups of processes, and only one process per group can read or write at any one time. When it is finished, it passes on a baton to the next process in its group, which then reads/writes its data, and so on. The motivation is that most supercomputers support multiple simultaneous connections to the file system for parallel I/O, but the number of these is much smaller than the number of cores on the system. Each level of the nested grid is written to its own file, and all data on all levels is saved.
#.. include:: dev_winds.inc