Source code for fbpic.openpmd_diag.field_diag

# Copyright 2016, FBPIC contributors
# Authors: Remi Lehe, Manuel Kirchen
# License: 3-Clause-BSD-LBNL
"""
This file defines the class FieldDiagnostic.
"""
import os
import numpy as np
from .generic_diag import OpenPMDDiagnostic

[docs] class FieldDiagnostic(OpenPMDDiagnostic): """ Class that defines the field diagnostics to be performed. """ def __init__(self, period=None, fldobject=None, comm=None, fieldtypes=["rho", "E", "B", "J"], write_dir=None, iteration_min=0, iteration_max=np.inf, dt_period=None ) : """ Initialize the field diagnostic. Parameters ---------- period : int, optional The period of the diagnostics, in number of timesteps. (i.e. the diagnostics are written whenever the number of iterations is divisible by `period`). Specify either this or `dt_period`. dt_period : float (in seconds), optional The period of the diagnostics, in physical time of the simulation. Specify either this or `period` fldobject : a Fields object Points to the data that has to be written at each output comm : an fbpic BoundaryCommunicator object or None If this is not None, the data is gathered on the first proc, and the guard cells are removed from the output. Otherwise, each proc writes its own data, including guard cells. (Make sure to use different write_dir in this case.) fieldtypes : a list of strings, optional The strings are either "rho", "E", "B" or "J" and indicate which field should be written. Default : all fields are written write_dir : string, optional The POSIX path to the directory where the results are to be written. If none is provided, this will be the path of the current working directory. iteration_min, iteration_max: ints The iterations between which data should be written (`iteration_min` is inclusive, `iteration_max` is exclusive) """ # Check input if fldobject is None: raise ValueError( "You need to pass the argument `fldobject` to `FieldDiagnostic`.") # General setup OpenPMDDiagnostic.__init__(self, period, comm, write_dir, iteration_min, iteration_max, dt_period=dt_period, dt_sim=fldobject.dt ) # Register the arguments self.fld = fldobject self.fieldtypes = fieldtypes self.coords = ['r', 't', 'z'] def write_hdf5( self, iteration ): """ Write an HDF5 file that complies with the OpenPMD standard Parameter --------- iteration : int The current iteration number of the simulation. """ # If needed: Bring rho/J from spectral space (where they where # smoothed/corrected) to real space if "rho" in self.fieldtypes: # Get 'rho_prev', since it correspond to rho at time n self.fld.spect2interp('rho_prev') # Exchange rho in real space if needed if (self.comm is not None) and (self.comm.size > 1) \ and (not self.fld.exchanged_source['rho_prev']): self.comm.exchange_fields(self.fld.interp, 'rho', 'add') if "J" in self.fieldtypes: self.fld.spect2interp('J') # Exchange J in real space if needed if (self.comm is not None) and (self.comm.size > 1) \ and (not self.fld.exchanged_source['J']): self.comm.exchange_fields(self.fld.interp, 'J', 'add') # If needed: Receive data from the GPU if self.fld.use_cuda : self.fld.receive_fields_from_gpu() # Extract information needed for the openPMD attributes dt = self.fld.dt time = iteration * dt dz = self.fld.interp[0].dz if self.comm is None: # No communicator: dump all the present subdomain # (including damp cells and guard cells) zmin = self.fld.interp[0].zmin Nz = self.fld.interp[0].Nz Nr = self.fld.interp[0].Nr else: # Communicator present: only dump physical cells zmin, _ = self.comm.get_zmin_zmax( local=False, with_damp=False, with_guard=False ) Nz, _ = self.comm.get_Nz_and_iz( local=False, with_damp=False, with_guard=False ) Nr = self.comm.get_Nr( with_damp=False ) # Create the file with these attributes filename = "data%08d.h5" %iteration fullpath = os.path.join( self.write_dir, "hdf5", filename ) self.create_file_empty_meshes( fullpath, iteration, time, Nr, Nz, zmin, dz, dt ) # Open the file again, and get the field path f = self.open_file( fullpath ) # (f is None if this processor does not participate in writing data) if f is not None: field_path = "/data/%d/fields/" %iteration field_grp = f[field_path] else: field_grp = None # Loop over the different quantities that should be written for fieldtype in self.fieldtypes: # Scalar field if fieldtype == "rho": self.write_dataset( field_grp, "rho", "rho" ) # Vector field elif fieldtype in ["E", "B", "J"]: for coord in self.coords: quantity = "%s%s" %(fieldtype, coord) path = "%s/%s" %(fieldtype, coord) self.write_dataset( field_grp, path, quantity ) # PML field (save as scalar field) elif fieldtype.endswith("_pml"): self.write_dataset( field_grp, fieldtype, fieldtype ) else: raise ValueError("Invalid string in fieldtypes: %s" %fieldtype) # Close the file (only the first proc does this) if f is not None: f.close() # Send data to the GPU if needed if self.fld.use_cuda : self.fld.send_fields_to_gpu() # Writing methods # --------------- def write_dataset( self, field_grp, path, quantity ) : """ Write a given dataset Parameters ---------- field_grp : an h5py.Group object The group that corresponds to the path indicated in meshesPath path : string The relative path where to write the dataset, in field_grp quantity : string Describes which field is being written. (Either rho, Er, Et, Ez, Br, Bz, Bt, Jr, Jt or Jz) """ # Extract the correct dataset if field_grp is not None: dset = field_grp[path] else: dset = None # Write the mode 0 : only the real part is non-zero mode0 = self.get_dataset( quantity, 0 ) if self.rank == 0: mode0 = mode0.T dset[0,:,:] = mode0[:,:].real # Write the higher modes # There is a factor 2 here so as to comply with the convention in # Lifschitz et al., which is also the convention adopted in Warp Circ for m in range(1,self.fld.Nm): mode = self.get_dataset( quantity, m ) if self.rank == 0: mode = mode.T dset[2*m-1,:,:] = 2*mode[:,:].real dset[2*m,:,:] = 2*mode[:,:].imag def get_dataset( self, quantity, m ): """ Get the field `quantity` in the mode `m` Gathers it on the first proc, in MPI mode Parameters ---------- quantity: string Describes which field is being written. (Either rho, Er, Et, Ez, Br, Bz, Bt, Jr, Jt or Jz) m: int The index of the mode that is being written """ # Get the data on each individual proc data_one_proc = getattr( self.fld.interp[m], quantity ) # Gather the data if self.comm is not None: data_all_proc = self.comm.gather_grid_array( data_one_proc ) else: data_all_proc = data_one_proc return( data_all_proc ) # OpenPMD setup methods # --------------------- def create_file_empty_meshes( self, fullpath, iteration, time, Nr, Nz, zmin, dz, dt ): """ Create an openPMD file with empty meshes and setup all its attributes Parameters ---------- fullpath: string The absolute path to the file to be created iteration: int The iteration number of this diagnostic time: float (seconds) The physical time at this iteration Nr, Nz: int The number of gridpoints along r and z in this diagnostics zmin: float (meters) The position of the left end of the box dz: float (meters) The resolution in z of this diagnostic dt: float (seconds) The timestep of the simulation """ # Determine the shape of the datasets that will be written # First write real part mode 0, then imaginary part of higher modes data_shape = ( 2*self.fld.Nm - 1, Nr, Nz ) # Create the file f = self.open_file( fullpath ) # Setup the different layers of the openPMD file # (f is None if this processor does not participate is writing data) if f is not None: # Setup the attributes of the top level of the file self.setup_openpmd_file( f, iteration, time, dt ) # Setup the meshes group (contains all the fields) field_path = "/data/%d/fields/" %iteration field_grp = f.require_group(field_path) self.setup_openpmd_meshes_group(field_grp) # Loop over the different quantities that should be written # and setup the corresponding datasets for fieldtype in self.fieldtypes: # Scalar field # e.g. 'rho', but also 'rho_electron' in the case of # the sub-class ParticleDensityDiagnostic # or PML component (saved as scalar field as well) if fieldtype.startswith("rho") or fieldtype.endswith("_pml"): # Setup the dataset dset = field_grp.require_dataset( fieldtype, data_shape, dtype='f8') self.setup_openpmd_mesh_component( dset, fieldtype ) # Setup the record to which it belongs self.setup_openpmd_mesh_record( dset, fieldtype, dz, zmin ) # Vector field elif fieldtype in ["E", "B", "J"]: # Setup the datasets for coord in self.coords: quantity = "%s%s" %(fieldtype, coord) path = "%s/%s" %(fieldtype, coord) dset = field_grp.require_dataset( path, data_shape, dtype='f8') self.setup_openpmd_mesh_component( dset, quantity ) # Setup the record to which they belong self.setup_openpmd_mesh_record( field_grp[fieldtype], fieldtype, dz, zmin ) # Unknown field else: raise ValueError( "Invalid string in fieldtypes: %s" %fieldtype) # Close the file f.close() def setup_openpmd_meshes_group( self, dset ) : """ Set the attributes that are specific to the mesh path Parameter --------- dset : an h5py.Group object that contains all the mesh quantities """ # Field Solver dset.attrs["fieldSolver"] = np.string_("PSATD") # Field boundary dset.attrs["fieldBoundary"] = np.array([ np.string_("reflecting"), np.string_("reflecting"), np.string_("reflecting"), np.string_("reflecting") ]) # Particle boundary dset.attrs["particleBoundary"] = np.array([ np.string_("absorbing"), np.string_("absorbing"), np.string_("absorbing"), np.string_("absorbing") ]) # Current Smoothing dset.attrs["currentSmoothing"] = np.string_("Binomial") dset.attrs["currentSmoothingParameters"] = \ np.string_("period=1;numPasses=1;compensator=false") # Charge correction dset.attrs["chargeCorrection"] = np.string_("spectral") dset.attrs["chargeCorrectionParameters"] = np.string_("period=1") def setup_openpmd_mesh_record( self, dset, quantity, dz, zmin ) : """ Sets the attributes that are specific to a mesh record Parameter --------- dset : an h5py.Dataset or h5py.Group object quantity : string The name of the record (e.g. "rho", "J", "E" or "B") dz: float (meters) The resolution in z of this diagnostic zmin: float (meters) The position of the left end of the grid """ # Generic record attributes self.setup_openpmd_record( dset, quantity ) # Geometry parameters dset.attrs['geometry'] = np.string_("thetaMode") dset.attrs['geometryParameters'] = \ np.string_("m={:d};imag=+".format(self.fld.Nm)) dset.attrs['gridSpacing'] = np.array([ self.fld.interp[0].dr, dz ]) dset.attrs["gridGlobalOffset"] = np.array([ self.fld.interp[0].rmin, zmin ]) dset.attrs['axisLabels'] = np.array([ b'r', b'z' ]) # Generic attributes dset.attrs["dataOrder"] = np.string_("C") dset.attrs["gridUnitSI"] = 1. dset.attrs["fieldSmoothing"] = np.string_("none") def setup_openpmd_mesh_component( self, dset, quantity ) : """ Set up the attributes of a mesh component Parameter --------- dset : an h5py.Dataset or h5py.Group object quantity : string The field that is being written """ # Generic setup of the component self.setup_openpmd_component( dset ) # Field positions dset.attrs["position"] = np.array([0.5, 0.5])