Source code for pycif.plugins.models.lagrangian.io.inputs.fluxes_ad

import copy
import datetime
import glob
import os
import tracemalloc
from logging import debug

import numpy as np
import pandas as pd

from ......utils.parallel import thread
from ...utils.read import read_footprint_grid
from ......utils.check.errclass import CifError


[docs] def flux_contribution_ad( self, mode, dataobs, fp_header_nest, fp_header_glob, spec, ddi ): """Compute the adjoint flux sensitivity from FLEXPART/STILT footprints. For each observation in *dataobs*, reads the footprint grid and accumulates the adjoint flux sensitivity (``adj_out`` departure) into the in-memory flux store ``self.dataflx``, indexed by period and grid cell. Args: self: Lagrangian model plugin instance. mode (str): ``'adj'``. dataobs: CIF observation data-store for the period (contains ``'adj_out'`` departure values). fp_header_nest: FLEXPART header for the nested-domain footprints. fp_header_glob: FLEXPART header for the outer-domain footprints. spec (str): species name. ddi (datetime): sub-simulation period start. """ nlat, nlon = self.domain.zlat.shape ndates = len(self.input_dates[ddi]) # Loading fluxes flux = self.dataflx[("flux", spec)][ddi]["spec"] flx_dates = pd.DatetimeIndex(flux.time.values).to_pydatetime() # Execute parallel threads nthreads = self.nthreads thread_intervals = np.linspace(0, len(dataobs), nthreads + 1).astype(int) @thread def thread_function(ithread): flx_sensit_tmp = np.zeros((ndates + 1, 1, nlat, nlon)) for obs_i in range(thread_intervals[ithread], thread_intervals[ithread + 1]): process_obs_row( self, dataobs, ithread, fp_header_nest, fp_header_glob, flux, flx_sensit_tmp, flx_dates, obs_i ) return flx_sensit_tmp flx_sensit = sum(thread_function(range(nthreads))) # Flush fluxes self.dataflx[("flux", spec)][ddi]["spec"] = None return flx_sensit
[docs] def process_obs_row( self, dataobs, ithread, fp_header_nest, fp_header_glob, flux, flx_sensit_tmp, flx_dates, obs_i ): """Compute the adjoint flux sensitivity for a single observation row. Reads the footprint grids for the observation at *obs_i*, weights them by ``adj_out``, and accumulates the result into *flx_sensit_tmp*. Args: self: Lagrangian model plugin instance. dataobs: CIF observation data-store for the period. ithread (int): thread index. fp_header_nest: nested-domain FLEXPART header. fp_header_glob: outer-domain FLEXPART header. flux (dict): forward flux arrays (for background subtraction). flx_sensit_tmp: thread-local accumulator for adjoint flux sensitivity. flx_dates: dates of the flux grid. obs_i (int): row index into the observation data-store. """ row = dataobs["metadata"].iloc[obs_i] mainrow = dataobs["maindata"].iloc[obs_i] station = row.station network = row.network spec = row.parameter molarmass = getattr(self.chemistry.acspecies, spec.upper()).molarmass # Translate station name if needed if hasattr(self, "station_name_dict"): station = self.dict_station_name[station.upper()].upper() # Infer folder structure subdir = row.date.strftime(self.footprint_dir_format) release_date = row.date - pd.to_timedelta(self.release_shift) file_date = release_date.strftime(self.footprint_date_format) # Read nested grids runsubdir_nest = os.path.join( self.run_dir_nest, self.footprint_stat_subdir_format.format( stat=station.upper(), network=network.upper()), subdir) file_name = self.file_nest_format.format( date=file_date, stat=station.upper(), network=network.upper()) list_valid = glob.glob(os.path.join(runsubdir_nest, file_name)) if list_valid == []: debug(f"WARNING: file not found: {os.path.join(runsubdir_nest, file_name)}") return elif len(list_valid) > 1: raise CifError( f"Multiple files fit the specified format {self.file_ini_format}. " f"This can be related to the use of a wildcard... " f"Please check your yml" ) file_name = os.path.basename(list_valid[0]) debug(f"Thread #{ithread}: Reading {file_name} for station {station}") grid_nest, gtime, ngrid, valid_file = \ read_footprint_grid(self, runsubdir_nest, file_name, release_date, fp_header_nest, numscale=self.numscale, stilt=self.footprint_type == "STILT") # Conversion of footprints grid_nest *= self.coeff * self.mmair / molarmass # Apply decay if any decay_coef = np.ones((ngrid, 1)) if hasattr(self, "exp_decay"): exp_decay = self.exp_decay halflife = pd.to_timedelta(exp_decay.halflife) / np.log(2) decay_coef = np.exp( -np.maximum(0, (row.date - np.array(gtime)) / halflife) )[:, np.newaxis] if exp_decay.inverse_decay: decay_coef = 1 - decay_coef nest_sensit = grid_nest.T[:ngrid].reshape(ngrid, -1) # Find time steps to compare inds_flx = (np.argmin( (np.array(gtime)[:, np.newaxis] - flx_dates[np.newaxis, :]) >= datetime.timedelta(0), axis=1) - 1) % len(flx_dates) zeros = np.zeros((inds_flx.size, self.domain.zlon_in.size), dtype=int) np.add.at( flx_sensit_tmp, (inds_flx.reshape(-1, 1), zeros, zeros, np.arange(self.domain.zlon_in.size)[np.newaxis, :]), nest_sensit * mainrow.adj_out, ) # Read global footprints # TODO: read correction factor dry air! if not self.domain.nested: return runsubdir_glob = os.path.join( self.run_dir_glob, station.upper(), subdir) file_name = self.file_glob_format.format( date=file_date, stat=station.upper(), network=network.upper()) list_valid = glob.glob(os.path.join(runsubdir_nest, file_name)) if list_valid == []: debug(f"WARNING: file not found: {os.path.join(runsubdir_nest, file_name)}") return elif len(list_valid) > 1: raise CifError( f"Multiple files fit the specified format {self.file_ini_format}. " f"This can be related to the use of a wildcard... " f"Please check your yml" ) file_name = os.path.basename(list_valid[0]) debug(f"Thread #{ithread}: Reading {file_name} for station {station}") grid_glob, gtime_glob, ngrid_glob, valid_file = \ read_footprint_grid(self, runsubdir_glob, file_name, release_date, fp_header_glob, numscale=self.numscale) # Conversion of footprints grid_glob *= self.coeff * self.mmair / molarmass # Keep only valid grids glob_sensit = grid_glob.T[:ngrid_glob].reshape(ngrid_glob, -1) # Find time steps to compare inds_flx = (np.argmin( (np.array(gtime_glob)[:, np.newaxis] - flx_dates[np.newaxis, :]) >= datetime.timedelta(0), axis=1) - 1) % len(flx_dates) zeros = np.zeros( (inds_flx.size, self.domain.zlon.size - self.domain.zlon_in.size), dtype=int) np.add.at( flx_sensit_tmp, (inds_flx.reshape(-1, 1), zeros, zeros, np.arange(self.domain.zlon_in.size, self.domain.zlon.size)[np.newaxis, :]), glob_sensit * mainrow.adj_out, )