Source code for pycif.plugins.models.iconart.io.outputs.apply_interpolation

import xarray as xr
import numpy as np
import glob
import os
import multiprocessing as mp
import pandas as pd
import time as tm
from itertools import repeat
from logging import info, debug
import math

from ..utils import OUTPUT_DIRNAME, OUTPUT_PATTERN
from ......utils.hdf5 import _hdf5_lock


#TODO: remove occurences of "height"", not supposed to know the name
# If all observations are processed
[docs] def apply_interpolation_by_chunk_full(segment_idx_obs, ds_icon, ds_interp, all_trcrs): """Apply full 3-D interpolation to a chunk of observations. Uses pre-computed adjacent-cell indices and vertical level indices (``ilev_below``, ``ilev_above``) from *ds_interp* to perform bilinear horizontal + linear vertical interpolation from the ICON icosahedral grid to observation coordinates. Args: segment_idx_obs (tuple[int, int]): (start, end) slice of the observation index in *ds_interp*. ds_icon (xr.Dataset): ICON output dataset on the icosahedral grid. ds_interp (xr.Dataset): pre-computed interpolation metadata. all_trcrs (list[str]): tracer variable names to interpolate. Returns: xr.Dataset: interpolated concentrations for the observation chunk. """ debug(segment_idx_obs) idx_obs_start, idx_obs_end = segment_idx_obs ds_interp_red = ds_interp.isel(index=slice(idx_obs_start, idx_obs_end)) ilev_below = ds_interp_red['ilev_below'].astype(int) ilev_above = ds_interp_red['ilev_above'].astype(int) iadjcells = ds_interp_red['iadjcells'] vert_scaling_fact = ds_interp_red['vert_scale_fact'] inv_dist_adjcells = ds_interp_red['inv_dist_adjcells'] vals_lev_above = ilev_above.values + 1 vals_lev_above = xr.DataArray(vals_lev_above, dims=("obs", "adjcell")) vals_lev_below = ilev_below.values + 1 vals_lev_below = xr.DataArray(vals_lev_below, dims=("obs", "adjcell")) vals_cell = iadjcells.values vals_cell = xr.DataArray(vals_cell, dims=("obs", "adjcell")) vals_vert_scal = vert_scaling_fact vals_inv_dist = inv_dist_adjcells ds_above_adj = ds_icon[all_trcrs].sel(height=vals_lev_above, ncells=vals_cell) ds_below_adj = ds_icon[all_trcrs].sel(height=vals_lev_below, ncells=vals_cell) ds_adj = ds_below_adj + vals_vert_scal.values * (ds_above_adj - ds_below_adj) ds_out = (ds_adj * vals_inv_dist.values).sum(dim='adjcell') / vals_inv_dist.sum(dim='adjcell').values return ds_out
# If observations are grouped by index
[docs] def apply_interpolation_by_chunk_reduced(segment_idx_obs, ds_icon, ds_interp, df_metadata, all_trcrs): """Apply reduced (level-fixed) interpolation to a chunk of observations. Like :func:`apply_interpolation_by_chunk_full` but does not perform vertical interpolation: uses the observation's prescribed level index directly. Used when ``full_interpolation=False`` or when observations specify only a pressure level without altitude. Args: segment_idx_obs (tuple[int, int]): (start, end) observation slice. ds_icon (xr.Dataset): ICON output on icosahedral grid. ds_interp (xr.Dataset): pre-computed interpolation metadata. df_metadata: observation metadata DataFrame (carries level column). all_trcrs (list[str]): tracer variable names to interpolate. Returns: xr.Dataset: interpolated concentrations for the observation chunk. """ debug(segment_idx_obs) idx_obs_start, idx_obs_end = segment_idx_obs ds_interp_red = ds_interp.isel(index=slice(idx_obs_start, idx_obs_end)) # Data that do not need to be interpolated vertically mask_alt_nan = np.isnan(ds_interp_red['ilev_below'][:, 0].values) index_without_alt = ds_interp_red.index[mask_alt_nan].values iadjcells = ds_interp_red['iadjcells'][mask_alt_nan] inv_dist_adjcells = ds_interp_red['inv_dist_adjcells'][mask_alt_nan] vals_cell = iadjcells.values vals_cell = xr.DataArray(vals_cell, dims=("obs_red", "adjcell")) vals_inv_dist = inv_dist_adjcells min_ilev = 1 #TODO: Find a better solution max_ilev = 60 #TODO: Find a better solution ds_adj = ds_icon[all_trcrs].sel(ncells=vals_cell, height=slice(min_ilev, max_ilev + 1)) ds_out = (ds_adj * vals_inv_dist.values).sum(dim='adjcell') / vals_inv_dist.sum(dim='adjcell').values ds_out = ds_out.reindex(height=list(reversed(ds_out.height))) ds_out = ds_out.stack(obs=("obs_red", "height"), create_index=False) ds_out = ds_out.drop_vars('height') ds_out['obs'] = df_metadata.loc[index_without_alt, 'iobs'].values # Data that need to be interpolated vertically if np.sum(~mask_alt_nan): index_with_alt = ds_interp_red.index[~mask_alt_nan].values ilev_below = ds_interp_red['ilev_below'][~mask_alt_nan] ilev_above = ds_interp_red['ilev_above'][~mask_alt_nan] iadjcells = ds_interp_red['iadjcells'][~mask_alt_nan] vert_scaling_fact = ds_interp_red['vert_scale_fact'][~mask_alt_nan] inv_dist_adjcells = ds_interp_red['inv_dist_adjcells'][~mask_alt_nan] vals_lev_above = ilev_above.values + 1 vals_lev_above = xr.DataArray(vals_lev_above, dims=("obs", "adjcell")) vals_lev_below = ilev_below.values + 1 vals_lev_below = xr.DataArray(vals_lev_below, dims=("obs", "adjcell")) vals_cell = xr.DataArray(iadjcells.values, dims=("obs", "adjcell")) vals_vert_scal = vert_scaling_fact vals_inv_dist = inv_dist_adjcells ds_above_adj = ds_icon[all_trcrs].sel(height=vals_lev_above, ncells=vals_cell) ds_below_adj = ds_icon[all_trcrs].sel(height=vals_lev_below, ncells=vals_cell) ds_adj = ds_below_adj + vals_vert_scal.values * (ds_above_adj - ds_below_adj) ds_out2 = (ds_adj * vals_inv_dist.values).sum(dim='adjcell') / vals_inv_dist.sum(dim='adjcell').values ds_out2['obs'] = df_metadata.loc[index_with_alt, 'iobs'] ds_out = xr.concat([ds_out, ds_out2], dim='obs') ds_out = ds_out.sortby('obs') return ds_out
[docs] def apply_interpolation(self, runsubdir, data2dump): """Interpolate ICON-ART output fields to all observation locations. Reads NetCDF output files from ``{runsubdir}/OUTPUT/``, computes distance-weighted horizontal interpolation to observation station coordinates using pre-computed adjacent-cell metadata, applies vertical interpolation (full or reduced), and stores results in ``self.sim_data``. Uses :func:`apply_interpolation_by_chunk_full` or :func:`apply_interpolation_by_chunk_reduced` depending on whether altitude is available in the observation metadata. Args: self: ICON-ART model plugin instance. runsubdir (str): path to the period run directory. data2dump (dict): tracer-ID-keyed data-store (provides the observation metadata for interpolation targets). """ output_path = os.path.join(runsubdir, OUTPUT_DIRNAME) # --------------------------------------------------------- # -- Fetch data, interpolate and convert to DataFrame # --------------------------------------------------------- info('Concatenating the files...') file_path_day = os.path.join(output_path, "concatenated_byday", OUTPUT_PATTERN) list_files_day = sorted(glob.glob(str(file_path_day))) with _hdf5_lock: ds_icon = xr.open_mfdataset(list_files_day, concat_dim='time', combine='nested', decode_cf=False) # Load the interpolation data interpolation_filename = os.path.join( runsubdir, OUTPUT_DIRNAME, "interpolation/ds_data_interpolation.nc") with _hdf5_lock: ds_interp = xr.open_dataset(interpolation_filename) # Create dataout dataout = data2dump.copy() dataout = pd.concat([dataout['metadata'], dataout['maindata']], axis=1) dataout = dataout[[ 'date', 'station', 'network', 'parameter', 'lon', 'lat', 'alt', 'i', 'j', 'level', 'tstep', 'tstep_glo', 'dtstep', 'duration', 'is_obsvect', 'obs', 'obserror' ]] # Extract station information nlev = self.domain.nlev # TODO: get tstep from ds_interp, dont use anymore data2dump, must be species-agnostic df_metadata = data2dump[('metadata')].copy() df_metadata['iobs'] = range(len(df_metadata)) obs_tsteps = df_metadata['tstep'].values.astype(int) vals_time = xr.DataArray(obs_tsteps, dims=("obs")) # Extract variable information dim_height = ds_icon['z_mc'].dims[1] all_trcrs = [tr for tr in list(ds_icon.data_vars) \ if ds_icon.data_vars[tr].dims == ('time', dim_height, 'ncells')] # Create all columns for output data dataout_all_trcrs = pd.DataFrame(index=dataout.index, columns=all_trcrs) dataout = pd.concat([dataout, dataout_all_trcrs], axis=1) # Prepare the chunks to apply the interpolation over all obs in ds_interp # Number of obs in ds_interp might be different from number # of obs in df_metadata nchunks = 36 nobs = len(ds_interp.index) nobs_per_chunk = int(np.ceil(nobs / nchunks)) bounds_idx_obs = list(range(0, nobs, nobs_per_chunk)) + [nobs] list_segments_idx_obs = [[idx_start, idx_end] for (idx_start, idx_end) in zip(bounds_idx_obs[:-1], bounds_idx_obs[1:])] # Apply the interpolation for every chunk info(f'Applying inverse-distance weighting interpolation with {nchunks} nchunks...') if self.full_interpolation: func_arguments = zip(list_segments_idx_obs, repeat(ds_icon), repeat(ds_interp), repeat(all_trcrs)) with mp.Pool(min(nchunks, 36)) as pool: list_ds_out = pool.starmap(apply_interpolation_by_chunk_full, func_arguments) # Fill dataout with interpolated data ds_out_concat = xr.concat(list_ds_out, dim='obs') ds_out_concat = ds_out_concat.isel(time=vals_time) dataout.loc[:, all_trcrs] = ds_out_concat.to_array().values.T else: func_arguments = zip(list_segments_idx_obs, repeat(ds_icon), repeat(ds_interp), repeat(df_metadata), repeat(all_trcrs)) with mp.Pool(min(nchunks, 36)) as pool: list_ds_out = pool.starmap(apply_interpolation_by_chunk_reduced, func_arguments) # Fill dataout with interpolated data ds_out_concat = xr.concat(list_ds_out, dim='obs') ds_out_concat = ds_out_concat.isel(time=vals_time) dataout.loc[:, all_trcrs] = ds_out_concat.to_array().values.T return dataout