Source code for pycif.plugins.transforms.basic.time_interpolation.utils.indexes

import numpy as np
import pandas as pd
import copy
import datetime
from logging import warning, debug
from ......utils.classes.transforms import Transform

from ......utils.check.errclass import CifError


[docs] def calc_indexes(mapper, trid_out, all_transforms, general_mapper): """Calculate indexes correspondance between input dates and output dates. The calculation relies on averages between inputs and outputs, weighted by the relative duration of targets compared to available inputs. Args: mapper (dict[str]): the mapper for the present transform trid_out (tuple[str]): all_transforms (Transform): the object storing information on all transforms general_mapper (dict[str]): the overall mapper for all transforms """ # Alias to variables in_dict = mapper["inputs"][trid_out] out_dict = mapper["outputs"][trid_out] in_sparse = in_dict.get("sparse_data", False) out_sparse = out_dict.get("sparse_data", False) in_dates = in_dict["input_dates"] out_dates = out_dict["input_dates"] # Check if another transform with same settings was initialized list_tinterp = [ transf for transf in all_transforms.attributes if getattr(all_transforms, transf).plugin.name == "time_interpolation"] for transf in list_tinterp: inputs = general_mapper[transf]["inputs"] in_trid = [] for trid in inputs: target_dates = inputs[trid].get("input_dates", {}) if in_dates.keys() == target_dates.keys(): all_equals = np.all([ False if len(in_dates[dd]) != len(target_dates[dd]) else np.all(in_dates[dd] == target_dates[dd]) for dd in in_dates]) if all_equals: in_trid.append(trid) outputs = general_mapper[transf]["outputs"] for trid in outputs: if trid in in_trid: target_dates = outputs[trid].get("input_dates", {}) if len(out_dates) != len(target_dates): continue if out_dates.keys() != target_dates.keys(): continue same_dates = [ False if len(out_dates[ddi]) != len(target_dates[ddi]) else out_dates[ddi].equals(target_dates[ddi]) for ddi in target_dates ] if ~np.all(same_dates): continue mapper["interpol_indexes"] = \ general_mapper[transf]["interpol_indexes"] mapper["do_interpolation"] = \ general_mapper[transf]["do_interpolation"] mapper["reorder_periods"] = \ general_mapper[transf]["reorder_periods"] in_dict["force_loadin"] = \ inputs[trid]["force_loadin"] debug("Fetching temporal interpolation indexes " "from another transform") return # Otherwise, do the job do_interpolation = {datei: False for datei in out_dates} reorder_periods = {datei: False for datei in out_dates} force_loadin = {datei: False for datei in in_dates} interpol_indexes = {} total_lengths = {} for datei in out_dates: # Special case if only one date if np.size(out_dates[datei]) == 0: continue elif np.size(out_dates[datei]) == 1: raise CifError( f"Output date {out_dates[datei]} for {trid_out} should be an interval (start and end date), even if they are the same. Please check your YAML configuration file." ) else: out_dates_start = out_dates[datei]["start_date"] out_dates_end = out_dates[datei]["end_date"] # Initialize a flag to check that all output dates are covered covered_output_dates = np.zeros_like(out_dates_start, dtype=bool) # Loop over input periods and find corresponding output periods tmp_indexes = {} total_length = pd.Series(np.zeros(len(out_dates_start))) length_per_input_period = [] ddi_masks = {} for ddi in in_dates: # Skip if empty input dates if len(in_dates[ddi]) == 0: continue # Skip if input date interval do not include any output dates if np.max(in_dates[ddi]["end_date"]) < np.min(out_dates_start) \ or np.min(in_dates[ddi]["start_date"]) > np.max(out_dates_end): continue in_dates_start = in_dates[ddi]["start_date"] # Special case if only one date if np.size(in_dates[ddi]) == 1: in_dates_end = in_dates_start + datetime.timedelta(hours=1) else: in_dates_end = in_dates[ddi]["end_date"] in_dates_all = in_dates[ddi].stack().reset_index( drop=True).drop_duplicates() in_dates_durations = \ pd.Series(in_dates_end - in_dates_start).dt.total_seconds() # Crop out_dates to the date interval covered by input dates ddi_mask = ( (out_dates_start <= in_dates_all.max()) & (out_dates_end >= in_dates_all.min()) ) ddi_masks[ddi] = ddi_mask out_dates_end_tmp = out_dates_end[ddi_mask] out_dates_start_tmp = out_dates_start[ddi_mask] covered_output_dates[ddi_mask] = True # Removing output dates duplicates to speed up the process out_dates_tmp = pd.DataFrame( data={'date_start': out_dates_start_tmp, 'date_end': out_dates_end_tmp, 'index': range(len(out_dates_end_tmp)) } ) unique_index = out_dates_tmp.drop_duplicates( ['date_start', 'date_end'])['index'].values out_dates_end_tmp = out_dates_end_tmp.iloc[unique_index] out_dates_start_tmp = out_dates_start_tmp.iloc[unique_index] # Find index of start dates index_tmpstart = \ pd.concat([in_dates_all, out_dates_start_tmp] ).drop_duplicates().sort_values() df_index = pd.Series(range(len(in_dates_all)), index=in_dates_all) df_index = df_index.reindex(index_tmpstart) start_inside = df_index.interpolate( method="index", limit_area="inside").loc[out_dates_start_tmp] df_index = df_index.interpolate( method="index", limit_direction="both") out_index_start = df_index.loc[out_dates_start_tmp] # Find index of end dates index_tmpend = \ np.sort(np.unique(np.append(in_dates_all, out_dates_end_tmp))) df_index = \ pd.Series(range(len(in_dates_all)), index=in_dates_all) df_index = df_index.reindex(index_tmpend) end_inside = df_index.interpolate( method="index", limit_area="inside").loc[out_dates_end_tmp] df_index = df_index.interpolate( method="index", limit_direction="both") out_index_end = df_index.loc[out_dates_end_tmp] # Compute weights for each time step # If outputs are snapshots (i.e., start and end dates are the same), # define duration as 1 by default out_durations = ( np.ceil(out_index_end.values) - np.floor(out_index_start.values)).astype(int) out_durations[out_dates_end_tmp == out_dates_start_tmp] = 1 out_durations[ np.isnan(start_inside).values & np.isnan(end_inside).values] = 0 out_df = pd.DataFrame({"start": out_index_start.values, "end": out_index_end.values, "duration": out_durations}) out_df.index = unique_index weights = copy.deepcopy( out_df.loc[ out_df.index.repeat( out_df["duration"])]) # Skip if weights are empty if len(weights) == 0: continue indexes = np.zeros(len(weights)) start_index = np.array( [0] + list(np.cumsum( out_df["duration"].loc[out_df["duration"] > 0].values[:-1])) ).astype(int) if len(weights) > 0: indexes[start_index] = start_index np.maximum.accumulate(indexes, out=indexes) weights["indexes"] = \ np.arange(len(weights)) - indexes + np.floor(weights["start"]) weights["weights"] = np.maximum( 1, np.minimum(weights["indexes"] + 1, weights["end"]) - np.maximum(weights["indexes"], weights["start"]) ) # Tweak index for snapshots at the end of the last input period mask_tweak = (weights["indexes"] == len(in_dates_start)) weights.loc[mask_tweak, "indexes"] -= 1 # Scale weights by duration of each sub-period mask_wgt = weights["weights"] != 0 weights.loc[mask_wgt, "weights"] *= \ in_dates_durations.iloc[ weights.loc[mask_wgt, "indexes"]].values group_weights = weights["weights"].groupby(by=weights.index).sum() total_length.loc[ np.where(ddi_mask)[0][group_weights.index] ] += group_weights.values tmp_indexes[ddi] = weights.loc[:, ["indexes", "weights"]] tmp_indexes[ddi]["total_weights"] = weights["weights"].sum() tmp_indexes[ddi]["period"] = ddi do_interpolation[datei] = True force_loadin[ddi] = True # Avoid recombining periods if asked if not mapper.get("recombine_periods", True): recombined = pd.concat(tmp_indexes.values()) indmax = \ recombined.reset_index().groupby( "index").idxmax()["total_weights"] recombined = recombined.iloc[indmax.values] tmp_indexes = { ddi: recombined.loc[recombined["period"] == ddi] for ddi in tmp_indexes} total_length = pd.concat(tmp_indexes.values()) total_length = \ total_length.groupby(total_length.index).sum()["weights"] # Check whether the inputs and outputs have exactly the same index # In that case, there is no need to do interpolation same_periods = [ np.all(tmp_indexes[ddi].index.values == tmp_indexes[ddi]["indexes"].values) for ddi in tmp_indexes] full_periods = [ len(out_dates_start) == len(in_dates[ddi]) and len(tmp_indexes[ddi]) != 0 for ddi in tmp_indexes] do_interpolation[datei] = ( (tmp_indexes != {}) and (~np.all(same_periods) or (sum(full_periods) != 1)) ) # Check whether the transform is only a re-ordering of sub-periods # Applies only if there is no interpolation to do # (i.e., files can be used as a whole) reorder = { ddi: out_dates[datei].equals(in_dates[ddi]) if len(out_dates[datei]) == len(in_dates[ddi]) != 0 else len(out_dates[datei]) == len(in_dates[ddi]) for ddi in in_dates } reorder_periods[datei] = [ reorder[ddi] and not do_interpolation[datei] for ddi in in_dates] # If no interpolation to be done, but found no re-ordering, force interpolation if not np.any(reorder_periods[datei]) and not do_interpolation[datei]: do_interpolation[datei] = True # Fix at 1 if total length is zero, to avoid nans in weights # TODO: Make something cleaner for data not characterized by intervals if np.any(total_length == 0): warning("Setting zero lengths to one in temporal interpolation to " "avoid nans in weights. Happens when interpolating to " "snapshots data contrary to interval data.") total_length.loc[total_length == 0] = 1 # Normalize weights by total length for ddi in in_dates: if ddi not in tmp_indexes: continue if "weights" in tmp_indexes.get(ddi, {}): tmp_indexes[ddi]["weights"] /= total_length.iloc[ np.where(ddi_masks[ddi])[0][tmp_indexes[ddi].index] ].values if len(tmp_indexes[ddi]) == 0: del tmp_indexes[ddi] interpol_indexes[datei] = tmp_indexes # Raise exception if any output is not covered if not np.all(covered_output_dates): raise CifError( f"Some output dates are not covered by the inputs for {trid_out}.\n" "Please check your Yml.\n" f"Missing dates are: " f"{out_dates_start[~covered_output_dates]}\n" "Whereas input dates are: \n" + ("\n".join( [f"- {ddi}\n" + "\n".join([f" - {di}" for di in in_dates[ddi]]) for ddi in in_dates ] )) ) # If interpolation is required for no date, no need to force reading in_dict["force_loadin"] = force_loadin mapper["do_interpolation"] = do_interpolation mapper["reorder_periods"] = reorder_periods mapper["interpol_indexes"] = interpol_indexes