Source code for pycif.plugins.models.lagrangian.utils.read

""" Routines for reading Lagrangian footprints

"""

import os
import numpy as np
import pandas as pd
from scipy.io import FortranFile
import datetime
from logging import info, warning
from netCDF4 import Dataset


[docs] def read_footprint_grid(self, subdir, file_name, release_date, fp_header, stilt=False, **kwargs): """ Reads footprints files. Convert s.m3/kg to s.m2/kg Inputs: file_name - full path name to file name fp_header - a Flexpart header object Returns: grid_fp: Array (nlon, nlat, ntime) of footprints gtime : Array (maxngrid) of file dates """ # Numeric scaling factor numscale = float(kwargs.get("numscale", 1.E12)) path_file = os.path.join(subdir, file_name) scaleconc = 1.e12 if stilt: grid_fp, ngrid, gtime_dt, valid_file = \ read_grid_stilt(self, path_file, release_date, fp_header) elif "nc" in path_file: grid_fp, ngrid, gtime_dt, valid_file = \ read_grid_nc(path_file, release_date, fp_header) else: grid_fp, ngrid, gtime_dt, valid_file = \ read_grid(path_file, release_date, fp_header, scaleconc) # Convert from ppt to ppmv or ppbv # Convert s.m3/kg to s.m2/kg and apply numerical scaling grid_fp = grid_fp / (fp_header.outheight[0] * numscale) return grid_fp, gtime_dt, ngrid, valid_file
[docs] def read_grid(path_file, release_date, fp_header, scaleconc): """Read a FLEXPART binary footprint grid file. Parses the Fortran-unformatted binary format (``grid_time_*_001`` or ``grid_initial_*_001``) and reconstructs the sparse footprint array as a dense ``(nlon, nlat, ntimes)`` array. Args: path_file (str): path to the binary footprint file. release_date (pd.Timestamp): observation release date. fp_header: FLEXPART domain header object (carries grid metadata). scaleconc (float): unit scaling factor applied to the footprint. Returns: tuple: ``(grid_fp, gtime, ngrid, valid_file)`` where *grid_fp* is the dense footprint array, *gtime* is the list of footprint dates, *ngrid* is the time dimension, and *valid_file* is ``True`` if the file was successfully read. """ days = [] times = [] counts_i = [] counts_r = [] sparse_i = [] sparse_r = [] valid_file = False with FortranFile(path_file, 'r') as f: # Looping until the end of the binary file while True: try: yyyymmdd = f.read_ints('i4')[0].astype(str) hhmmss = f"{f.read_ints('i4')[0]:06d}" i = f.read_ints('i4') spi = f.read_ints('i4') r = f.read_ints('i4') spr = f.read_reals('f4') days.append(yyyymmdd) times.append(hhmmss) counts_i.extend(i) sparse_i.extend(spi) counts_r.extend(r) sparse_r.extend(spr) except TypeError: break gtime_dt = [datetime.datetime.strptime(f'{d}{h}', '%Y%m%d%H%M%S') for d, h in zip(days, times)][::-1] ngrid = len(gtime_dt) grid_fp = np.zeros((fp_header.numx, fp_header.numy, ngrid + 2)) if len(sparse_r) > 0: sign = np.sign(sparse_r) cum_counts = np.unique(np.cumsum(counts_r) % sign.size) signchange = ((np.roll(sign, 1) - sign) != 0) signchange[cum_counts] = True inds_out = np.zeros((len(sparse_r)), dtype=int) inds_out[signchange] = sparse_i mask = inds_out == 0 idx = np.where(~mask, np.arange(mask.size), 0) np.maximum.accumulate(idx, out=idx) inds_out = inds_out[idx] + np.arange(mask.size) \ - idx - fp_header.numx * fp_header.numy try: jy, jx = np.unravel_index( inds_out, (fp_header.numy, fp_header.numx)) jt = np.zeros((len(sparse_r)), dtype=int) jt[cum_counts] = np.arange(ngrid)[np.array(counts_r) > 0] np.maximum.accumulate(jt, out=jt) jt = ngrid - jt - 1 grid_fp[jx, jy, jt] = np.abs(sparse_r) * scaleconc valid_file = True except ValueError: warning(f"Could not properly read file {path_file}. Returning zero footprints") pass grid_fp = np.roll(grid_fp, fp_header.xshift, axis=0) return grid_fp, ngrid, gtime_dt, valid_file
[docs] def read_grid_nc(path_file, release_date, fp_header): """Read a FLEXPART NetCDF footprint file. Reads the ``spec001`` variable from the NetCDF footprint file and returns a dense ``(nlon, nlat, ntimes)`` footprint array. Args: path_file (str): path to the NetCDF footprint file. release_date (pd.Timestamp): observation release date. fp_header: FLEXPART domain header object. Returns: tuple: ``(grid_fp, gtime, ngrid, valid_file)``. """ valid_file = False with Dataset(path_file) as nc: # spec001 has dimension nageclass, numpoint, time, level, lat, lon # Assume that there is 1 numpoint/station per file shape = nc['spec001'].shape if shape[0] > 1 or shape[1] > 1: info('WARNING: There are more than 1 station in the flexpart file.') if shape[3] > 1: info('INFO: Select the bottom layer of the grid') grid = nc['spec001'][0, 0, :, 0, :, :].data.astype(np.float64) # time, # lat, lon # swap axes to get it to lon, lat, time grid_fp = np.swapaxes(grid, 0, -1) times = np.sort(nc['time'][:]) enddate = datetime.datetime.strptime(nc.getncattr('iedate'), '%Y%m%d') gtime = [] for t in times: gtime.append(enddate + datetime.timedelta(seconds=int(t))) valid_file = True grid_fp *= 1e12 # This is applied in mod_flexpart return grid_fp, len(gtime), gtime, valid_file
[docs] def read_grid_stilt(self, path_file, release_date, fp_header): """Read a STILT footprint CSV file. Reads the STILT footprint format (one CSV per observation) and reconstructs a dense ``(nlon, nlat, nhours)`` footprint array matched to the CIF domain grid. Args: self: Lagrangian model plugin instance (carries ``backward_trajdays`` and ``domain``). path_file (str): path to the STILT footprint CSV file. release_date (pd.Timestamp): observation release date. fp_header: FLEXPART header (for grid metadata; only ``numpoint`` used). Returns: tuple: ``(grid_fp, gtime, ngrid, valid_file)``. """ nhours = int(pd.to_timedelta( self.backward_trajdays).total_seconds() / 3600) grid_fp = np.zeros((self.domain.nlon, self.domain.nlat, nhours)) gtime = release_date.to_pydatetime() \ - np.arange(nhours) * datetime.timedelta(hours=1) valid_file = False # Try several times in case there is a hick-up in data trials = 0 while not valid_file and trials < 3: try: with Dataset(path_file, "r") as f: if release_date.strftime("ftp_%Y%m%d%H_lon") not in f.variables: info(f"No footprint is available for date {release_date}") return grid_fp, len(gtime), gtime, valid_file lon = f.variables[release_date.strftime("ftp_%Y%m%d%H_lon")][:] lat = f.variables[release_date.strftime("ftp_%Y%m%d%H_lat")][:] val = f.variables[release_date.strftime("ftp_%Y%m%d%H_val")][:] ind = f.variables[release_date.strftime( "ftp_%Y%m%d%H_indptr")][:] valid_file = True except RuntimeError: trials += 1 nb_indexes = np.diff(ind) hours2process = np.where(nb_indexes != 0)[0] domain = self.domain xmin = domain.xmin xmax = domain.xmax nlon = domain.nlon dx = (xmax - xmin) / nlon ymin = domain.ymin ymax = domain.ymax nlat = domain.nlat dy = (ymax - ymin) / nlat ind_lon = ((lon - xmin) / dx).astype(int) ind_lat = ((lat - ymin) / dy).astype(int) ind_hour = np.zeros(len(lon)) # np.append(0, np.cumsum(nb_indexes)) ind_hour[ np.minimum(len(lon) - 1, np.append(0, np.cumsum(nb_indexes[hours2process])[:-1])) ] = np.arange(nhours)[hours2process] ind_hour = np.maximum.accumulate(ind_hour).astype(int) + 1 np.add.at(grid_fp, (ind_lon, ind_lat, ind_hour), val) return grid_fp, len(gtime), gtime, valid_file
[docs] def read_flexpart_gridinit(subdir, filename, fp_header, scaleconc=1, **kwargs): """Read a FLEXPART initial-condition sensitivity binary file. Parses ``grid_initial_*_001`` Fortran-binary files and reconstructs the sparse initial-condition sensitivity as a dense ``(nlon, nlat, nlev)`` array. Args: subdir (str): directory containing the file. filename (str): file name (without directory). fp_header: FLEXPART domain header object. scaleconc (float): unit scaling factor. **kwargs: unused. Returns: tuple: ``(grid_fp, gtime, ngrid, valid_file)`` where *grid_fp* is the dense sensitivity array, *gtime* is the sensitivity date list, *ngrid* is the time dimension, and *valid_file* is ``True`` on success. """ path_file = os.path.join(subdir, filename) with FortranFile(path_file, 'r') as f: yyyymmdd = f.read_ints('i4')[0].astype(str) hhmmss = f"{f.read_ints('i4')[0]:06d}" sp_count_i = f.read_ints('i4')[0] sparse_i = f.read_ints('i4') sp_count_r = f.read_ints('i4')[0] sparse_r = f.read_reals('f4') nx = fp_header.numx ny = fp_header.numy nz = (fp_header.outheight != 0.).sum() grid_init = np.zeros((nx, ny, nz)) if len(sparse_r) > 0: sign = np.sign(sparse_r) cum_counts = np.unique(np.cumsum(sp_count_r) % sign.size) signchange = ((np.roll(sign, 1) - sign) != 0) signchange[cum_counts] = True inds_out = np.zeros((len(sparse_r)), dtype=int) inds_out[signchange] = sparse_i mask = inds_out == 0 idx = np.where(~mask, np.arange(mask.size), 0) np.maximum.accumulate(idx, out=idx) inds_out = inds_out[idx] + np.arange(mask.size) \ - idx - fp_header.numx * fp_header.numy jz, jy, jx = np.unravel_index(inds_out, (nz, fp_header.numy, fp_header.numx)) grid_init[jx, jy, jz] = np.abs(sparse_r) * scaleconc grid_init = np.roll(grid_init, fp_header.xshift, axis=0) return grid_init
[docs] def get_spec(subdir, **kwargs): """ Get species name from simulation output """ with open(os.path.join(subdir, 'header_txt'), 'r') as f: txt = f.readlines() spec = txt[20].split()[1] info("species", spec) return spec