From b1429a78101347362ad69617296a5c6ba58dc3d7 Mon Sep 17 00:00:00 2001 From: vanessa-tamara Date: Thu, 23 Mar 2023 13:11:15 +0100 Subject: [PATCH 1/2] forecast tool --- tools/forecast/config.py | 90 ++ tools/forecast/forecast.py | 144 +++ tools/forecast/forecast.xml | 478 +++++++++ tools/forecast/models.py | 191 ++++ tools/forecast/utils.py | 1955 +++++++++++++++++++++++++++++++++++ 5 files changed, 2858 insertions(+) create mode 100644 tools/forecast/config.py create mode 100644 tools/forecast/forecast.py create mode 100644 tools/forecast/forecast.xml create mode 100644 tools/forecast/models.py create mode 100644 tools/forecast/utils.py diff --git a/tools/forecast/config.py b/tools/forecast/config.py new file mode 100644 index 0000000..346103e --- /dev/null +++ b/tools/forecast/config.py @@ -0,0 +1,90 @@ +""" +Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted +to fit the galaxy interface. +""" + +import os +import pandas as pd + +''' +Defines globals used throughout the codebase. +''' + +############################################################################### +### Folder structure naming system +############################################################################### + +data_folder = 'data' +obs_data_folder = os.path.join(data_folder, 'obs') +cmip6_data_folder = os.path.join(data_folder, 'cmip6') +mask_data_folder = os.path.join(data_folder, 'masks') +forecast_data_folder = os.path.join(data_folder, 'forecasts') +network_dataset_folder = os.path.join(data_folder, 'network_datasets') + +dataloader_config_folder = 'dataloader_configs' + +networks_folder = 'trained_networks' + +results_folder = 'results' +forecast_results_folder = os.path.join(results_folder, 'forecast_results') +permute_and_predict_results_folder = os.path.join(results_folder, 'permute_and_predict_results') +uncertainty_results_folder = os.path.join(results_folder, 'uncertainty_results') + +figure_folder = 'figures' + +video_folder = 'videos' + +active_grid_cell_file_format = 'active_grid_cell_mask_{}.npy' +land_mask_filename = 'land_mask.npy' +region_mask_filename = 'region_mask.npy' + +############################################################################### +### Polar hole/missing months +############################################################################### + +# Pre-defined polar hole radii (in number of 25km x 25km grid cells) +# The polar hole radii were determined from Sections 2.1, 2.2, and 2.3 of +# http://osisaf.met.no/docs/osisaf_cdop3_ss2_pum_sea-ice-conc-climate-data-record_v2p0.pdf +polarhole1_radius = 28 +polarhole2_radius = 11 +polarhole3_radius = 3 + +# Whether or not to mask out the 3rd polar hole mask from +# Nov 2005 to Dec 2015 with a radius of only 3 grid cells. Including it creates +# some complications when analysing performance on a validation set that +# overlaps with the 3rd polar hole period. +use_polarhole3 = False + +polarhole1_fname = 'polarhole1_mask.npy' +polarhole2_fname = 'polarhole2_mask.npy' +polarhole3_fname = 'polarhole3_mask.npy' + +# Final month that each of the polar holes apply +# NOTE: 1st of the month chosen arbitrarily throughout as always working wit +# monthly averages +polarhole1_final_date = pd.Timestamp('1987-06-01') # 1987 June +polarhole2_final_date = pd.Timestamp('2005-10-01') # 2005 Oct +polarhole3_final_date = pd.Timestamp('2015-12-01') # 2015 Dec + +missing_dates = [pd.Timestamp('1986-4-1'), pd.Timestamp('1986-5-1'), + pd.Timestamp('1986-6-1'), pd.Timestamp('1987-12-1')] + +############################################################################### +### Weights and biases config (https://docs.wandb.ai/guides/track/advanced/environment-variables) +############################################################################### + +# Get API key from https://wandb.ai/authorize +WANDB_API_KEY = 'YOUR-KEY-HERE' +# Absolute path to store wandb generated files (folder must exist) +# Note: user must have write access +WANDB_DIR = '/path/to/wandb/dir' +# Absolute path to wandb config dir ( +WANDB_CONFIG_DIR = '/path/to/wandb/config/dir' +WANDB_CACHE_DIR = '/path/to/wandb/cache/dir' + +############################################################################### +### ECMWF details +############################################################################### + +ECMWF_API_KEY = 'YOUR-KEY-HERE' +ECMWF_API_EMAIL = 'YOUR-KEY-HERE' diff --git a/tools/forecast/forecast.py b/tools/forecast/forecast.py new file mode 100644 index 0000000..f4fce85 --- /dev/null +++ b/tools/forecast/forecast.py @@ -0,0 +1,144 @@ +""" +Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted +to fit the galaxy interface. +""" +import os +import sys +import argparse +sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) +from utils import IceNetDataLoader +import pandas as pd +import xarray as xr +import numpy as np +from tqdm import tqdm +import re +from tensorflow.keras.models import load_model +import time +import config + + +parser = argparse.ArgumentParser() +parser.add_argument("--config", type=str, help="config file") +parser.add_argument("--models", type=str, help="network models" ) +parser.add_argument("--siconca", type=str, help="siconca netcdf file" ) +args = parser.parse_args() + +# Load dataloader +dataloader_ID = '2021_09_03_1300_icenet_demo' +dataloader_config_fpath = args.config + +# Data loader +#print("\nSetting up the data loader with config file: {}\n\n".format(dataloader_ID)) +dataloader = IceNetDataLoader(dataloader_config_fpath) +print('\n\nDone.\n') + +#load networks +network_regex = re.compile('^network_tempscaled_([0-9]*).h5$') + +network_fpaths = args.models.split(",") + +#ensemble_seeds = [36, 42, 53] +ensemble_seeds = [network_regex.match(f)[1] for f in + ["network_tempscaled_36.h5", "network_tempscaled_42.h5", "network_tempscaled_53.h5"] if network_regex.match(f)] +print(ensemble_seeds) +networks = [] +for network_fpath in network_fpaths: + print('Loading model from {}... '.format(network_fpath), end='', flush=True) + networks.append(load_model(network_fpath, compile=False)) + print('Done.') + +model = 'IceNet' + +forecast_start = pd.Timestamp('2020-01-01') +forecast_end = pd.Timestamp('2020-12-01') + +n_forecast_months = dataloader.config['n_forecast_months'] + + +forecast_folder = os.path.join(config.forecast_data_folder, 'icenet', dataloader_ID, model) + +if not os.path.exists(forecast_folder): + os.makedirs(forecast_folder) + +#load ground truth +print('Loading ground truth SIC... ', end='', flush=True) +true_sic_fpath = args.siconca +true_sic_da = xr.open_dataarray(true_sic_fpath) +print('Done.') + + +#set up forecast folder + +# define list of lead times +leadtimes = np.arange(1, n_forecast_months+1) + +# add ensemble to the list of models +ensemble_seeds_and_mean = ensemble_seeds.copy() +ensemble_seeds_and_mean.append('ensemble') + +all_target_dates = pd.date_range( + start=forecast_start, + end=forecast_end, + freq='MS' +) + +all_start_dates = pd.date_range( + start=forecast_start - pd.DateOffset(months=n_forecast_months-1), + end=forecast_end, + freq='MS' +) + +shape = (len(all_target_dates), + *dataloader.config['raw_data_shape'], + n_forecast_months) + +coords = { + 'time': all_target_dates, # To be sliced to target dates + 'yc': true_sic_da.coords['yc'], + 'xc': true_sic_da.coords['xc'], + 'lon': true_sic_da.isel(time=0).coords['lon'], + 'lat': true_sic_da.isel(time=0).coords['lat'], + 'leadtime': leadtimes, + 'seed': ensemble_seeds_and_mean, + 'ice_class': ['no_ice', 'marginal_ice', 'full_ice'] +} + +# Probabilistic SIC class forecasts +dims = ('seed', 'time', 'yc', 'xc', 'leadtime', 'ice_class') +shape = (len(ensemble_seeds_and_mean), *shape, 3) +print(dims) +print(shape) +model_forecast = xr.DataArray( + data=np.zeros(shape, dtype=np.float32), + coords=coords, + dims=dims +) + +for start_date in tqdm(all_start_dates): + + # Target forecast dates for the forecast beginning at this `start_date` + target_dates = pd.date_range( + start=start_date, + end=start_date + pd.DateOffset(months=n_forecast_months-1), + freq='MS' + ) + + X, y, sample_weights = dataloader.data_generation([start_date]) + mask = sample_weights > 0 + pred = np.array([network.predict(X)[0] for network in networks]) + pred *= mask # mask outside active grid cell region to zero + # concat ensemble mean to the set of network predictions + ensemble_mean_pred = pred.mean(axis=0, keepdims=True) + pred = np.concatenate([pred, ensemble_mean_pred], axis=0) + + for i, (target_date, leadtime) in enumerate(zip(target_dates, leadtimes)): + if target_date in all_target_dates: + model_forecast.\ + loc[:, target_date, :, :, leadtime] = pred[..., i] + +print('Saving forecast NetCDF for {}... '.format(model), end='', flush=True) + +forecast_fpath = os.path.join(forecast_folder, f'{model.lower()}_forecasts.nc'.format(model.lower())) +model_forecast.to_netcdf(forecast_fpath) #export file as Net + +print('Done.') diff --git a/tools/forecast/forecast.xml b/tools/forecast/forecast.xml new file mode 100644 index 0000000..7deb605 --- /dev/null +++ b/tools/forecast/forecast.xml @@ -0,0 +1,478 @@ + + for regridding and normalizing icenet data + + python + xarray + numpy + pandas + scipy + iris + netcdf4 + imageio + matplotlib + tqdm + cdsapi + tensorflow + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @misc{https://doi.org/10.5281/zenodo.5176573, + doi = {10.5281/ZENODO.5176573}, + url = {https://zenodo.org/record/5176573}, + author = {Andersson, Tom R.}, + title = {Code associated with the paper: 'Seasonal Arctic sea ice forecasting with probabilistic deep learning'}, + publisher = {Zenodo}, + year = {2021}, + copyright = {Open Access} + } + + + diff --git a/tools/forecast/models.py b/tools/forecast/models.py new file mode 100644 index 0000000..04515fe --- /dev/null +++ b/tools/forecast/models.py @@ -0,0 +1,191 @@ +""" +Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted +to fit the galaxy interface. +""" +import sys +import os +sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) # if using jupyter kernel +import config +import numpy as np +import pandas as pd +import xarray as xr +import tensorflow as tf +from tensorflow.keras.models import Model +from tensorflow.keras.layers import Conv2D, BatchNormalization, UpSampling2D, \ + concatenate, MaxPooling2D, Input +from tensorflow.keras.optimizers import Adam + +''' +Defines the Python-based sea ice forecasting models, such as the IceNet architecture +and the linear trend extrapolation model. +''' + +### Custom layers: +# -------------------------------------------------------------------- + + +@tf.keras.utils.register_keras_serializable() +class TemperatureScale(tf.keras.layers.Layer): + ''' + Implements the temperature scaling layer for probability calibration, + as introduced in Guo 2017 (http://proceedings.mlr.press/v70/guo17a.html). + ''' + def __init__(self, **kwargs): + super(TemperatureScale, self).__init__() + self.temp = tf.Variable(initial_value=1.0, trainable=False, + dtype=tf.float32, name='temp') + + def call(self, inputs): + ''' Divide the input logits by the T value. ''' + return tf.divide(inputs, self.temp) + + def get_config(self): + ''' For saving and loading networks with this custom layer. ''' + return {'temp': self.temp.numpy()} + + +### Network architectures: +# -------------------------------------------------------------------- + +def unet_batchnorm(input_shape, loss, weighted_metrics, learning_rate=1e-4, filter_size=3, + n_filters_factor=1, n_forecast_months=1, use_temp_scaling=False, + n_output_classes=3, + **kwargs): + inputs = Input(shape=input_shape) + + conv1 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(inputs) + conv1 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv1) + bn1 = BatchNormalization(axis=-1)(conv1) + pool1 = MaxPooling2D(pool_size=(2, 2))(bn1) + + conv2 = Conv2D(np.int(128*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool1) + conv2 = Conv2D(np.int(128*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv2) + bn2 = BatchNormalization(axis=-1)(conv2) + pool2 = MaxPooling2D(pool_size=(2, 2))(bn2) + + conv3 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool2) + conv3 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv3) + bn3 = BatchNormalization(axis=-1)(conv3) + pool3 = MaxPooling2D(pool_size=(2, 2))(bn3) + + conv4 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool3) + conv4 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv4) + bn4 = BatchNormalization(axis=-1)(conv4) + pool4 = MaxPooling2D(pool_size=(2, 2))(bn4) + + conv5 = Conv2D(np.int(512*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool4) + conv5 = Conv2D(np.int(512*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv5) + bn5 = BatchNormalization(axis=-1)(conv5) + + up6 = Conv2D(np.int(256*n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2,2), interpolation='nearest')(bn5)) + merge6 = concatenate([bn4, up6], axis=3) + conv6 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge6) + conv6 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv6) + bn6 = BatchNormalization(axis=-1)(conv6) + + up7 = Conv2D(np.int(256*n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2,2), interpolation='nearest')(bn6)) + merge7 = concatenate([bn3,up7], axis=3) + conv7 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge7) + conv7 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv7) + bn7 = BatchNormalization(axis=-1)(conv7) + + up8 = Conv2D(np.int(128*n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2,2), interpolation='nearest')(bn7)) + merge8 = concatenate([bn2,up8], axis=3) + conv8 = Conv2D(np.int(128*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge8) + conv8 = Conv2D(np.int(128*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv8) + bn8 = BatchNormalization(axis=-1)(conv8) + + up9 = Conv2D(np.int(64*n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2,2), interpolation='nearest')(bn8)) + merge9 = concatenate([conv1,up9], axis=3) + conv9 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge9) + conv9 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv9) + conv9 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv9) + + final_layer_logits = [(Conv2D(n_output_classes, 1, activation='linear')(conv9)) for i in range(n_forecast_months)] + final_layer_logits = tf.stack(final_layer_logits, axis=-1) + + if use_temp_scaling: + # Temperature scaling of the logits + final_layer_logits_scaled = TemperatureScale()(final_layer_logits) + final_layer = tf.nn.softmax(final_layer_logits_scaled, axis=-2) + else: + final_layer = tf.nn.softmax(final_layer_logits, axis=-2) + + model = Model(inputs, final_layer) + + model.compile(optimizer=Adam(lr=learning_rate), loss=loss, weighted_metrics=weighted_metrics) + + return model + + +### Benchmark models: +# -------------------------------------------------------------------- + + +def linear_trend_forecast(forecast_month, n_linear_years='all', da=None, dataset='obs'): + ''' + Returns a simple sea ice forecast based on a gridcell-wise linear extrapolation. + + Parameters: + forecast_month (datetime.datetime): The month to forecast + + n_linear_years (int or str): Number of past years to use for linear trend + extrapolation. + + da (xr.DataArray): xarray data array to use instead of observational + data (used for setting up CMIP6 pre-training linear trend inputs in IceUNetDataPreProcessor). + + dataset (str): 'obs' or 'cmip6'. If 'obs', missing observational SIC months + will be skipped + + Returns: + output_map (np.ndarray): The output SIC map predicted + by fitting a least squares linear trend to the past n_linear_years + for the month being predicted. + + sie (np.float): The predicted sea ice extend (SIE). + ''' + + if da is None: + with xr.open_dataset('data/obs/siconca_EASE.nc') as ds: + da = next(iter(ds.data_vars.values())) + + valid_dates = [pd.Timestamp(date) for date in da.time.values] + + input_dates = [forecast_month - pd.DateOffset(years=1+lag) for lag in range(n_linear_years)] + input_dates + + # Do not use missing months in the linear trend projection + input_dates = [date for date in input_dates if date not in config.missing_dates] + + # Chop off input date from before data start + input_dates = [date for date in input_dates if date in valid_dates] + + input_dates = sorted(input_dates) + + # The actual number of past years used + actual_n_linear_years = len(input_dates) + + da = da.sel(time=input_dates) + + input_maps = np.array(da.data) + + x = np.arange(actual_n_linear_years) + y = input_maps.reshape(actual_n_linear_years, -1) + + # Fit the least squares linear coefficients + r = np.linalg.lstsq(np.c_[x, np.ones_like(x)], y, rcond=None)[0] + + # y = mx + c + output_map = np.matmul(np.array([actual_n_linear_years, 1]), r).reshape(432, 432) + + land_mask_path = os.path.join(config.mask_data_folder, config.land_mask_filename) + land_mask = np.load(land_mask_path) + output_map[land_mask] = 0. + + output_map[output_map < 0] = 0. + output_map[output_map > 1] = 1. + + sie = np.sum(output_map > 0.15) * 25**2 + + return output_map, sie diff --git a/tools/forecast/utils.py b/tools/forecast/utils.py new file mode 100644 index 0000000..626c60d --- /dev/null +++ b/tools/forecast/utils.py @@ -0,0 +1,1955 @@ +""" +Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted +to fit the galaxy interface. +""" +import os +import sys +import numpy as np +import tensorflow as tf +sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) # if using jupyter kernel +from models import linear_trend_forecast +import config +import itertools +import requests +import json +import time +import re +import xarray as xr +import pandas as pd +from dateutil.relativedelta import relativedelta +import iris +import cartopy.crs as ccrs +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable +import imageio +from tqdm import tqdm + + +############################################################################### +############### DATA PROCESSING & LOADING +############################################################################### + + +class IceNetDataPreProcessor(object): + """ + Normalises IceNet input data and saves the normalised monthly averages + as .npy files. If preprocessing climate model data for transfer learning, + the observational normalisation is repeated for the climate model data in order + to preserve the mapping from raw values to normalised values. + + Data is stored in the following form with observations separated from climate + model transfer learning data: + - data/network_datasets//obs/tas/2006_04.npy + - data/network_datasets//transfer/MRI-ESM2-0/r1i1p1f1/tas/2056_09.npy + + Normalisation parameters computed over the observational training data are + stored in a JSON file at data/network_datasets//norm_params.json + so that they are only computed once. Similarly, monthly climatology fields + used for computing anomaly fields are saved next to the raw NetCDF files so that + climatologies are only computed once for each variable. + """ + + def __init__(self, dataloader_config_fpath, preproc_vars, + n_linear_years, minmax, verbose_level, + preproc_obs_data=True, + preproc_transfer_data=False, cmip_transfer_data={}): + """ + Parameters: + + dataloader_config_fpath (str): Path to the data loader configuration + settings JSON file, defining IceNet's input-output data configuration. + This also defines the dataset name, used as the folder name to + store the preprocessed data within data/network_datasets/. + + preproc_vars (dict): Which variables to preprocess. Example: + + preproc_vars = { + 'siconca': {'anom': True, 'abs': True}, + 'tas': {'anom': True, 'abs': False}, + 'tos': {'anom': True, 'abs': False}, + 'rsds': {'anom': True, 'abs': False}, + 'rsus': {'anom': True, 'abs': False}, + 'psl': {'anom': False, 'abs': True}, + 'zg500': {'anom': False, 'abs': True}, + 'zg250': {'anom': False, 'abs': True}, + 'ua10': {'anom': False, 'abs': True}, + 'uas': {'anom': False, 'abs': True}, + 'vas': {'anom': False, 'abs': True}, + 'sfcWind': {'anom': False, 'abs': True}, + 'land': {'metadata': True, 'include': True}, + 'circmonth': {'metadata': True, 'include': True} + } + + n_linear_years (int): Number of past years to used in the linear trend + projections. + + minmax (bool): Whether to use min-max normalisation to (-1, 1) or normalise + the mean and standard deviation to 0 and 1. + + verbose_level (int): Controls how much to print. 0: Print nothing. + 1: Print key set-up stages. 2: Print debugging info. + + preproc_obs_data (bool): Whether to preprocess observational data + (default True). + + preproc_transfer_data (bool): Whether to also preprocess CMIP6 data for each variable + (default False). + + cmip_transfer_data (dict): Which CMIP6 models & model runs to + preprocess for transfer learning. Example: + + cmip_transfer_data = { + 'MRI-ESM2-0': ('r1i1p1f1', 'r2i1p1f1', 'r3i1p1f1', + 'r4i1p1f1', 'r5i1p1f1') + } + + """ + + with open(dataloader_config_fpath, 'r') as readfile: + self.config = json.load(readfile) + + self.preproc_vars = preproc_vars + self.n_linear_years = n_linear_years + self.minmax = minmax + self.verbose_level = verbose_level + self.preproc_obs_data = preproc_obs_data + self.preproc_transfer_data = preproc_transfer_data + self.cmip_transfer_data = cmip_transfer_data + + self.load_or_instantiate_norm_params_dict() + self.set_obs_train_dates() + self.set_up_folder_hierarchy() + + if self.verbose_level >= 1: + print("Loading and normalising the raw input maps.\n") + tic = time.time() + + self.preproc_and_save_icenet_data() + + if self.verbose_level >= 1: + print("\nPreprocessing completed in {:.0f}s.\n".format(time.time() - tic)) + + def load_or_instantiate_norm_params_dict(self): + + # Path to JSON file storing normalisation parameters for each variable + self.norm_params_fpath = os.path.join( + config.network_dataset_folder, self.config['dataset_name'], 'norm_params.json') + + if not os.path.exists(self.norm_params_fpath): + self.norm_params = {} + + else: + with open(self.norm_params_fpath, 'r') as readfile: + self.norm_params = json.load(readfile) + + def set_obs_train_dates(self): + + forecast_start_date_ends = self.config['sample_IDs']['obs_train_dates'] + + if forecast_start_date_ends is not None: + + # Convert to Pandas Timestamps + forecast_start_date_ends = [ + pd.Timestamp(date).to_pydatetime() for date in forecast_start_date_ends + ] + + self.obs_train_dates = list(pd.date_range( + forecast_start_date_ends[0], + forecast_start_date_ends[1], + freq='MS', + closed='right', + )) + + def set_up_folder_hierarchy(self): + + """ + Initialise the folders to store the datasets. + """ + + if self.verbose_level >= 1: + print('Setting up the folder hierarchy for {}... '.format(self.config['dataset_name']), + end='', flush=True) + + # Parent folder for this dataset + self.dataset_path = os.path.join(config.data_folder, 'network_datasets', self.config['dataset_name']) + + # Dictionary data structure to store folder paths + self.paths = {} + + # Set up the folder hierarchy + self.paths['obs'] = {} + + for varname, vardict in self.preproc_vars.items(): + + if 'metadata' not in vardict.keys(): + self.paths['obs'][varname] = {} + + for data_format in vardict.keys(): + + if vardict[data_format] is True: + path = os.path.join(self.dataset_path, 'obs', + varname, data_format) + + self.paths['obs'][varname][data_format] = path + + if not os.path.exists(path): + os.makedirs(path) + + self.paths['transfer'] = {} + + for model_name, member_ids in self.cmip_transfer_data.items(): + self.paths['transfer'][model_name] = {} + for member_id in member_ids: + self.paths['transfer'][model_name][member_id] = {} + + for varname, vardict in self.preproc_vars.items(): + + if 'metadata' not in vardict.keys(): + self.paths['transfer'][model_name][member_id][varname] = {} + + for data_format in vardict.keys(): + + if vardict[data_format] is True: + path = os.path.join(self.dataset_path, 'transfer', + model_name, member_id, + varname, data_format) + + self.paths['transfer'][model_name][member_id][varname][data_format] = path + + if not os.path.exists(path): + os.makedirs(path) + + for varname, vardict in self.preproc_vars.items(): + if 'metadata' in vardict.keys(): + + if vardict['include'] is True: + path = os.path.join(self.dataset_path, 'meta') + + self.paths['meta'] = path + + if not os.path.exists(path): + os.makedirs(path) + + if self.verbose_level >= 1: + print('Done.') + + @staticmethod + def standardise_cmip6_time_coord(da): + + """ + Convert the cmip6 xarray time dimension to use day=1, hour=0 convention + used in the rest of the project. + """ + + standardised_dates = [] + for datetime64 in da.time.values: + date = pd.Timestamp(datetime64, unit='s') + date = date.replace(day=1, hour=0) + standardised_dates.append(date) + da = da.assign_coords({'time': standardised_dates}) + + return da + + @staticmethod + def mean_and_std(list, verbose_level=2): + + # Must use float64s to be JSON serialisable + mean = np.nanmean(list, dtype=np.float64) + std = np.nanstd(list, dtype=np.float64) + + return mean, std + + def normalise_array_using_all_training_months(self, da, minmax=False, + mean=None, std=None, + min=None, max=None): + + """ + Using the *training* months only, compute the mean and + standard deviation of the input raw satellite DataArray (`da`) + and return a normalised version. If minmax=True, + instead normalise to lie between min and max of the elements of `array`. + + If min, max, mean, or std are given values other than None, + those values are used rather than being computed from the training months. + + Returns: + new_da (xarray.DataArray): Normalised array. + + mean, std (float): Pre-computed mean and standard deviation for the + normalisation. + + min, max (float): Pre-computed min and max for the normalisation. + """ + + if (min is not None and max is not None) or (mean is not None and std is not None): + # Function has been passed precomputed normalisation parameters + pass + else: + # Function will be computing new normalisation parameters + training_samples = da.sel(time=self.obs_train_dates).data + training_samples = training_samples.ravel() + + if not minmax: + if mean is None and std is None: + # Compute mean and std + mean, std = IceNetDataPreProcessor.mean_and_std( + training_samples, self.verbose_level) + elif mean is not None and std is None: + # Compute std only + _, std = IceNetDataPreProcessor.mean_and_std( + training_samples, self.verbose_level) + elif mean is None and std is not None: + # Compute mean only + mean, _ = IceNetDataPreProcessor.mean_and_std( + training_samples, self.verbose_level) + + new_da = (da - mean) / std + + elif minmax: + if min is None: + # Compute min + min = np.nanmin(training_samples).astype(np.float64) + if max is None: + # Compute max + max = np.nanmax(training_samples).astype(np.float64) + + new_da = (da - min) / (max - min) + + if minmax: + return new_da, min, max + elif not minmax: + return new_da, mean, std + + def save_xarray_in_monthly_averages(self, da, dataset_type, varname, data_format, + model_name=None, member_id=None): + + """ + Saves an xarray DataArray as monthly averaged .npy files using the + self.paths data structure. + + Parameters: + da (xarray.DataArray): The DataArray to save. + + dataset_type (str): Either 'obs' or 'transfer' (for CMIP6 data) - the type + of dataset being saved. + + varname (str): Variable name being saved. + + data_format (str): Either 'abs' or 'anom' - the format of the data + being saved. + """ + + if self.verbose_level >= 2: + print('Saving {} {} monthly averages... '.format(data_format, varname), end='', flush=True) + + # Allow for datasets without a time dimension (a single time slice) + dates = da.time.values + if hasattr(dates, '__iter__'): + pass # Dataset has 'time' dimension; dates already iterable + else: + dates = [dates] # Convert single time value to iterable + da = da.expand_dims({'time': dates}) + + for date in dates: + slice = da.sel(time=date).data + date = pd.Timestamp(date) + year_str = '{:04d}'.format(date.year) + month_str = '{:02d}'.format(date.month) + fname = '{}_{}.npy'.format(year_str, month_str) + + if dataset_type == 'obs': + np.save(os.path.join(self.paths[dataset_type][varname][data_format], fname), + slice) + + if dataset_type == 'transfer': + np.save(os.path.join(self.paths[dataset_type][model_name][member_id][varname][data_format], fname), + slice) + + if self.verbose_level >= 2: + print('Done.') + + def build_linear_trend_da(self, input_da, dataset): + + """ + Construct a DataArray `linea_trend_da` containing the linear trend SIC + forecasts based on the input DataArray `input_da`. + + `linear_trend_da` will be saved in monthly averages using + the `save_xarray_in_monthly_averages` method. + + Parameters: + `input_da` (xarray.DataArray): Input DataArray to produce linear SIC + forecasts for. + + `dataset` (str): 'obs' or 'cmip6' (dictates whether to skip missing + observational months in the linear trend extrapolation) + + Returns: + `linear_trend_da` (xarray.DataArray): DataArray whose time slices + correspond to the linear trend SIC projection for that month. + """ + + linear_trend_da = input_da.copy(data=np.zeros(input_da.shape, dtype=np.float32)) + + # No prediction possible for the first year of data + forecast_dates = input_da.time.values[12:] + + # Convert from datetime64 to pd.Timestamp + forecast_dates = [pd.Timestamp(date) for date in forecast_dates] + + # Add on the future year + last_year = forecast_dates[-12:] + forecast_dates.extend([date + pd.DateOffset(years=1) for date in last_year]) + + linear_trend_da = linear_trend_da.assign_coords({'time': forecast_dates}) + + for forecast_date in forecast_dates: + linear_trend_da.loc[dict(time=forecast_date)] = \ + linear_trend_forecast(forecast_date, self.n_linear_years, da=input_da, dataset=dataset)[0] + + return linear_trend_da + + def check_if_params_precomputed(self, varname, data_format): + ''' Searches self.norm_params for normalisation parameters + for a given variable name and data format. ''' + + if varname == 'siconca': + # No normalisation for SIC + return True + + # Grab existing parameters if stored in norm_params JSON file + precomputed_params_exists = False + if varname in self.norm_params.keys(): + if data_format in self.norm_params[varname].keys(): + params = self.norm_params[varname][data_format] + if self.minmax: + if 'min' in params.keys() and 'max' in params.keys(): + precomputed_params_exists = True + elif not self.minmax: + if 'mean' in params.keys() and 'std' in params.keys(): + precomputed_params_exists = True + + return precomputed_params_exists + + def save_variable(self, varname, data_format, dates=None): + + """ + Save a normalised 3-dimensional satellite/reanalysis dataset as monthly + averages (either the absolute values or the monthly anomalies + computed with xarray). + + This method assumes there is only one variable stored in the NetCDF files. + + Parameters: + varname (str): Name of the variable to load & save + + data_format (str): 'abs' for absolute values, or 'anom' to compute the + anomalies, or 'linear_trend' for SIC linear trend projections. + + dates (list of dates): Months to use to compute the monthly + climatologies (defaults to the months used for training). + """ + + if data_format == 'anom': + if dates is None: + dates = self.obs_train_dates + + ######################################################################## + ################# Observational variable + ######################################################################## + + if self.preproc_obs_data: + if self.verbose_level >= 2: + print("Preprocessing {} data for {}... ".format(data_format, varname), end='', flush=True) + tic = time.time() + + fpath = os.path.join(config.obs_data_folder, '{}_EASE.nc'.format(varname)) + with xr.open_dataset(fpath) as ds: + da = next(iter(ds.data_vars.values())) + + if data_format == 'anom': + + # Check if climatology already computed + train_start = self.obs_train_dates[0].strftime('%Y') + train_end = self.obs_train_dates[-1].strftime('%Y') + + climatology_fpath = os.path.join( + config.obs_data_folder, + '{}_climatology_{}_{}.nc'.format(varname, train_start, train_end)) + + if os.path.exists(climatology_fpath): + with xr.open_dataset(climatology_fpath) as ds: + climatology = next(iter(ds.data_vars.values())) + else: + climatology = da.sel(time=dates). \ + groupby("time.month", restore_coord_dims=True).mean("time") + climatology.to_netcdf(climatology_fpath) + + da = da.groupby("time.month", restore_coord_dims=True) - climatology + + elif data_format == 'linear_trend': + da = self.build_linear_trend_da(da, dataset='obs') + + # Realise the array + da.data = np.asarray(da.data, dtype=np.float32) + + # Normalise the array + if varname == 'siconca': + # Don't normalise SIC - already betw 0 and 1 + mean, std = None, None + min, max = None, None + + elif varname != 'siconca': + precomputed_params_exists = self.check_if_params_precomputed(varname, data_format) + + if precomputed_params_exists: + if self.minmax: + min = self.norm_params[varname][data_format]['min'] + max = self.norm_params[varname][data_format]['max'] + if self.verbose_level >= 2: + print("Using precomputed min/max: {}/{}... ".format(min, max), + end='', flush=True) + elif not self.minmax: + mean = self.norm_params[varname][data_format]['mean'] + std = self.norm_params[varname][data_format]['std'] + if self.verbose_level >= 2: + print("Using precomputed mean/std: {}/{}... ".format(mean, std), + end='', flush=True) + elif not precomputed_params_exists: + mean, std = None, None + min, max = None, None + self.norm_params[varname] = {} + self.norm_params[varname][data_format] = {} + + if self.minmax: + da, min, max = self.normalise_array_using_all_training_months( + da, self.minmax, min=min, max=max) + if not precomputed_params_exists: + if self.verbose_level >= 2: + print("Newly computed min/max: {}/{}... ".format(min, max), + end='', flush=True) + self.norm_params[varname][data_format]['min'] = min + self.norm_params[varname][data_format]['max'] = max + elif not self.minmax: + da, mean, std = self.normalise_array_using_all_training_months( + da, self.minmax, mean=mean, std=std) + if not precomputed_params_exists: + if self.verbose_level >= 2: + print("Newly computed mean/std: {}/{}... ".format(mean, std), + end='', flush=True) + self.norm_params[varname][data_format]['mean'] = mean + self.norm_params[varname][data_format]['std'] = std + + da.data[np.isnan(da.data)] = 0. # Convert any NaNs to zeros + + self.save_xarray_in_monthly_averages(da, 'obs', varname, data_format) + + if self.verbose_level >= 2: + print("Done in {:.0f}s.\n".format(time.time() - tic)) + + ######################################################################## + ################# Transfer variable + ######################################################################## + + if self.preproc_transfer_data: + if self.verbose_level >= 2: + print("Preprocessing CMIP6 {} data for {}... ".format(data_format, varname), end='', flush=True) + tic = time.time() + + if not self.check_if_params_precomputed(varname, data_format): + raise ValueError('Normalisation parameters must be computed ' + 'from observational data before preprocessing ' + 'CMIP6 data.') + + elif varname != 'siconca' and self.minmax: + min = self.norm_params[varname][data_format]['min'] + max = self.norm_params[varname][data_format]['max'] + if self.verbose_level >= 2: + print("Using precomputed min/max: {}/{}... ".format(min, max), + end='', flush=True) + + elif varname != 'siconca' and not self.minmax: + mean = self.norm_params[varname][data_format]['mean'] + std = self.norm_params[varname][data_format]['std'] + if self.verbose_level >= 2: + print("Using precomputed mean/std: {}/{}... ".format(mean, std), + end='', flush=True) + + for model_name, member_ids in self.cmip_transfer_data.items(): + print('{}: '.format(model_name), end='', flush=True) + + for member_id in member_ids: + print('{}, '.format(member_id), end='', flush=True) + + fname = '{}_EASE_cmpr.nc'.format(varname) + fpath = os.path.join(config.cmip6_data_folder, model_name, member_id, fname) + + with xr.open_dataset(fpath) as ds: + da = next(iter(ds.data_vars.values())) + + # Convert to my month convention of day=1 and time=00:00 + da = IceNetDataPreProcessor.standardise_cmip6_time_coord(da) + + # Realise the array + da.data = np.asarray(da.data, dtype=np.float32) + + if data_format == 'anom': + + climatology = da.sel(time=dates). \ + groupby("time.month", restore_coord_dims=True).mean("time") + da = da.groupby("time.month", restore_coord_dims=True) - climatology + + elif data_format == 'linear_trend': + da = self.build_linear_trend_da(da, dataset='cmip6') + + # Normalise the array + if varname != 'siconca': + if self.minmax: + da, _, _ = self.normalise_array_using_all_training_months( + da, self.minmax, min=min, max=max) + elif not self.minmax: + da, _, _ = self.normalise_array_using_all_training_months( + da, self.minmax, mean=mean, std=std) + + self.save_xarray_in_monthly_averages(da, 'transfer', varname, data_format, + model_name, member_id) + + if self.verbose_level >= 2: + print("Done in {:.0f}s.\n".format(time.time() - tic)) + + def preproc_and_save_icenet_data(self): + + ''' + Loop through each variable, preprocessing and saving. + ''' + + for varname, vardict in self.preproc_vars.items(): + + if 'metadata' not in vardict.keys(): + + for data_format in vardict.keys(): + + if vardict[data_format] is True: + + self.save_variable(varname, data_format) + + elif 'metadata' in vardict.keys(): + + if vardict['include']: + if varname == 'land': + if self.verbose_level >= 2: + print("Setting up the land map: ", end='', flush=True) + + land_mask = np.load(os.path.join(config.mask_data_folder, config.land_mask_filename)) + land_map = np.ones(self.config['raw_data_shape'], np.float32) + land_map[~land_mask] = -1. + + np.save(os.path.join(self.paths['meta'], 'land.npy'), land_map) + + print('\n') + + elif varname == 'circmonth': + if self.verbose_level >= 2: + print("Computing circular month values... ", end='', flush=True) + tic = time.time() + + for month in np.arange(1, 13): + cos_month = np.cos(2 * np.pi * month / 12, dtype='float32') + sin_month = np.sin(2 * np.pi * month / 12, dtype='float32') + + np.save(os.path.join(self.paths['meta'], 'cos_month_{:02d}.npy'.format(month)), cos_month) + np.save(os.path.join(self.paths['meta'], 'sin_month_{:02d}.npy'.format(month)), sin_month) + + if self.verbose_level >= 2: + print("Done in {:.0f}s.\n".format(time.time() - tic)) + + with open(self.norm_params_fpath, 'w') as outfile: + json.dump(self.norm_params, outfile) + + +class IceNetDataLoader(tf.keras.utils.Sequence): + """ + Custom data loader class for generating batches of input-output tensors for + training IceNet. Inherits from keras.utils.Sequence, which ensures each the + network trains once on each sample per epoch. Must implement a __len__ + method that returns the number of batches and a __getitem__ method that + returns a batch of data. The on_epoch_end method is called after each + epoch. + See: https://www.tensorflow.org/api_docs/python/tf/keras/utils/Sequence + + """ + + def __init__(self, dataloader_config_fpath, seed=None): + + ''' + Params: + dataloader_config_fpath (str): Path to the data loader configuration + settings JSON file, defining IceNet's input-output data configuration. + + seed (int): Random seed used for shuffling the training samples before + each epoch. + ''' + + with open(dataloader_config_fpath, 'r') as readfile: + self.config = json.load(readfile) + + if seed is None: + self.set_seed(self.config['default_seed']) + else: + self.set_seed(seed) + + self.do_transfer_learning = False + + self.set_obs_forecast_IDs(dataset='train') + self.set_transfer_forecast_IDs() + self.all_forecast_IDs = self.obs_forecast_IDs + self.remove_missing_dates() + self.set_variable_path_formats() + self.set_number_of_input_channels_for_each_input_variable() + self.load_polarholes() + self.determine_tot_num_channels() + self.on_epoch_end() + + if self.config['verbose_level'] >= 1: + print("Setup complete.\n") + + def set_obs_forecast_IDs(self, dataset='train'): + """ + Build up a list of forecast initialisation dates for the train, val, or + test sets based on the configuration JSON file start & end points for + each dataset. + """ + + forecast_start_date_ends = self.config['sample_IDs']['obs_{}_dates'.format(dataset)] + + if forecast_start_date_ends is not None: + + # Convert to Pandas Timestamps + forecast_start_date_ends = [ + pd.Timestamp(date).to_pydatetime() for date in forecast_start_date_ends + ] + + self.obs_forecast_IDs = list(pd.date_range( + forecast_start_date_ends[0], + forecast_start_date_ends[1], + freq='MS', + closed='right', + )) + + def set_transfer_forecast_IDs(self): + + ''' + Use self.cmip6_transfer_train_dict to set up a list array of + 3-tuples of the form: + (cmip6_model_name, member_id, forecast_start_date) + + This list is used as IDs into the transfer data hierarchy + to train on all cmip6 models and and their runs simultaneously. + ''' + + self.transfer_forecast_IDs = [] + for cmip6_model_name, member_id_dict in self.config['cmip6_run_dict'].items(): + for member_id, (start_date, end_date) in member_id_dict.items(): + + member_id_dates = list(pd.date_range( + start_date, + end_date, + freq='MS', + closed='right', + )) + + self.transfer_forecast_IDs.extend( + itertools.product([cmip6_model_name], [member_id], member_id_dates) + ) + + def set_variable_path_formats(self): + + """ + Initialise the paths to the .npy files of each variable based on + `self.config['input_data']`. + """ + + if self.config['verbose_level'] >= 1: + print('Setting up the variable paths for {}... '.format(self.config['dataset_name']), + end='', flush=True) + + # Parent folder for this dataset + self.dataset_path = os.path.join(config.network_dataset_folder, self.config['dataset_name']) + + # Dictionary data structure to store image variable paths + self.variable_paths = {} + + for varname, vardict in self.config['input_data'].items(): + + if 'metadata' not in vardict.keys(): + self.variable_paths[varname] = {} + + for data_format in vardict.keys(): + + if vardict[data_format]['include'] is True: + + if not self.do_transfer_learning: + path = os.path.join( + self.dataset_path, 'obs', + varname, data_format, '{:04d}_{:02d}.npy' + ) + elif self.do_transfer_learning: + path = os.path.join( + self.dataset_path, 'transfer', '{}', '{}', + varname, data_format, '{:04d}_{:02d}.npy' + ) + + self.variable_paths[varname][data_format] = path + + elif 'metadata' in vardict.keys(): + + if vardict['include'] is True: + + if varname == 'land': + path = os.path.join(self.dataset_path, 'meta', 'land.npy') + self.variable_paths['land'] = path + + elif varname == 'circmonth': + path = os.path.join(self.dataset_path, 'meta', + '{}_month_{:02d}.npy') + self.variable_paths['circmonth'] = path + + if self.config['verbose_level'] >= 1: + print('Done.') + + def set_seed(self, seed): + """ + Set the seed used by the random generator (used to randomly shuffle + the ordering of training samples after each epoch). + """ + if self.config['verbose_level'] >= 1: + print("Setting the data generator's random seed to {}".format(seed)) + self.rng = np.random.default_rng(seed) + + def determine_variable_names(self): + """ + Set up a list of strings for the names of each input variable (in the + correct order) by looping over the `input_data` dictionary. + """ + variable_names = [] + + for varname, vardict in self.config['input_data'].items(): + # Input variables that span time + if 'metadata' not in vardict.keys(): + for data_format in vardict.keys(): + if vardict[data_format]['include']: + if data_format != 'linear_trend': + for lag in np.arange(1, vardict[data_format]['max_lag']+1): + variable_names.append(varname+'_{}_{}'.format(data_format, lag)) + elif data_format == 'linear_trend': + for leadtime in np.arange(1, self.config['n_forecast_months']+1): + variable_names.append(varname+'_{}_{}'.format(data_format, leadtime)) + + # Metadata input variables that don't span time + elif 'metadata' in vardict.keys() and vardict['include']: + if varname == 'land': + variable_names.append(varname) + + elif varname == 'circmonth': + variable_names.append('cos(month)') + variable_names.append('sin(month)') + + return variable_names + + def set_number_of_input_channels_for_each_input_variable(self): + """ + Build up the dict `self.num_input_channels_dict` to store the number of input + channels spanned by each input variable. + """ + + if self.config['verbose_level'] >= 1: + print("Setting the number of input months for each input variable.") + + self.num_input_channels_dict = {} + + for varname, vardict in self.config['input_data'].items(): + if 'metadata' not in vardict.keys(): + # Variables that span time + for data_format in vardict.keys(): + if vardict[data_format]['include']: + varname_format = varname+'_{}'.format(data_format) + if data_format != 'linear_trend': + self.num_input_channels_dict[varname_format] = vardict[data_format]['max_lag'] + elif data_format == 'linear_trend': + self.num_input_channels_dict[varname_format] = self.config['n_forecast_months'] + + # Metadata input variables that don't span time + elif 'metadata' in vardict.keys() and vardict['include']: + if varname == 'land': + self.num_input_channels_dict[varname] = 1 + + if varname == 'circmonth': + self.num_input_channels_dict[varname] = 2 + + def determine_tot_num_channels(self): + """ + Determine the number of channels for the input 3D volumes. + """ + + self.tot_num_channels = 0 + for varname, num_channels in self.num_input_channels_dict.items(): + self.tot_num_channels += num_channels + + def all_sic_input_dates_from_forecast_start_date(self, forecast_start_date): + """ + Return a list of all the SIC dates used as input for a particular forecast + date, based on the "max_lag" options of self.config['input_data']. + """ + + # Find all SIC lags + max_lags = [] + if self.config['input_data']['siconca']['abs']['include']: + max_lags.append(self.config['input_data']['siconca']['abs']['max_lag']) + if self.config['input_data']['siconca']['anom']['include']: + max_lags.append(self.config['input_data']['siconca']['anom']['max_lag']) + max_lag = np.max(max_lags) + + input_dates = [ + forecast_start_date - pd.DateOffset(months=int(lag)) for lag in np.arange(1, max_lag+1) + ] + + return input_dates + + def check_for_missing_date_dependence(self, forecast_start_date): + """ + Check a forecast ID and return a bool for whether any of the input SIC maps + are missing. Used to remove forecast IDs that depend on missing SIC data. + + Note: If one of the _forecast_ dates are missing but not _input_ dates, + the sample weight matrix for that date will be all zeroes so that the + samples for that date do not appear in the loss function. + """ + contains_missing_date = False + + # Check SIC input dates + input_dates = self.all_sic_input_dates_from_forecast_start_date(forecast_start_date) + + for input_date in input_dates: + if any([input_date == missing_date for missing_date in config.missing_dates]): + contains_missing_date = True + break + + return contains_missing_date + + def remove_missing_dates(self): + + ''' + Remove dates from self.obs_forecast_IDs that depend on a missing + observation of SIC. + ''' + + if self.config['verbose_level'] >= 2: + print('Checking forecast start dates for missing SIC dates... ', end='', flush=True) + + new_obs_forecast_IDs = [] + for forecast_start_date in self.obs_forecast_IDs: + if self.check_for_missing_date_dependence(forecast_start_date): + if self.config['verbose_level'] >= 3: + print('Removing {}, '.format( + forecast_start_date.strftime('%Y_%m_%d')), end='', flush=True) + + else: + new_obs_forecast_IDs.append(forecast_start_date) + + self.obs_forecast_IDs = new_obs_forecast_IDs + + def load_polarholes(self): + """ + Loads each of the polar holes. + """ + + if self.config['verbose_level'] >= 1: + tic = time.time() + print("Loading and augmenting the polar holes... ", end='', flush=True) + + polarhole_path = os.path.join(config.mask_data_folder, config.polarhole1_fname) + self.polarhole1_mask = np.load(polarhole_path) + + polarhole_path = os.path.join(config.mask_data_folder, config.polarhole2_fname) + self.polarhole2_mask = np.load(polarhole_path) + + if config.use_polarhole3: + polarhole_path = os.path.join(config.mask_data_folder, config.polarhole3_fname) + self.polarhole3_mask = np.load(polarhole_path) + + self.nopolarhole_mask = np.full((432, 432), False) + + if self.config['verbose_level'] >= 1: + print("Done in {:.0f}s.\n".format(time.time() - tic)) + + def determine_polar_hole_mask(self, forecast_start_date): + """ + Determine which polar hole mask to use (if any) by finding the oldest SIC + input month based on the current output month. The polar hole active for + the oldest input month is used (because the polar hole size decreases + monotonically over time, and we wish to use the largest polar hole for + the input data). + + Parameters: + forecast_start_date (pd.Timestamp): Timepoint for the forecast initialialisation. + + Returns: + polarhole_mask: Mask array with NaNs on polar hole grid cells and 1s + elsewhere. + """ + + oldest_input_date = min(self.all_sic_input_dates_from_forecast_start_date(forecast_start_date)) + + if oldest_input_date <= config.polarhole1_final_date: + polarhole_mask = self.polarhole1_mask + if self.config['verbose_level'] >= 3: + print("Forecast start date: {}, polar hole: {}".format( + forecast_start_date.strftime("%Y_%m"), 1)) + + elif oldest_input_date <= config.polarhole2_final_date: + polarhole_mask = self.polarhole2_mask + if self.config['verbose_level'] >= 3: + print("Forecast start date: {}, polar hole: {}".format( + forecast_start_date.strftime("%Y_%m"), 2)) + + else: + polarhole_mask = self.nopolarhole_mask + if self.config['verbose_level'] >= 3: + print("Forecast start date: {}, polar hole: {}".format( + forecast_start_date.strftime("%Y_%m"), "none")) + + return polarhole_mask + + def determine_active_grid_cell_mask(self, forecast_date): + """ + Determine which active grid cell mask to use (a boolean array with + True on active cells and False on inactive cells). The cells with 'True' + are where predictions are to be made with IceNet. The active grid cell + mask for a particular month is determined by the sum of the land cells, + the ocean cells (for that month), and the missing polar hole. + + The mask is used for removing 'inactive' cells (such as land or polar + hole cells) from the loss function in self.data_generation. + """ + + output_month_str = '{:02d}'.format(forecast_date.month) + output_active_grid_cell_mask_fname = config.active_grid_cell_file_format. \ + format(output_month_str) + output_active_grid_cell_mask_path = os.path.join( + config.mask_data_folder, output_active_grid_cell_mask_fname) + output_active_grid_cell_mask = np.load(output_active_grid_cell_mask_path) + + # Only use the polar hole mask if predicting observational data + if not self.do_transfer_learning: + polarhole_mask = self.determine_polar_hole_mask(forecast_date) + + # Add the polar hole mask to that land/ocean mask for the current month + output_active_grid_cell_mask[polarhole_mask] = False + + return output_active_grid_cell_mask + + def turn_on_transfer_learning(self): + + ''' + Converts the data loader to use CMIP6 pre-training data + for transfer learning. + ''' + + self.do_transfer_learning = True + self.all_forecast_IDs = self.transfer_forecast_IDs + self.on_epoch_end() # Shuffle transfer training indexes + self.set_variable_path_formats() + + def turn_off_transfer_learning(self): + + ''' + Converts the data loader back to using ERA5/OSI-SAF observational + training data. + ''' + + self.do_transfer_learning = False + self.all_forecast_IDs = self.obs_forecast_IDs + self.on_epoch_end() # Shuffle transfer training indexes + self.set_variable_path_formats() + + def convert_to_validation_data_loader(self): + + """ + Resets the `all_forecast_IDs` array to correspond to the validation + months defined by the data loader configuration file. + """ + + self.set_obs_forecast_IDs(dataset='val') + self.remove_missing_dates() + self.all_forecast_IDs = self.obs_forecast_IDs + + def convert_to_test_data_loader(self): + + """ + As above but for the testing months. + """ + + self.set_obs_forecast_IDs(dataset='test') + self.remove_missing_dates() + self.all_forecast_IDs = self.obs_forecast_IDs + + def data_generation(self, forecast_IDs): + """ + Generate input-output data for IceNet for a given forecast ID. + + Parameters: + forecast_IDs (list): + If self.do_transfer_learning is False, a list of pd.Timestamp objects + corresponding to the forecast initialisation dates (first month + being forecast) for the batch of X-y data to load. + + If self.do_transfer_learning is True, a list of tuples + of the form (cmip6_model_name, member_id, forecast_start_date). + + Returns: + X (ndarray): Batch of input 3D volumes. + + y (ndarray): Batch of ground truth output SIC class maps + + sample_weight (ndarray): Batch of pixelwise weights for weighting the + loss function (masking outside the active grid cell region and + up-weighting summer months). + """ + + # Allow non-list input for single forecasts + forecast_IDs = pd.Timestamp(forecast_IDs) if isinstance(forecast_IDs, str) else forecast_IDs + forecast_IDs = [forecast_IDs] if not isinstance(forecast_IDs, list) else forecast_IDs + + current_batch_size = len(forecast_IDs) + + if self.do_transfer_learning: + cmip6_model_names = [forecast_ID[0] for forecast_ID in forecast_IDs] + cmip6_member_ids = [forecast_ID[1] for forecast_ID in forecast_IDs] + forecast_start_dates = [forecast_ID[2] for forecast_ID in forecast_IDs] + else: + forecast_start_dates = forecast_IDs + + ######################################################################## + # OUTPUT LABELS + ######################################################################## + + # Build up the set of N_samps output SIC time-series + # (each n_forecast_months long in the time dimension) + + # To become array of shape (N_samps, *raw_data_shape, n_forecast_months) + batch_sic_list = [] + + # True = forecasts months corresponding to no data + missing_month_dict = {} + + for sample_idx, forecast_date in enumerate(forecast_start_dates): + + # To become array of shape (*raw_data_shape, n_forecast_months) + sample_sic_list = [] + + # List of forecast indexes with missing data + missing_month_dict[sample_idx] = [] + + for forecast_leadtime_idx in range(self.config['n_forecast_months']): + + forecast_date = forecast_start_dates[sample_idx] + pd.DateOffset(months=forecast_leadtime_idx) + + if self.do_transfer_learning: + sample_sic_list.append( + np.load(self.variable_paths['siconca']['abs'].format( + cmip6_model_names[sample_idx], cmip6_member_ids[sample_idx], + forecast_date.year, forecast_date.month)) + ) + + elif not self.do_transfer_learning: + if any([forecast_date == missing_date for missing_date in config.missing_dates]): + # Output file does not exist + sample_sic_list.append(np.zeros(self.config['raw_data_shape'])) + + else: + fpath = self.variable_paths['siconca']['abs'].format( + forecast_date.year, forecast_date.month) + if os.path.exists(fpath): + sample_sic_list.append(np.load(fpath)) + else: + # Ground truth data doesn't exist: fill with NaNs + sample_sic_list.append( + np.full(self.config['raw_data_shape'], np.nan, dtype=np.float32)) + + batch_sic_list.append(np.stack(sample_sic_list, axis=2)) + + batch_sic = np.stack(batch_sic_list, axis=0) + + no_ice_gridcells = batch_sic <= 0.15 + ice_gridcells = batch_sic >= 0.80 + marginal_ice_gridcells = ~((no_ice_gridcells) | (ice_gridcells)) + + # Categorical representation with channel dimension for class probs + y = np.zeros(( + current_batch_size, + *self.config['raw_data_shape'], + self.config['n_forecast_months'], + 3 + ), dtype=np.float32) + + y[no_ice_gridcells, 0] = 1 + y[marginal_ice_gridcells, 1] = 1 + y[ice_gridcells, 2] = 1 + + # Move lead time to final axis + y = np.moveaxis(y, source=3, destination=4) + + # Missing months + for sample_idx, forecast_leadtime_idx_list in missing_month_dict.items(): + if len(forecast_leadtime_idx_list) > 0: + y[sample_idx, :, :, :, forecast_leadtime_idx_list] = 0 + + ######################################################################## + # PIXELWISE LOSS FUNCTION WEIGHTING + ######################################################################## + + sample_weight = np.zeros(( + current_batch_size, + *self.config['raw_data_shape'], + 1, # Broadcastable class dimension + self.config['n_forecast_months'] + ), dtype=np.float32) + for sample_idx, forecast_date in enumerate(forecast_start_dates): + + for forecast_leadtime_idx in range(self.config['n_forecast_months']): + + forecast_date = forecast_start_dates[sample_idx] + pd.DateOffset(months=forecast_leadtime_idx) + + if any([forecast_date == missing_date for missing_date in config.missing_dates]): + # Leave sample weighting as all-zeros + pass + + else: + # Zero loss outside of 'active grid cells' + sample_weight_ij = self.determine_active_grid_cell_mask(forecast_date) + sample_weight_ij = sample_weight_ij.astype(np.float32) + + # Scale the loss for each month s.t. March is + # scaled by 1 and Sept is scaled by 1.77 + if self.config['loss_weight_months']: + sample_weight_ij *= 33928. / np.sum(sample_weight_ij) + + sample_weight[sample_idx, :, :, 0, forecast_leadtime_idx] = \ + sample_weight_ij + + ######################################################################## + # INPUT FEATURES + ######################################################################## + + # Batch tensor + X = np.zeros(( + current_batch_size, + *self.config['raw_data_shape'], + self.tot_num_channels + ), dtype=np.float32) + + # Build up the batch of inputs + for sample_idx, forecast_start_date in enumerate(forecast_start_dates): + + present_date = forecast_start_date - relativedelta(months=1) + + # Initialise variable indexes used to fill the input tensor `X` + variable_idx1 = 0 + variable_idx2 = 0 + + for varname, vardict in self.config['input_data'].items(): + + if 'metadata' not in vardict.keys(): + + for data_format in vardict.keys(): + + if vardict[data_format]['include']: + + varname_format = '{}_{}'.format(varname, data_format) + + if data_format != 'linear_trend': + lbs = range(vardict[data_format]['max_lag']) + input_months = [present_date - relativedelta(months=lb) for lb in lbs] + elif data_format == 'linear_trend': + input_months = [present_date + relativedelta(months=forecast_leadtime) + for forecast_leadtime in np.arange(1, self.config['n_forecast_months']+1)] + + variable_idx2 += self.num_input_channels_dict[varname_format] + + if not self.do_transfer_learning: + X[sample_idx, :, :, variable_idx1:variable_idx2] = \ + np.stack([np.load(self.variable_paths[varname][data_format].format( + date.year, date.month)) + for date in input_months], axis=-1) + elif self.do_transfer_learning: + cmip6_model_name = cmip6_model_names[sample_idx] + cmip6_member_id = cmip6_member_ids[sample_idx] + + X[sample_idx, :, :, variable_idx1:variable_idx2] = \ + np.stack([np.load(self.variable_paths[varname][data_format].format( + cmip6_model_name, cmip6_member_id, date.year, date.month)) + for date in input_months], axis=-1) + + variable_idx1 += self.num_input_channels_dict[varname_format] + + elif 'metadata' in vardict.keys() and vardict['include']: + + variable_idx2 += self.num_input_channels_dict[varname] + + if varname == 'land': + X[sample_idx, :, :, variable_idx1] = np.load(self.variable_paths['land']) + + elif varname == 'circmonth': + X[sample_idx, :, :, variable_idx1] = \ + np.load(self.variable_paths['circmonth'].format('cos', forecast_start_date.month)) + X[sample_idx, :, :, variable_idx1 + 1] = \ + np.load(self.variable_paths['circmonth'].format('sin', forecast_start_date.month)) + + variable_idx1 += self.num_input_channels_dict[varname] + + return X, y, sample_weight + + def __getitem__(self, batch_idx): + ''' + Generate one batch of data of size `batch_size` at batch index `batch_idx` + into the set of batches in the epoch. + ''' + + batch_start = batch_idx * self.config['batch_size'] + batch_end = np.min([(batch_idx + 1) * self.config['batch_size'], len(self.all_forecast_IDs)]) + + sample_idxs = np.arange(batch_start, batch_end) + batch_IDs = [self.all_forecast_IDs[sample_idx] for sample_idx in sample_idxs] + + return self.data_generation(batch_IDs) + + def __len__(self): + ''' Returns the number of batches per training epoch. ''' + return int(np.ceil(len(self.all_forecast_IDs) / self.config['batch_size'])) + + def on_epoch_end(self): + """ Randomly shuffles training samples after each epoch. """ + + if self.config['verbose_level'] >= 2: + print("on_epoch_end called") + + # Randomly shuffle the forecast IDs in-place + self.rng.shuffle(self.all_forecast_IDs) + + +################## MISC FUNCTIONS +################################################################################ + + +def create_results_dataset_index(model_compute_list, leadtimes, + all_target_dates, icenet_ID, + icenet_seeds): + + ''' + Returns a pandas.MultiIndex object of results dataset indexes for a + given list of models to compute metrics for. For IceNet, the 'Ensemble + member' column delineates the performance of each IceNet ensemble + member (identified by the integer random seed value it was trained + with) and the ensemble mean models ('ensemble' or 'ensemble_tempscaled'). + ''' + + multi_index = pd.MultiIndex.from_product( + [model_compute_list, leadtimes, all_target_dates]) + + idxs = [] + for row in multi_index: + model = row[0] + row = [[item] for item in row] + if model == icenet_ID: + idxs.extend(list(itertools.product(*row, icenet_seeds))) + else: + idxs.extend(list(itertools.product(*row, ['NA']))) + + multi_index = pd.MultiIndex.from_tuples( + idxs, names=['Model', 'Leadtime', 'Forecast date', 'Ensemble member']).\ + reorder_levels(['Model', 'Ensemble member', 'Leadtime', 'Forecast date']) + + return multi_index + + +def make_varname_verbose(varname, leadtime, fc_month_idx): + + ''' + Takes IceNet short variable name (e.g. siconca_abs_3) and converts it to a + long name for a given forecast calendar month and lead time (e.g. + 'Feb SIC'). + + Inputs: + varname: Short variable name. + leadtime: Lead time of the forecast. + fc_month_index: Mod-12 calendar month index for the month being forecast + (e.g. 8 for September) + + Returns: + verbose_varname: Long variable name. + ''' + + month_names = np.array(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']) + + varname_regex = re.compile('^(.*)_(abs|anom|linear_trend)_([0-9]+)$') + + var_lookup_table = { + 'siconca': 'SIC', + 'tas': '2m air temperature', + 'ta500': '500 hPa air temperature', + 'tos': 'sea surface temperature', + 'rsds': 'downwelling solar radiation', + 'rsus': 'upwelling solar radiation', + 'psl': 'sea level pressure', + 'zg500': '500 hPa geopotential height', + 'zg250': '250 hPa geopotential height', + 'ua10': '10 hPa zonal wind speed', + 'uas': 'x-direction wind', + 'vas': 'y-direction wind' + } + + initialisation_month_idx = (fc_month_idx - leadtime) % 12 + + varname_match = varname_regex.match(varname) + + field = varname_match[1] + data_format = varname_match[2] + lead_or_lag = int(varname_match[3]) + + verbose_varname = '' + + month_suffix = ' ' + month_prefix = '' + if data_format != 'linear_trend': + # Read back from initialisation month to get input lag month + lag = lead_or_lag # In no of months + input_month_name = month_names[(initialisation_month_idx - lag + 1) % 12] + + if (initialisation_month_idx - lag + 1) // 12 == -1: + # Previous calendar year + month_prefix = 'Previous ' + + elif data_format == 'linear_trend': + # Read forward from initialisation month to get linear trend forecast month + lead = lead_or_lag # In no of months + input_month_name = month_names[(initialisation_month_idx + lead) % 12] + + if (initialisation_month_idx + lead) // 12 == 1: + # Next calendar year + month_prefix = 'Next ' + + # Month the input corresponds to + verbose_varname += month_prefix + input_month_name + month_suffix + + # verbose variable name + if data_format != 'linear_trend': + verbose_varname += var_lookup_table[field] + if data_format == 'anom': + verbose_varname += ' anomaly' + elif data_format == 'linear_trend': + verbose_varname += 'linear trend SIC forecast' + + return verbose_varname + + +def make_varname_verbose_any_leadtime(varname): + + ''' As above, but agnostic to what the target month or lead time is. E.g. + "SIC (1)" for sea ice concentration at a lag of 1 month. ''' + + varname_regex = re.compile('^(.*)_(abs|anom|linear_trend)_([0-9]+)$') + + var_lookup_table = { + 'siconca': 'SIC', + 'tas': '2m air temperature', + 'ta500': '500 hPa air temperature', + 'tos': 'sea surface temperature', + 'rsds': 'downwelling solar radiation', + 'rsus': 'upwelling solar radiation', + 'psl': 'sea level pressure', + 'zg500': '500 hPa geopotential height', + 'zg250': '250 hPa geopotential height', + 'ua10': '10 hPa zonal wind speed', + 'uas': 'x-direction wind', + 'vas': 'y-direction wind', + 'land': 'land mask', + 'cos(month)': 'cos(init month)', + 'sin(month)': 'sin(init month)', + } + + exception_vars = ['cos(month)', 'sin(month)', 'land'] + + if varname in exception_vars: + return var_lookup_table[varname] + else: + varname_match = varname_regex.match(varname) + + field = varname_match[1] + data_format = varname_match[2] + lead_or_lag = int(varname_match[3]) + + # verbose variable name + if data_format != 'linear_trend': + verbose_varname = var_lookup_table[field] + if data_format == 'anom': + verbose_varname += ' anomaly' + elif data_format == 'linear_trend': + verbose_varname = 'Linear trend SIC forecast' + + verbose_varname += ' ({:.0f})'.format(lead_or_lag) + + return verbose_varname + + +################################################################################ +################## FUNCTIONS +################################################################################ + + +def assignLatLonCoordSystem(cube): + ''' Assign coordinate system to iris cube to allow regridding. ''' + + cube.coord('latitude').coord_system = iris.coord_systems.GeogCS(6367470.0) + cube.coord('longitude').coord_system = iris.coord_systems.GeogCS(6367470.0) + + return cube + + +def fix_near_real_time_era5_func(latlon_path): + + ''' + Near-real-time ERA5 data is classed as a different dataset called 'ERA5T'. + This results in a spurious 'expver' dimension. This method detects + whether that dim is present and removes it, concatenating into one array + ''' + + ds = xr.open_dataarray(latlon_path) + + if len(ds.data.shape) == 4: + print('Fixing spurious ERA5 "expver dimension for {}.'.format(latlon_path)) + + arr = xr.open_dataarray(latlon_path).data + arr = ds.data + # Expver 1 (ERA5) + era5_months = ~np.isnan(arr[:, 0, :, :]).all(axis=(1, 2)) + + # Expver 2 (ERA5T - near real time) + era5t_months = ~np.isnan(arr[:, 1, :, :]).all(axis=(1, 2)) + + ds = xr.concat((ds[era5_months, 0, :], ds[era5t_months, 1, :]), dim='time') + + ds = ds.reset_coords('expver', drop=True) + + os.remove(latlon_path) + ds.load().to_netcdf(latlon_path) + + +############################################################################### +############### LEARNING RATE SCHEDULER +############################################################################### + + +def make_exp_decay_lr_schedule(rate, start_epoch=1, end_epoch=np.inf, verbose=False): + + ''' Returns an exponential learning rate function that multiplies by + exp(-rate) each epoch after `start_epoch`. ''' + + def lr_scheduler_exp_decay(epoch, lr): + ''' Learning rate scheduler for fine tuning. + Exponential decrease after start_epoch until end_epoch. ''' + + if epoch >= start_epoch and epoch < end_epoch: + lr = lr * np.math.exp(-rate) + + if verbose: + print('\nSetting learning rate to: {}\n'.format(lr)) + + return lr + + return lr_scheduler_exp_decay + + +############################################################################### +############### REGRIDDING VECTOR DATA +############################################################################### + + +def rotate_grid_vectors(u_cube, v_cube, angles): + """ + Author: Tony Phillips (BAS) + + Wrapper for :func:`~iris.analysis.cartography.rotate_grid_vectors` + that can rotate multiple masked spatial fields in one go by iterating + over the horizontal spatial axes in slices + """ + # lists to hold slices of rotated vectors + u_r_all = iris.cube.CubeList() + v_r_all = iris.cube.CubeList() + + # get the X and Y dimension coordinates for each source cube + u_xy_coords = [u_cube.coord(axis='x', dim_coords=True), + u_cube.coord(axis='y', dim_coords=True)] + v_xy_coords = [v_cube.coord(axis='x', dim_coords=True), + v_cube.coord(axis='y', dim_coords=True)] + + # iterate over X, Y slices of the source cubes, rotating each in turn + for u, v in zip(u_cube.slices(u_xy_coords, ordered=False), + v_cube.slices(v_xy_coords, ordered=False)): + u_r, v_r = iris.analysis.cartography.rotate_grid_vectors(u, v, angles) + u_r_all.append(u_r) + v_r_all.append(v_r) + + # return the slices, merged back together into a pair of cubes + return (u_r_all.merge_cube(), v_r_all.merge_cube()) + + +def gridcell_angles_from_dim_coords(cube): + """ + Author: Tony Phillips (BAS) + + Wrapper for :func:`~iris.analysis.cartography.gridcell_angles` + that derives the 2D X and Y lon/lat coordinates from 1D X and Y + coordinates identifiable as 'x' and 'y' axes + + The provided cube must have a coordinate system so that its + X and Y coordinate bounds (which are derived if necessary) + can be converted to lons and lats + """ + + # get the X and Y dimension coordinates for the cube + x_coord = cube.coord(axis='x', dim_coords=True) + y_coord = cube.coord(axis='y', dim_coords=True) + + # add bounds if necessary + if not x_coord.has_bounds(): + x_coord = x_coord.copy() + x_coord.guess_bounds() + if not y_coord.has_bounds(): + y_coord = y_coord.copy() + y_coord.guess_bounds() + + # get the grid cell bounds + x_bounds = x_coord.bounds + y_bounds = y_coord.bounds + nx = x_bounds.shape[0] + ny = y_bounds.shape[0] + + # make arrays to hold the ordered X and Y bound coordinates + x = np.zeros((ny, nx, 4)) + y = np.zeros((ny, nx, 4)) + + # iterate over the bounds (in order BL, BR, TL, TR), mesh them and + # put them into the X and Y bound coordinates (in order BL, BR, TR, TL) + c = [0, 1, 3, 2] + cind = 0 + for yi in [0, 1]: + for xi in [0, 1]: + xy = np.meshgrid(x_bounds[:, xi], y_bounds[:, yi]) + x[:,:,c[cind]] = xy[0] + y[:,:,c[cind]] = xy[1] + cind += 1 + + # convert the X and Y coordinates to longitudes and latitudes + source_crs = cube.coord_system().as_cartopy_crs() + target_crs = ccrs.PlateCarree() + pts = target_crs.transform_points(source_crs, x.flatten(), y.flatten()) + lons = pts[:, 0].reshape(x.shape) + lats = pts[:, 1].reshape(x.shape) + + # get the angles + angles = iris.analysis.cartography.gridcell_angles(lons, lats) + + # add the X and Y dimension coordinates from the cube to the angles cube + angles.add_dim_coord(y_coord, 0) + angles.add_dim_coord(x_coord, 1) + + # if the cube's X dimension preceeds its Y dimension + # transpose the angles to match + if cube.coord_dims(x_coord)[0] < cube.coord_dims(y_coord)[0]: + angles.transpose() + + return angles + + +def invert_gridcell_angles(angles): + """ + Author: Tony Phillips (BAS) + + Negate a cube of gridcell angles in place, transforming + gridcell_angle_from_true_east <--> true_east_from_gridcell_angle + """ + angles.data *= -1 + + names = ['true_east_from_gridcell_angle', 'gridcell_angle_from_true_east'] + name = angles.name() + if name in names: + angles.rename(names[1 - names.index(name)]) + + +############################################################################### +############### CMIP6 +############################################################################### + + +# Below taken from https://hub.binder.pangeo.io/user/pangeo-data-pan--cmip6-examples-ro965nih/lab +def esgf_search(server="https://esgf-node.llnl.gov/esg-search/search", + files_type="OPENDAP", local_node=False, latest=True, project="CMIP6", + verbose1=False, verbose2=False, format="application%2Fsolr%2Bjson", + use_csrf=False, **search): + client = requests.session() + payload = search + payload["project"] = project + payload["type"]= "File" + if latest: + payload["latest"] = "true" + if local_node: + payload["distrib"] = "false" + if use_csrf: + client.get(server) + if 'csrftoken' in client.cookies: + # Django 1.6 and up + csrftoken = client.cookies['csrftoken'] + else: + # older versions + csrftoken = client.cookies['csrf'] + payload["csrfmiddlewaretoken"] = csrftoken + + payload["format"] = format + + offset = 0 + numFound = 10000 + all_files = [] + files_type = files_type.upper() + while offset < numFound: + payload["offset"] = offset + url_keys = [] + for k in payload: + url_keys += ["{}={}".format(k, payload[k])] + + url = "{}/?{}".format(server, "&".join(url_keys)) + if verbose1: + print(url) + r = client.get(url) + r.raise_for_status() + resp = r.json()["response"] + numFound = int(resp["numFound"]) + resp = resp["docs"] + offset += len(resp) + for d in resp: + if verbose2: + for k in d: + print("{}: {}".format(k,d[k])) + url = d["url"] + for f in d["url"]: + sp = f.split("|") + if sp[-1] == files_type: + all_files.append(sp[0].split(".html")[0]) + return sorted(all_files) + + +def regrid_cmip6(cmip6_cube, grid_cube, verbose=False): + + if verbose: + tic = time.time() + print("regridding... ", end='', flush=True) + + cs = grid_cube.coord_system().ellipsoid + + for coord in ['longitude', 'latitude']: + cmip6_cube.coord(coord).coord_system = cs + + cmip6_ease = cmip6_cube.regrid(grid_cube, iris.analysis.Linear()) + + if verbose: + dur = time.time() - tic + print("done in {}m:{:.0f}s... ".format(np.floor(dur / 60), dur % 60), end='', flush=True) + + return cmip6_ease + + +def save_cmip6(cmip6_ease, fpath, compress=True, verbose=False): + tic = time.time() + + if compress: + if verbose: + print('compressing & saving... ', end='', flush=True) + iris.fileformats.netcdf.save(cmip6_ease, fpath, complevel=7, zlib=True) + else: + if verbose: + print('saving uncompressed... ', end='', flush=True) + iris.save(cmip6_ease, fpath) + + if verbose: + dur = time.time() - tic + print("done in {}m:{:.0f}s... ".format(np.floor(dur / 60), dur % 60), end='', flush=True) + + +############################################################################### +############### PLOTTING +############################################################################### + + +def compute_heatmap(results_df, model, seed='NA', metric='Binary accuracy'): + ''' + Returns a binary accuracy heatmap of lead time vs. calendar month + for a given model. + ''' + + month_names = np.array(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']) + + # Mean over calendar month + mean_df = results_df.loc[model, seed].reset_index().\ + groupby(['Calendar month', 'Leadtime']).mean() + + # Pivot + heatmap_df = mean_df.reset_index().\ + pivot('Calendar month', 'Leadtime', metric).reindex(month_names) + + return heatmap_df + + +def arr_to_ice_edge_arr(arr, thresh, land_mask, region_mask): + + ''' + Compute a boolean mask with True over ice edge contour grid cells using + matplotlib.pyplot.contour and an input threshold to define the ice edge + (e.g. 0.15 for the 15% SIC ice edge or 0.5 for SIP forecasts). The contour + along the coastline is removed using the region mask. + ''' + + X, Y = np.meshgrid(np.arange(arr.shape[0]), np.arange(arr.shape[1])) + X = X.T + Y = Y.T + + cs = plt.contour(X, Y, arr, [thresh], alpha=0) # Do not plot on any axes + x = [] + y = [] + for p in cs.collections[0].get_paths(): + x_i, y_i = p.vertices.T + x.extend(np.round(x_i)) + y.extend(np.round(y_i)) + x = np.array(x, int) + y = np.array(y, int) + ice_edge_arr = np.zeros(arr.shape, dtype=bool) + ice_edge_arr[x, y] = True + # Mask out ice edge contour that hugs the coastline + ice_edge_arr[land_mask] = False + ice_edge_arr[region_mask == 13] = False + + return ice_edge_arr + + +def arr_to_ice_edge_rgba_arr(arr, thresh, land_mask, region_mask, rgb): + + ice_edge_arr = arr_to_ice_edge_arr(arr, thresh, land_mask, region_mask) + + # Contour pixels -> alpha=1, alpha=0 elsewhere + ice_edge_rgba_arr = np.zeros((*arr.shape, 4)) + ice_edge_rgba_arr[:, :, 3] = ice_edge_arr + ice_edge_rgba_arr[:, :, :3] = rgb + + return ice_edge_rgba_arr + + +############################################################################### +############### VIDEOS +############################################################################### + + +def xarray_to_video(da, video_path, fps, mask=None, mask_type='contour', clim=None, + crop=None, data_type='abs', video_dates=None, cmap='viridis', + figsize=15, dpi=300): + + ''' + Generate video of an xarray.DataArray. Optionally input a list of + `video_dates` to show, otherwise the full set of time coordiantes + of the dataset is used. + + Parameters: + da (xr.DataArray): Dataset to create video of. + + video_path (str): Path to save the video to. + + fps (int): Frames per second of the video. + + mask (np.ndarray): Boolean mask with True over masked elements to overlay + as a contour or filled contour. Defaults to None (no mask plotting). + + mask_type (str): 'contour' or 'contourf' dictating whether the mask is overlaid + as a contour line or a filled contour. + + data_type (str): 'abs' or 'anom' describing whether the data is in absolute + or anomaly format. If anomaly, the colorbar is centred on 0. + + video_dates (list): List of Pandas Timestamps or datetime.datetime objects + to plot video from the dataset. + + crop (list): [(a, b), (c, d)] to crop the video from a:b and c:d + + clim (list): Colormap limits. Default is None, in which case the min and max values + of the array are used. + + cmap (str): Matplotlib colormap. + + figsize (int or float): Figure size in inches. + + dpi (int): Figure DPI. + ''' + + if clim is not None: + min = clim[0] + max = clim[1] + elif clim is None: + max = da.max().values + min = da.min().values + + if data_type == 'anom': + if np.abs(max) > np.abs(min): + min = -max + elif np.abs(min) > np.abs(max): + max = -min + + def make_frame(date): + fig, ax = plt.subplots(figsize=(figsize, figsize)) + fig.set_dpi(dpi) + im = ax.imshow(da.sel(time=date), cmap=cmap, clim=(min, max)) + if mask is not None: + if mask_type == 'contour': + ax.contour(mask, levels=[.5, 1], colors='k') + elif mask_type == 'contourf': + ax.contourf(mask, levels=[.5, 1], colors='k') + ax.axes.xaxis.set_visible(False) + ax.axes.yaxis.set_visible(False) + + ax.set_title('{:04d}/{:02d}/{:02d}'.format(date.year, date.month, date.day), fontsize=figsize*4) + + divider = make_axes_locatable(ax) + cax = divider.append_axes('right', size='5%', pad=0.05) + plt.colorbar(im, cax) + + # TEMP crop to image + # fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0) + + fig.canvas.draw() + image = np.frombuffer(fig.canvas.tostring_rgb(), dtype='uint8') + image = image.reshape(fig.canvas.get_width_height()[::-1] + (3,)) + + plt.close() + return image + + if video_dates is None: + video_dates = [pd.Timestamp(date).to_pydatetime() for date in da.time.values] + + if crop is not None: + a = crop[0][0] + b = crop[0][1] + c = crop[1][0] + d = crop[1][1] + da = da.isel(xc=np.arange(a, b), yc=np.arange(c, d)) + if mask is not None: + mask = mask[a:b, c:d] + + imageio.mimsave(video_path, + [make_frame(date) for date in tqdm(video_dates)], + fps=fps) From d7631cfcfd073bcc64c7e370f78b41ac024639b9 Mon Sep 17 00:00:00 2001 From: vanessa-tamara Date: Wed, 17 May 2023 19:49:32 +0200 Subject: [PATCH 2/2] changed cml file for forecast tool --- .../forecast/2021_09_03_1300_icenet_demo.json | 207 ++++++++++++++++++ tools/forecast/config.py | 13 +- tools/forecast/forecast.py | 43 ++-- tools/forecast/forecast.xml | 111 +++------- tools/forecast/masks/.listing | 32 +++ .../masks/active_grid_cell_mask_01.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_02.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_03.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_04.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_05.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_06.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_07.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_08.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_09.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_10.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_11.npy | Bin 0 -> 186752 bytes .../masks/active_grid_cell_mask_12.npy | Bin 0 -> 186752 bytes tools/forecast/masks/land_mask.npy | Bin 0 -> 186752 bytes tools/forecast/masks/polarhole1_mask.npy | Bin 0 -> 186752 bytes tools/forecast/masks/polarhole2_mask.npy | Bin 0 -> 186752 bytes tools/forecast/masks/polarhole3_mask.npy | Bin 0 -> 186752 bytes tools/forecast/masks/region_mask.npy | Bin 0 -> 186752 bytes tools/forecast/meta/cos_month_01.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_02.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_03.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_04.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_05.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_06.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_07.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_08.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_09.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_10.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_11.npy | Bin 0 -> 132 bytes tools/forecast/meta/cos_month_12.npy | Bin 0 -> 132 bytes tools/forecast/meta/land.npy | Bin 0 -> 746624 bytes tools/forecast/meta/sin_month_01.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_02.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_03.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_04.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_05.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_06.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_07.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_08.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_09.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_10.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_11.npy | Bin 0 -> 132 bytes tools/forecast/meta/sin_month_12.npy | Bin 0 -> 132 bytes tools/forecast/models.py | 67 +++--- tools/forecast/utils.py | 54 ++--- 49 files changed, 353 insertions(+), 174 deletions(-) create mode 100644 tools/forecast/2021_09_03_1300_icenet_demo.json create mode 100644 tools/forecast/masks/.listing create mode 100644 tools/forecast/masks/active_grid_cell_mask_01.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_02.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_03.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_04.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_05.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_06.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_07.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_08.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_09.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_10.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_11.npy create mode 100644 tools/forecast/masks/active_grid_cell_mask_12.npy create mode 100644 tools/forecast/masks/land_mask.npy create mode 100644 tools/forecast/masks/polarhole1_mask.npy create mode 100644 tools/forecast/masks/polarhole2_mask.npy create mode 100644 tools/forecast/masks/polarhole3_mask.npy create mode 100644 tools/forecast/masks/region_mask.npy create mode 100644 tools/forecast/meta/cos_month_01.npy create mode 100644 tools/forecast/meta/cos_month_02.npy create mode 100644 tools/forecast/meta/cos_month_03.npy create mode 100644 tools/forecast/meta/cos_month_04.npy create mode 100644 tools/forecast/meta/cos_month_05.npy create mode 100644 tools/forecast/meta/cos_month_06.npy create mode 100644 tools/forecast/meta/cos_month_07.npy create mode 100644 tools/forecast/meta/cos_month_08.npy create mode 100644 tools/forecast/meta/cos_month_09.npy create mode 100644 tools/forecast/meta/cos_month_10.npy create mode 100644 tools/forecast/meta/cos_month_11.npy create mode 100644 tools/forecast/meta/cos_month_12.npy create mode 100644 tools/forecast/meta/land.npy create mode 100644 tools/forecast/meta/sin_month_01.npy create mode 100644 tools/forecast/meta/sin_month_02.npy create mode 100644 tools/forecast/meta/sin_month_03.npy create mode 100644 tools/forecast/meta/sin_month_04.npy create mode 100644 tools/forecast/meta/sin_month_05.npy create mode 100644 tools/forecast/meta/sin_month_06.npy create mode 100644 tools/forecast/meta/sin_month_07.npy create mode 100644 tools/forecast/meta/sin_month_08.npy create mode 100644 tools/forecast/meta/sin_month_09.npy create mode 100644 tools/forecast/meta/sin_month_10.npy create mode 100644 tools/forecast/meta/sin_month_11.npy create mode 100644 tools/forecast/meta/sin_month_12.npy diff --git a/tools/forecast/2021_09_03_1300_icenet_demo.json b/tools/forecast/2021_09_03_1300_icenet_demo.json new file mode 100644 index 0000000..47691d7 --- /dev/null +++ b/tools/forecast/2021_09_03_1300_icenet_demo.json @@ -0,0 +1,207 @@ +{ + "dataloader_name": "icenet_demo", + "dataset_name": "dataset1", + "input_data": { + "siconca": { + "abs": { + "include": true, + "max_lag": 12 + }, + "anom": { + "include": false, + "max_lag": 3 + }, + "linear_trend": { + "include": true + } + }, + "tas": { + "abs": { + "include": false, + "max_lag": 3 + }, + "anom": { + "include": true, + "max_lag": 3 + } + }, + "ta500": { + "abs": { + "include": false, + "max_lag": 3 + }, + "anom": { + "include": true, + "max_lag": 3 + } + }, + "tos": { + "abs": { + "include": false, + "max_lag": 3 + }, + "anom": { + "include": true, + "max_lag": 3 + } + }, + "rsds": { + "abs": { + "include": false, + "max_lag": 3 + }, + "anom": { + "include": true, + "max_lag": 3 + } + }, + "rsus": { + "abs": { + "include": false, + "max_lag": 3 + }, + "anom": { + "include": true, + "max_lag": 3 + } + }, + "psl": { + "abs": { + "include": false, + "max_lag": 3 + }, + "anom": { + "include": true, + "max_lag": 3 + } + }, + "zg500": { + "abs": { + "include": false, + "max_lag": 3 + }, + "anom": { + "include": true, + "max_lag": 3 + } + }, + "zg250": { + "abs": { + "include": false, + "max_lag": 3 + }, + "anom": { + "include": true, + "max_lag": 3 + } + }, + "ua10": { + "abs": { + "include": true, + "max_lag": 3 + }, + "anom": { + "include": false, + "max_lag": 3 + } + }, + "uas": { + "abs": { + "include": true, + "max_lag": 1 + }, + "anom": { + "include": false, + "max_lag": 1 + } + }, + "vas": { + "abs": { + "include": true, + "max_lag": 1 + }, + "anom": { + "include": false, + "max_lag": 1 + } + }, + "land": { + "metadata": true, + "include": true + }, + "circmonth": { + "metadata": true, + "include": true + } + }, + "batch_size": 2, + "shuffle": true, + "n_forecast_months": 6, + "sample_IDs": { + "obs_train_dates": [ + "1980-1-1", + "2011-6-1" + ], + "obs_val_dates": [ + "2012-1-1", + "2017-6-1" + ], + "obs_test_dates": [ + "2018-1-1", + "2019-6-1" + ] + }, + "cmip6_run_dict": { + "EC-Earth3": { + "r2i1p1f1": [ + "1851-1-1", + "2099-6-1" + ], + "r7i1p1f1": [ + "1851-1-1", + "2099-6-1" + ], + "r10i1p1f1": [ + "1851-1-1", + "2099-6-1" + ], + "r12i1p1f1": [ + "1851-1-1", + "2099-6-1" + ], + "r14i1p1f1": [ + "1851-1-1", + "2099-6-1" + ] + }, + "MRI-ESM2-0": { + "r1i1p1f1": [ + "1851-1-1", + "2099-6-1" + ], + "r2i1p1f1": [ + "1851-1-1", + "2029-6-1" + ], + "r3i1p1f1": [ + "1851-1-1", + "2029-6-1" + ], + "r4i1p1f1": [ + "1851-1-1", + "2029-6-1" + ], + "r5i1p1f1": [ + "1851-1-1", + "2029-6-1" + ] + } + }, + "raw_data_shape": [ + 432, + 432 + ], + "default_seed": 42, + "loss_weight_months": true, + "verbose_level": 0 +} diff --git a/tools/forecast/config.py b/tools/forecast/config.py index 346103e..9fa6f12 100644 --- a/tools/forecast/config.py +++ b/tools/forecast/config.py @@ -1,17 +1,16 @@ """ -Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted -to fit the galaxy interface. +Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted +to fit the galaxy interface. """ import os import pandas as pd - ''' Defines globals used throughout the codebase. ''' ############################################################################### -### Folder structure naming system +# Folder structure naming system ############################################################################### data_folder = 'data' @@ -39,7 +38,7 @@ region_mask_filename = 'region_mask.npy' ############################################################################### -### Polar hole/missing months +# Polar hole/missing months ############################################################################### # Pre-defined polar hole radii (in number of 25km x 25km grid cells) @@ -70,7 +69,7 @@ pd.Timestamp('1986-6-1'), pd.Timestamp('1987-12-1')] ############################################################################### -### Weights and biases config (https://docs.wandb.ai/guides/track/advanced/environment-variables) +# Weights and biases config (https://docs.wandb.ai/guides/track/advanced/environment-variables) ############################################################################### # Get API key from https://wandb.ai/authorize @@ -83,7 +82,7 @@ WANDB_CACHE_DIR = '/path/to/wandb/cache/dir' ############################################################################### -### ECMWF details +# ECMWF details ############################################################################### ECMWF_API_KEY = 'YOUR-KEY-HERE' diff --git a/tools/forecast/forecast.py b/tools/forecast/forecast.py index f4fce85..16036a1 100644 --- a/tools/forecast/forecast.py +++ b/tools/forecast/forecast.py @@ -1,11 +1,10 @@ """ -Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted -to fit the galaxy interface. +Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted +to fit the galaxy interface. """ import os import sys import argparse -sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) from utils import IceNetDataLoader import pandas as pd import xarray as xr @@ -13,14 +12,16 @@ from tqdm import tqdm import re from tensorflow.keras.models import load_model -import time import config +sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) parser = argparse.ArgumentParser() parser.add_argument("--config", type=str, help="config file") -parser.add_argument("--models", type=str, help="network models" ) -parser.add_argument("--siconca", type=str, help="siconca netcdf file" ) +parser.add_argument("--models", type=str, help="network models") +parser.add_argument("--siconca", type=str, help="siconca netcdf file") +parser.add_argument("--forecast_start", type=str, help="forecast start date") +parser.add_argument("--forecast_end", type=str, help="forecast end date") args = parser.parse_args() # Load dataloader @@ -28,18 +29,18 @@ dataloader_config_fpath = args.config # Data loader -#print("\nSetting up the data loader with config file: {}\n\n".format(dataloader_ID)) +# print("\nSetting up the data loader with config file: {}\n\n".format(dataloader_ID)) dataloader = IceNetDataLoader(dataloader_config_fpath) print('\n\nDone.\n') -#load networks +# load networks network_regex = re.compile('^network_tempscaled_([0-9]*).h5$') network_fpaths = args.models.split(",") -#ensemble_seeds = [36, 42, 53] +# ensemble_seeds = [36, 42, 53] ensemble_seeds = [network_regex.match(f)[1] for f in - ["network_tempscaled_36.h5", "network_tempscaled_42.h5", "network_tempscaled_53.h5"] if network_regex.match(f)] + ["network_tempscaled_36.h5", "network_tempscaled_42.h5", "network_tempscaled_53.h5"] if network_regex.match(f)] print(ensemble_seeds) networks = [] for network_fpath in network_fpaths: @@ -49,8 +50,8 @@ model = 'IceNet' -forecast_start = pd.Timestamp('2020-01-01') -forecast_end = pd.Timestamp('2020-12-01') +forecast_start = pd.Timestamp(args.forecast_start) +forecast_end = pd.Timestamp(args.forecast_end) n_forecast_months = dataloader.config['n_forecast_months'] @@ -60,17 +61,17 @@ if not os.path.exists(forecast_folder): os.makedirs(forecast_folder) -#load ground truth +# load ground truth print('Loading ground truth SIC... ', end='', flush=True) true_sic_fpath = args.siconca true_sic_da = xr.open_dataarray(true_sic_fpath) print('Done.') -#set up forecast folder +# set up forecast folder # define list of lead times -leadtimes = np.arange(1, n_forecast_months+1) +leadtimes = np.arange(1, n_forecast_months + 1) # add ensemble to the list of models ensemble_seeds_and_mean = ensemble_seeds.copy() @@ -83,7 +84,7 @@ ) all_start_dates = pd.date_range( - start=forecast_start - pd.DateOffset(months=n_forecast_months-1), + start=forecast_start - pd.DateOffset(months=n_forecast_months - 1), end=forecast_end, freq='MS' ) @@ -119,7 +120,7 @@ # Target forecast dates for the forecast beginning at this `start_date` target_dates = pd.date_range( start=start_date, - end=start_date + pd.DateOffset(months=n_forecast_months-1), + end=start_date + pd.DateOffset(months=n_forecast_months - 1), freq='MS' ) @@ -133,12 +134,12 @@ for i, (target_date, leadtime) in enumerate(zip(target_dates, leadtimes)): if target_date in all_target_dates: - model_forecast.\ - loc[:, target_date, :, :, leadtime] = pred[..., i] - + model_forecast.\ + loc[:, target_date, :, :, leadtime] = pred[..., i] + print('Saving forecast NetCDF for {}... '.format(model), end='', flush=True) forecast_fpath = os.path.join(forecast_folder, f'{model.lower()}_forecasts.nc'.format(model.lower())) -model_forecast.to_netcdf(forecast_fpath) #export file as Net +model_forecast.to_netcdf(forecast_fpath) # export file as Net print('Done.') diff --git a/tools/forecast/forecast.xml b/tools/forecast/forecast.xml index 7deb605..1746b7a 100644 --- a/tools/forecast/forecast.xml +++ b/tools/forecast/forecast.xml @@ -1,5 +1,5 @@ - - for regridding and normalizing icenet data + + for forecasting sea ice concentration with IceNet python xarray @@ -16,10 +16,8 @@ - - - - - - + + + + + + - + + - - @misc{https://doi.org/10.5281/zenodo.5176573, - doi = {10.5281/ZENODO.5176573}, - url = {https://zenodo.org/record/5176573}, - author = {Andersson, Tom R.}, - title = {Code associated with the paper: 'Seasonal Arctic sea ice forecasting with probabilistic deep learning'}, - publisher = {Zenodo}, - year = {2021}, - copyright = {Open Access} - } - + 10.5281/zenodo.5176573 + diff --git a/tools/forecast/masks/.listing b/tools/forecast/masks/.listing new file mode 100644 index 0000000..738cb2b --- /dev/null +++ b/tools/forecast/masks/.listing @@ -0,0 +1,32 @@ +drwxr-xr-x 2 ftp ftp 0 May 09 2017 . +drwxr-xr-x 2 ftp ftp 0 May 09 2017 .. +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901021200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901041200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901061200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901081200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901101200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901121200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901141200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901161200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901181200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901201200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901221200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901241200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901261200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901281200.nc +-rwxr-xr-x 1 ftp ftp 9856120 May 09 2017 ice_conc_nh_ease2-250_cdr-v2p0_197901301200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901021200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901041200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901061200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901081200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901101200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901121200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901141200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901161200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901181200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901201200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901221200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901241200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901261200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901281200.nc +-rwxr-xr-x 1 ftp ftp 9856141 Jun 08 2022 ice_conc_sh_ease2-250_cdr-v2p0_197901301200.nc diff --git a/tools/forecast/masks/active_grid_cell_mask_01.npy b/tools/forecast/masks/active_grid_cell_mask_01.npy new file mode 100644 index 0000000000000000000000000000000000000000..75d54122c919772941a48e687c025c9e20a185b0 GIT binary patch literal 186752 zcmeHQxw17mQtokiiuM{J#$mB|06ZpeFtCV$X##^_h;9N0cnV$^WlAY!N~x`ORpntg z%6<91sygT1|HjP!{qFDo_V51TH$VK>5C8qsAAbJzpMUx3uYdgMfBxyufBNl@KmE%e zfBDZ}e*f?P_{U%V@H5!|=J)^h>(9>q>%ac~KYn)lpZ?Wf{>5*9bZ>w5+aLe;$4#$4 z`N3X>fFWQA7y^cXAz%m?0)~JgUJ5CFE|m6=U_A_AVO@E!(~+7qY# z>`a0#seBfbCoe83655l;{^)FipIrGUCeK|vCn(ctaQh-cNZfo1e%vfVk1RRs&2w6N zM8NYvB!LO!NLxS&{Xw?lQUp95u_ZErHqGJ4gGe6~b4)k|0Yq6=_P{4*lAdfK!@GVk zz9fRDX&M#MrfF=P(#@PE9*aN;_Mjz=0STa`#N;#SSOgNr(-I%V_~K}sCr^{a68Kc? zi=vC*9+MA5fT52tOTDLMgT(O6Zt4*TM082upFd=4pJ9w}Kc(0s#d)Qg9tr3 z%T)3Vog>pxp|aJxEU+hRMEf?**5Zw{&_HIlo`|Pj+2jgDr;vsjf>{ zX(!&jd8QVq*kYREBNN7u;?HpL#q1XeZo#2@D*#d1T*4(ZJwt@l6-v$`$Slt+2vr%e zzUG#4OK6+cY}(GXSg>pH!yQAitglyeRv8wwYV?YZfTy}>&8X~I3q`vUUxBtL6H=pH zEyav6kPFPey6Dz|75H{zvRu5KBV)bR&WK(StkubEF;NIv(84OLwS_KBi+c-j3MGay zp$O6qJ|p;8dR^4(RG!K8GEeom_1tKcB|GN$g}EK4uHH_GUd;8HTBb0@%uFZ(7d7ml zD?_BJYRFWBGY95+n^s1#@dE5RR3eu&ZT$4&sPyFmnSP3ofW#16)MyyOzd>WfhV(|| z+g;nAhNGCfxO~guR7mu}d5IYvD$K+`IQHz@%fl@J7-q|ZjN!7vto9bR9GbBDIDoT} zSUW>^yoOJN_^iZdRdxj@@7rlir|hRWtt=r zVE^r!@QoG7qTo(H7~rGpHCDdfg>a3(H3l3n!XwY& z>)0E%vGnCGj$Kc#57*1=`RkRpcwDzFX^Z6aunhjzHfjl%pxQNKtN6SRI?QJ+lc52~ zY;KRv`XM;y2LWJ(8sf27VWVS9KB!s)#=*s7m@Gv*N~@|-=s_p+IH=SE%cuw@V|U_& zO-f3D30qa^Hkk{6K64rCL{~8+M{kt~4J8P$n6V|oXI-z+gdbW>gs&9ZLecx0+azF8 zA(@cu9Z9C`)LzdQ;1mAER^r+LjsfHI@_1RTq2-A?^_SjY3_u4E)1o+on+B3Aq}GiM zywLj^Ix_~w!Yu4+n$CY@LI#?q9y7OAIoVD?7N~*-$Bh4oDhaQ)bjYyH7J%#_U9bog zOYM%Udv~R@Q2p0}ut>zFAJSAnWh(%wAK;lS9asc{`m9(06 z0|J62rif9DF9FAW3L~f#(^$@z6qD{kprlAz;#|g8gyK#?vQyDuK)k!`9=nq(q@hBQ zVh+E-x_jbp_g8r7n!b(Lz1w%msRZ0R^=G?R*zjKkYfiL(7P31RuQ7r%)tugb=h$EE zU1!80Qg?#qtB~Egcbk%?_tv>T+Pq0f$N5pn?p(XWNZWnq;NR@s=A-NVCTRC<-6fI`4(@Y`LNj*);50e8=;BP7hl+_T3wy3R2%82t_CF5hwx{pqmF7x_fPX&S~8 zK7rrGOOB(zA@br~-rjBl19K^F2k_GU=Ahpaed#(cZng#cEjcfS@XFP;K))^L%B|kp zYa8~Pvchjn-izfWn{9%AYtkhfy}QX)=(pq@{694GZesN9w!pqUN1ws0qX=wAep@EZ zWq;Q}NKBsFVBenQ$Fsk~nWTxrw*$X56FOrqjbP^l0$Wkvm?!@DZDLFd4G3(+eJYLp zi6Rs2k+|g|oC1Dhu$zlAnTVz>BY!uC|5xS{qGz0(FIoS)*l*cfVt1bAX48v%$ClB* znWLk4f|dAEiTft@lUw_b`xB_Clyu|OJ(IJ4F}woaS!RS-FaAaJXLokL4${Fp%Sr>5 z^~qyr0|ECmERj3a5{4;zG^R8cAyC40wxz-s4P<8%fwS4s>f=Dt(VWqo2>}V)H5zo! zI;DY3Y9PQ-%OT{xgJq+u!Aac}SqLFfzh0;hX&yo#u~J%+?;43Fu(O7M;2{JF+qFt< zOtX6g1RDtyVZKsHBbbySfPdj4xJUuQe5K+>u)n-XJTQrVzY7S(SM*2A)q)!)4q-w< z!0~`YfdwIQZxX7jnM(cAlqL}Z0UcC{FYzc!@Lev^jCMu@fCH!aXHR?r@f+-NmQuqw zr3r*U;0sm35(TgiwFKlVyV4f&|h#9XFzs!xc zyT(^!Bi?)d7L4|*1pQg9t??yW@c|Y;yd+;&v6T84U)pt8etr>Ri7)@%gzV>^I488x z2L7+DHEF@lX!o8iQFi9W~kqJ~VCy@iDDCajLIgUGhH#Ucf0s+rh#5?)W#BV=%O2R9~ zo*~2zD17HFVA)<6?-mG2$om8&28`yAOU#7 z3ZS?blVjm?1lG?r@Y)yN8Jf*?LtKbqStO#zE9^ET@A0Q>(wzN8PJv@xfRxR4!DrEacwYhJMLcxbR5rmvcG@?T$?bTw_ ztdO_A_dQ!Tsvh~u-Z6><7w5zn?afg|fkF>Fv1wLMHhrCzZ7WoDSrL&GL09X!O7!Viq_L)N6pMX@A#9v(95kd5?35JD=6U6-j}Kp4Mstsoof9k&gC6&5M%06X2O4CR zQ#2SOHhhsV1QxrE00hbjFa$O7MZA&QpBz0ZaPk=jc+=5CERy4v4v5E;fP*um2kqYG z!~_zFbV8+TUTKE6NL}30qLKpupAmpzP|U|iA*BOJNrTYyU#Z_)q&{YGQx11DN&%{& zg-+?_TEFoD5xn6$8Px$a4T1SpF~$WhShcRnD(5ATQZ^4jks zkc15}RBWq|AnuxQ4?#}JtcV;LV{?q_B*tD@X@d{|$8|%bU)CewOlfzoH0q^Ls?i8O z)|IzjS8wCu_JROdF9nhjE#L~TJT7GfDZ9%G|2c-ND^0uT*}({T!qqB)X>At(iW+yyES0+{mI_@M$(L7B z;m@`yBuU<)g-_T9>2kf?1aSx&X3=v3A0<@4p!kuYDpYF1gmI8C?9YaP4l5GaM+Pp< z_KRPoS|s`zRy^L3?NWLjw5piq)1|vIujuNpvD!>B^f8)Tv!=Ombr` zYKeWD8=xGnGO!q9F{W^KWz`-+0t#P5Y_8@DpcDlwAn|H?$2i27pPTg-Q#1R6#mT1- z;)3fCfPOc*Ir+rd0fAKzX=6&sm#fE9;U0unu=!zFPj7hq`@{u}yOw;x&QOKFI8aIE z5p6l;?2sZizDD3Hd}QIo`Pl8Dp5Psa1Ez&&c*DtA>fwB0C)S`Ty1CT&jIa77^#mfu zW>9coTShS!qj)fo>`uCLLmcx+G1ln`GOefaQCO^;(K+(2V1oGE{#YG`jW1##ZI5&F zbz&^8m^2>alDI?4qe#fCwpO{jiV(suRtPe# zp%s}eLq52Vh3Z9?))Im=`vYyXPZ*60X% zHQ&Q*xKNsj@0YAmV(w^D1QwQwN^ofc!m_+ADikjB%61s0nxAKM(8y;&T2LFd?F|_z zsF1XpicSBzS5)g5ps*66^n)U+dND z`k_TVs6W8+5q|mF8g*#UQ%kGaJLN{)P`VOV>-s9Y-@Ud{1({&3W!3D>YE>GtEGV9t zP^~q6mEA9%tpwjNf=L4=+Oo8+8WR-(lNEbug2>WJ19yj5JAf7!TYxbb$giCur>hKn z&D5HdHt3{VyECjGK#L;?VI8p1X5?2+3K>3?8f0Rf$y7ZtHunw>v3j6i9JU~i(1L5V zZiyH^3P6r9)8T%>37D-Vma(HltRLrb@d)^B(85F=x!KM2DqzJ*b+_WlGG<3vH$Gwk zEAab^R*C_cK>qPFE0EMsA{hoG%!zSM19^OwMI+;b$(+pbgyigVn279LaiySMxXE*GMW_DJ zO@_CganUF^$Z7<|7!an}s`4JnLSu~a@1j{vDTaM3Phqbb4m~?!J9~Q}8Li4Nl=FAv z8a0eB4IID~P@O|Zt0Zh#PFQA@xq;S;mUs+b*ly7ceD%vi-CeKs7?_-R8#;ln$zFXj zpzfa6^gs(H06xyi z(;z4Ydu#}E9b6XmS?Jt4^C(8Upcrj3AGa)D_~|ka>KH;Bi-&Ri$KF_{=_veGIluQK zGTgG0bBw_=Su_uKQ;qf69F=!mMxSJC44}Q+k9FA*wRgZf^QRaa!)VVsaKn#v*%7s8 zz}s_S@6jQ~FdD6v$Ke=bE%rs_jTh60`5HuN>-zq(8EmpC3@002ck3Rf=fMa3K$Ag; z1}c>y{@w!nU>7Ycl+|2lLqA9%aM>emQ(a;7J6p9t&oSBnXVuJA>e#9iHUJ-n zY~6kXU+q?I8f6`rv7ProZviasfU8uO!JPiv)@?nC{rW6oTdVcx3MVhKbK}uYlW{L(_rMeD_YKT;G`ozAC!li4vg-@t1 zxUt^ZX@2o=H$gLw1Zvp?VL=|&khA1*)zuWEGgg;m%xW5T5?6`zw?skYiJUkg}`FNA@XDQVzz zQWTzJ4=a(v6=Ww-Wc4CG6>ag-qo$W()p=u8;OT|Afk z+}4qKsTrBd&g9%3OEde+%)P^Z@|QAX9-`|?qBDh9|NM=rUSYthH|bbq-A?e6+-Yz> zm3u%W6G{2NWbd4L8tR>#X}8*>X)IbaqaWsE!^g>(jl}2+0@*ufp62vU#;jZEFl0)7 zhQc2cl;#c}z1$#VF|zQ~6?*3RTM=HHhqJkUKZ%d=O;dDd5H^drgI3thm z%CuECa4vw#?m9!uT{Z4V=1|4{+{k3`Gqor7%JuSff2_Jwz)N#yjeF7)F2IJHAsIji z3%*bhKCWbG5KLJ-Y@h5)e*K+e7-82y;ek>P(b5Q)@Ifzan(x{es1Agn6>wBP+2~e+ z3|0#6&4XUWEFU+huqI4YH#4S4Co%g$q9|_)AfO@3kEo6Z7bNaXauJp@ivcMfB_(Oj z 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 literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_03.npy b/tools/forecast/masks/active_grid_cell_mask_03.npy new file mode 100644 index 0000000000000000000000000000000000000000..7b1f2554487fe3335c640632e4280ef5ccd5e7c6 GIT binary patch literal 186752 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 literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_04.npy b/tools/forecast/masks/active_grid_cell_mask_04.npy new file mode 100644 index 0000000000000000000000000000000000000000..c233eb6e55063d72b2299d6d01b8e1407380968f GIT binary patch literal 186752 zcmeHQyRI}zQl52ritz@db+}kO03ItG3@o&;gD?<_W)KeW6udAQkr^2gnUS}uuIjo( zYo{~rm+z~p?sNVl?f?Dm@BjAi{^2)2{MQfv{nHCb=q?T&q_%Ad23kNCa_~D1bfZyf|TY|~I z=bwMQ6ilERD1$-BMRK;JDT6;rew>8>and-EGcgN>Njink#lsP3u%ygti0Ju&Z$IUA zmv2S|_0`w?Jdl_Vioj)qbUlA{W{t2+_Mz$#&tExJR~H+jFkcCyk;S1d(koh!)0 z4p=~tm1%7DcgGR}ks2+ME5IFp>({zbs*Gf4@RY+>rg~DWx`5+i zJ1*V~CkX;7GEJVqbdNMq`9)#}ybUPS)kKln*|T6`FJQ-2C5F0>7_jJGzgvFDsqmF> zRL-tp%oi>o8$4M?ta^_d2v`IFYtkEB3p$@Xb7#n%2-F=Ot058-cq{>+;H@*2d#r>4 zPSRD}!d25p7{XNv7v=ylcDI)1h+3+1>X?pYe!9BR z(<}fJ?9_p)a#+Ojgm$>{+?gRgJJNOR?WG$g&Abn4BSuHFS*9}9C04Sla+fpg z__EQ0rnFl-moY6JbEDHLW7uRRaUB|ww=|vb=@maA6*>~so(xIJukr^<`X@Y$+K@h| z{#7^d)kEwfzH-CqqL3Ja%MvR(R9K0>aO}~ySA<&wFwIs38N+pjRqZKk1vFvxc>q@< zv37y(e2th0@kNQ(s^SVp823|GR}kRNQ^h1XSpvW)A~*@5HAt)I`ex;BRq(*TnJ#Gr z@?V$QV5LLDg7#0D`n`E4tKiWUK+@rHU%mNbxLv2isXECAT?Y| z*c$oKv{J>2Z5DP%g@A>hVXN|l!3f7^oN2d=qLzyz1S|lBEu;@VU{Y}TeFOW7LLG55 zqWc+v4vgsH$d;7DbSVngR#T%;KPM0{5oF`A0^fa{6r_}=Y)xg@!~+O{CWhpoz}9I| z!?9FlaKiUQPI9S@zyQb*j_l_`x*6LCl1_8YMu0#BZ~7NoGR4zQ%T{q;*zvkrN6RH$ z=pY~f3{gVMVkfzBSr4^P$gG4W7qP?r1dOyV+-do4BjX27Y;{QG4-9wh5=;m4 zr6%S8Wp?E&2uL;(VdWg3`JPc$b^QYf#OKrp3VDe$5OBKAK>2e$)C18UgP#NTkV=WI zaA0}o>kyPbQWPm-zMQa+pn6mhAWHrEDKU3cKd-wB0dZexaaUDOJ_rGL_-UT9c2Gwz zyAuKPX&71CS=rN%LLlE%g>Y0que%e0f~TzREbHmVA&@yz!g2k)@=gR4Pps^$>gk6e zz#J(__mjWV*B;kSMnJJ76g7tPrQrCUA_%HQ9xM1*ap_wK)D&q;p3C{lP<&I+>@+kO z5brL($G*uG($JvDFsI*OeS6|}_cwSMn%<4rcefvs(+K$P)UWN{U?YANY&p^XTFAb+ zc#9F7spX9JZ;t)e-fcz#B5fyl-U``Q_wG~D^?r5kmp1PbGH|{WvTv?EV5IN<=HTz_ z-REQIeJ5z&+j>aIFyMQ$zo;{OvBPh7Lpo*xUIg5CR-GYXE#^CWyrb(H6O+;JfbR4y zC(&OHJAIK?G+d@(KH(MkoxJ2c`Wqro-sS1-HZZW3@^k=C-ER&0Ezzg0^WbJ%u-}sN zUW?Y@?_z`i|a z_qZ=FupRkrnFP20T?Zktcy5DzdsZCJ{|;x7CJNsU{MJn9jJY&|p9=_VMSWwQ`s25S zF+DUPuo3sAH1Q{jLi9)CmWyx+_>I9~F3J=lnzoGm-khob6AXfS$0_;JW!{VZmd!PG z$7x|UyLfbL8U39(Hi`#Wi7%75cVfS|b^N$LfSN|hFkaiUIQtvJ8{i#fMu_d=Z$y7} z=kV(w8@!{eG+4P| z3jxJL3R1RnmDZSUj|eC>7!+kbQ{fR@DiFZGa8X=vfHI${gc1BNZJf#bSK;jEk#S#UG4>bnzEGZA+LP8)QpRo{Pej=X5jM*no<^mxg_>y$d z1JZ=?EF;b6CnF&FVk=3RneRLmX$(IJ0U(5A<0QUUI;idUHizV*kd|B6F{rN?RF<<|klop^&(Ut|N)DYm1Q}K&USlTWBO0 z0i7fQY%CruDORAfzYqa{-GqYp5OfKVyhokoNcN0n)bn-3JFgkEET>y3=@17Z#-H)V zbGRfBKpp}UB^j9-ip9V{*IA5FUq2NB`Bn+mEE6~J6b_(OMIEQ8DP1812=J0Fvg1>b zmGk(LPi7j{Py22WH<1tpYsZUDjfsE>e26sdCmPLvFfccGn=z1}=>Y)r*NSM+QO9!k zHWH@y1+I_|!k;sRg~U@%xVL!1CgyNaArKkMc(V^p z^7ezLB)oF$5kl^O!h7Blmi>kCZh;U+J|-YBU`+X<8iyVsrS`FOYW|8NgW20Q{;~qu z3Smiu1mFoPfD&Fp&V`Q=M8D7=YHxTKXm;04aVbV*k%*qCh})37pFeez?(7e8Dja)z zg+M?khd^SwpOOnK`lTV&zFk_JLYByLJy?+YM!CBz&|?LJR4`)&e9v0l;6s);hA0*d zPktB6N^yE+zSTlHWW^(yWyw#=G@`0Y#>bv&TUKyi^EOgQ#Dru*j=&OhcuUn{sib`F z$aAQY+4@*Ptt*uN!jY2{9z!`d-ejorqm$hqk@9#&wQPT?Q0$x4?!Gw+g%}z{5K1P| zhz^~!SIbR{Lec)*_hQ|wM&uiN=O_|FTo7|~G-nkB3jN@TO^bTE>EpCwTcw)Ist8sT zUFqXX^%GC*FSAtMs&fSNpPKw}Dw)??pOt7jeq=;G2$OVTk*(nT}{fAB$+U=yNaVhqFV z1K+6E6VCx`<;sYB%n7c{0)(MUoY9?V%aMbAA9BMyk9>{sk!$N{=?QXhf(2sG6MoHz zdQ$K}gRXLj24m!g4-%%p61Ne6KsgD9q-MT|H*&{QqDKV|KGOhCI!1_Pa>CLH@tl%y z2xj!4J=%iEAdyHXR66IyGdx8a;>L?=4gh>X0ER)a7@q}B2a;HW(Bog+?tAN&{w$1%n&o|{D}Y_xEP2X_mak-&^}?|67^D#BtQa`#UteyXpK96AR-PQ zhl%pN-)A668)B&1)*wOMHR&FLoSIn`IWxxQn9xa$eP^W)LINDuO_6cgh(Iv$?osK~ z<4~&63I43BY<*w7k4xAK0$@E3BqLhDm0o2WXC$e(%PRjdhN3G?d((4(5%PqqH3E6< z5CDo9C#?|E3^cQM8=>7pUL!03X;`%tSHfV%8TsvsW`$kGa>hIBGeESkjU=k=?Q$4` zE|Qx3+XZavLJ?R&4SH9_MnYS|ZrVT^n#Ja1FV7f)R*o)%*7ep{btTwes?#8^TYrMuE@<4f<_mU) zD)J?PVp&G?WzN|lWo}}P#8>*r!imeV`$Jv9I}Zm;3(@d~lZ(`k^M##6gRU6nGUE%r z+LzQ7h?twfz(H)8#axWy!9a32>C#Pc!lT7pXC&yfktRnGv3kbfD7%6Q;!FEwbr?3k zhymUn=a%c_SX_}c855GEL(8*B=&ZihxVx$mLlv_$#ca3`&m{L_YnFsN+YCX36`~Pbn}Dz)Z;J|r>%6)hhMDH;83Q!>S-=Zg z!}h%?gM%u`s%hBluX|OsfdL8|Ax1w4v&^@!#T6T)=KULfH0Pj7=w-1FFQO`U+0dG$ z_%fTqxc%*<-nEucvv-2T-o0UGS=ME8tOO`**@G?3nOpZ(&Ovt+;{J`v0f+`bU~Xg? z=1?YBJEf*vP`D9DcIv$_8>QUhOzSshZGc1OMpoNQi5pKW+{xv}Po*nfmb|ByI?$j? zM~}5py`djkw1dV2+&&^M-&&&$4SHH>E&HI{j2lWf;#yrFWskeJR%#%V%&n}Jy<4qG zLy-l=3lplXrjN46#fz2T8%8o&z+_vIHdJ$>Az-p$uT2nHTWR3o5cdwC#nl#IOa}5> zr^*>B6JIl}CbbPZ+1B9<_Ya`O5rni3*l08I8z+TKpGFNbiOytdo)}wtr-!(EU|bxw zAdb+2YxQo496k#`iLlV&e!&TZttZy8vqRiJF5(gqh})n=h&FPoo7+{uij~@K!&79e zj&k4lj0LP9?=M;@2NVMN=g*=5tEmJVCM3+saX|xlewLd?=7n+Bys%hZL`ZSNzA|0Q zxtKP0wVV@#94L)Xlf%WB_XGj=j!PyL6&1tEIvSi?OSxL@YFcuRSc7d(lp++gF4Bi< zqCGD>0a%Xh2+cC!NO|#^=|&1CHOr>30ofjT=0#D+e6bn!XH7}LzJ`g)E|pLU>ZO~! z^fq+rpWS45>X{eKLV&DAP>um%nXM-8sVp?cnE0-mwUlbur}C8cYT+=lBetuz7n0Gd z3_}I~Fs@U>`Le(PTmjWJbgW9shUJuHRaqP8e$g6_=?gn7x`D58d8oVVbw36cC*Fom z;%l+jm<*`9=e0c04HE#LWt0HdwO!}h?Br{4rnL<&7QJ{~ z@LT2j-j~P-%P!6_2hU>BGCWK**JpE7+3_~|B4cv^?cIK^%Z{kM1Aa4qiLp71_N)Up z{9KnEQF{jbdM@leJH#ADv(?Ht9AmD693{I%S(KyJ#~U9CpfmtZ@Ncu|0Gb z0d}?vOnA7=gw(IsEznQByfje~3;|{n=a~beXytb|yQJiRyLWX@>i`LcU!*l1t`kOmQ--BkSA+4d&_8)vkrQWhkv(+OMz2$rTV5 zUv$r@z?QOS>sHO$zN5>1=?Xl+-s-Zp`HGP>Y_(+p$m+RUS8G`ox>TWe<_ihaprmgd z)tVtx2ijVlmn`wp$nKb(RlH2>iiTCBEsLSta?W)UV0McLV?qNr!(=2v3wTMF%p||G z^<-IRQKqtsX%`9)D8m2BmoZ@(V(Ug?Fr`?$omsUjjI4Htu~pX{$6qdjuJ~~3T|g_R zts_iJnTEO~8L z;6(~a4O#eFDn0XJu666SGRJl`&jtlLY)HFIN*ccdlo+)WRk(6eVc)6dvYQ1MU~}>n z64U%0AbHA8kaDeOsPvY?Wp_wad`F4zriKN=0O7ypgA3=QJ*Q6EXexLzh-&UVQ@g!o zzLpR3=jILUz~691wzYFqPDf8@x!@-Ryfi;i=sPtKE?^_HSJ3g-25>5uP}0!CLi8)s z4&^(rT(&DAnc1=?rJ(BMVCBlily#HB9b7^L5{f})NkcQdyJQ_KLZvn_q>}U`t_1`X zKAb?5QSq^ zghED+uD`5>ipx1SNT`A0p=+PCa2!a|(~xt5_M=&wP|M+4gjjD&=EZY*Kp8$Zm3y&A zOJ!Wt%NV>Fz{tegiIdh?e=_(^VH!Bp^7w|2O*twH>|-fAn?i`yK1$ZsH7(Xy%ehH3 z!6QzdTo?XAP?C{%EsdeC_N7K8BSP2Wm*!h&3-d07-!+tJfel#G()G8)d9Od_ z{t1Z~tVBwA?r(E&h1by4zNZ9d2ZQ~A@^RP?6uLnPK}E zVX15_cy6AYILhbtKfPPMFj-C?Wm7|(R(#XlROCiU#7=$QW(hg^ZHC@Ds9aXdTPvQQ zC+0rC`Nw@Jgqikxb z=7$P*8h-!AY5h%wzLO*g`_2SscaxNg``xpbh==Hws@3%2=h3Eaof8WHCu=j0$zl|kd{t0#i{v=^H z829xfO1}ICyZ+_Cd|iK5pD_N0BXZ_w+ya4ogWY0UA2RS2K#1>-#tjhgZy7fr$wzbq z905nb5pVd^_zMa+p literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_05.npy b/tools/forecast/masks/active_grid_cell_mask_05.npy new file mode 100644 index 0000000000000000000000000000000000000000..afb4ceb3d125e9660689fb979c68266c8abb53a6 GIT binary patch literal 186752 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 literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_06.npy b/tools/forecast/masks/active_grid_cell_mask_06.npy new file mode 100644 index 0000000000000000000000000000000000000000..78c5641cf384833181152b5a4a711ddb8aa4d49b GIT binary patch literal 186752 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 literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_07.npy b/tools/forecast/masks/active_grid_cell_mask_07.npy new file mode 100644 index 0000000000000000000000000000000000000000..7f322d34e83f4c08aeda51088efce84e0815e1bb GIT binary patch literal 186752 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 literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_08.npy b/tools/forecast/masks/active_grid_cell_mask_08.npy new file mode 100644 index 0000000000000000000000000000000000000000..137838ede04f9e20340a687ea382844b7a589f84 GIT binary patch literal 186752 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 UAOHd&00JNY0w4eaAfO2RA8|Zyw*UYD literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_09.npy b/tools/forecast/masks/active_grid_cell_mask_09.npy new file mode 100644 index 0000000000000000000000000000000000000000..621c997581ff6881d5b590529819c11e2a3c01b9 GIT binary patch literal 186752 zcmeI3&ywUg5r=24JjL7wBKE?c6A!?R4K5rw5eqv62ZAML2`<1>@WLolN-1SacG=x_ zxvZ~ZX{9n#Qs(z7xoUfx|9$)W-~R3oA3yx-!@oa&|K-O&fBpRHPoMwur=NfR=F{iD z{P6XkU%&hNAAk7z{TK27=DWZB_$By%{OfoB_!8zn{namj@y)03<7eM|`tPS*ub+J2 z1px>^00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz z00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_< z0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$## zAOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1g=NmM9^TB>xbL9Q;ED+ZEWS1gU1ngsatYGS#n6S=OcOuMe?fyfv8Hl)jVlUiZWlF;ii z%FGH;iHcqbHY0Q;hY0oIgOe+nSE`X(GJ&~J=#}AI&ya^-N3!b`OGQGU`813g0_dUy zI#(=y^|)xwLzjhMFDHLnvX^UPoPyhe@l3qzJK!O~`0rzv(XXRdeZ$b}B6{`IQh%v2 zOv?o8Sp*i<@)~*jS;ca0@0T34jKWE87kBL0Z<$({#aa3d*0U=f`}{?PI>E7tJ-7Rg zB&+V{Rz2+LJzW$?FR1=ukv+5Vo+9gV@#X#ry7lDJuYvubM4nJBM=ndU7zN(>1QrEZ z37%i=@F%V)vK$_s-su3A^;nIbUhm)st_rf6FgU<-+LteO4}ASw6)Ps3Q}e*5JgCEl zw1HtB-uea^9#1{I>M;*^T!3wvW70gj>unlr%09aE5%=FDz}DmufgafNR{Wdu53GH> ztvA!(ykI=0$8@|I{LTf(R6pFVJL&IOHXPbR8r}hZ?~?QzlSA4VXVbm(cdZ)->tUVl z;=XfH{OiQSS{Y&Eo#b~e8-eJNJ?_N5dztmZ-*p&?jpJ_eI~T_C_&c19GFkdg>Tg&k zC+l)6fpG(YH-O)>wEDkIY$%h|L|_l}t!vVKl4Z{1k+|ngxRw2${7{!IbEdlXlsxX5 zkC~(KJZAKI)hCS0eoyB$yW?h|w)W;Ru&4Af*R&}f;5EIP!i~v(W7qNH{s3t@TZZ!b zk&U&FnBIYRlv@$EHy@Gy)}HGx)@_dO>a_yrn_7f%fk1c~&dD9>NyW_1Sg4Ya5tw5; z+SAd?3SwL&aI`wD{k5PvuvjC(LZHTW&P-09oKyWS5g3I)&_RU~k0J^`YE)+{Rn99_Vi|!%N2jJ& z^(ZR%PPeQ^#!Nso2$T5i$xk4E!A@7H9GI)bFak+0omMJYg8NV_AkVUL5g3U;AfH({ zGd~f}Ql@!Mt_;HnIK8}F_JC|+Jj-b{G8O_}ui`Z;EAyPEqLsmD1Vj+C8>8tJLS~*h zHYys|6?BU1=Ln*NMH$H`v-7NZWdnQUVuk-T?^pFl93kyVdd2t6z#7e37tLnQ>N2UO zl5*a)@+UQl8`;&?%K?i*2_ok_&m38sR_`snvKsmB#VeToXBq8hb*qh$e6tU*{NYvW zb(Tuy9Q10v59gm>My%)+znhr<_)+VWV_F3d?3;Z(tfVk70h|6)1&^H62iuHMn71e- zEn?_MVcL7Uk|e=R2RC~(5=NnuOu<&ngRM#xIhmId5bRA9riYWunHD{2oR-wCahi4i zx$@3!hFP}LRZ4To6I1ig_)+S>as*U|pu|*+ERE%AVvy@B#q3>B6KR?Rn&3lb(|@9~`5z3L z9gOA-BGmE-fck4ibkteLYB!n)5BDotsT{_?taKhTPZ{NImnyI-0+~#b!Uatnab01;wj`~T!j&5N>A}RwJdr+ zD;*y!29(?^1xJd&f4@F6CnP1L6j$vF5W)#L*QFe;qx z8VLl8B@Ahr^)lTo97j*jHaK`p1&lBl$t+H2IuW;!2@pm#)t|KS?`@Z71|8h>DE`XIGG(I*`^kYSm96uE>!s(^*EFo>T)jB*BpNe^eh<;uBs5(aa_oRZ z$5Qa5pc1%=l`&dBq2Vd@={$-c3uLtpX-$JxKHx|r79Wd=_FaD#im5|&RNXd2sBTTV zL$YSBY?WCKJHQGjRb}6EBH$8mZ8w38%|-&6Vco|`%4!XkXk#nqAfFw1=TXpw{?g!X zgE^J0Bak)JK;y^cmNhbta7|8i1>eh5cB+^5)j?T$4hb@8=6SE=bQ7Da-|Up*ZvTpu zgtWBUV>ZRm%xkRg@7UboP0neJ_na3*J+@KAs{69YTbiN{v%32GWo!0^O{9gY(ECja zt14`Ejj%LEbsSZ_~2n@R`3;a8-t{xeC1S zWClV%dWu(Dt3QrP&{=B;;#*yW-}9QU3g=BXiY`~k{q<4?+Offi?bn@KtQsAsmg#n= zmOVC^X`@%(t+fY0a~4{Uv3gnb)j zKVIO4GZQkYP)zxu)h2)P)eX3qGwYninOn7LhPt$}Y2rk$Sam+tWoO?ljL=x_!VZ^_{tWZAIIRuDZ3HHZ$tHh1qQ7lB0U(Adn2`VXh>_hU*DJeUuq>M&5Wg0B^KDtR_<`?Ek!-)NNdJzI0K;rJKYPiET z!Mm{4Lm}C?lW2ZBdqZuuiHLU z>&h@$C7VxNI_({LD}QW!(vp0`NUj!`?3*lGo#urNLl-yvbC+b!y;ksQ5wD$KjaOZW zGdV2Y8ml#{lLx+Lwho#5?-ZX zlhkW&*tIU9#%l+R#xBW~H|wS3glI^bcjaZ?d1pHTFbbYWW>Fj4Y1HfMS>CO7+boBZ zq5k5uG%LSE?rKTL`b5zNYaE5f#M%a9vbdh6t>n_w0iDebiaF-aE5c&)-F%dmvtGG> z{dIboRaW{s_c)ho^>h-pi3O-pMVlvRhDCC^IqpAN%kmC2duiOPmcAJlc9erPC;qg| z4*3|K72C63)PUKKPYq^OrCC4yev!!=yUC+q*`Zf;Z<^Z&sI`BAa-CQTTUc_lP?JE@DO~lRjRcyTIj8LF zUrXEjJhPYbZy2+-*GJ*H4S?)fi_toz)@%8MxIX0Q)^YFU6}OAF5O#d}Wo`q2&NfrG zmvnXa>04&&dua85%O_0n*}Q(=t9NrRw%4QM<^x7tAaB_OG#mCd7JqQ6e+QdKb;`%@ zIAhc$Q5Vtf{RnK+>;29heW$&j5?k~do$(d=@9UMSD+yO9ckmkG$6k@dS(!}oyJ`?K+1##pef%q?Q z6PawfxRT)61mfM2Q%#_HJi8vwTO+XeEBUp>#GgW-zEf`YlxjG1g@6;NHdodWeg=Vs z2jVlz;mkDx^__CFwS~l=Lcl3hn@_2RLpucYR~izlMJlt-I^54Au=J~q=T*hY9Rl&c zYnB`3t%|ln?ROy;S1G-%EU72Fwa`jS>5ojrAF~$oLs06!UeUt3xXlK2{sH*!3$%_mMl4K=jG(3 zGW}N!CnZ^yrSGrIsy@>b{O`N(fB%O+zJK@ccmMhL!P`~3HBUqbxb-#-8IOYndGn_vIx(}(cm7oR@-_rs>w&)(q$ z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4ea zAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd& z00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY z0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JNY0w4eaAOHd&00JN|IDz+r z&wros-@Dg+`X01Xr&e$V#c-Rk-@hvqyG`fAHVVP+CB5oJZR3gf^5Vr$EmDo-f?%&N zSnQCrqSwp!iv}Us9p3}-_QDNz7ww?eU6hhOqhjLW`tk%=u$S)=4JHEVi3lEdA<%mu zhPAtlF-o85{;KN2Yoma;$(*8>3YPDwv~-g}25D3DqH!m`xu?ZJINW7S(JLxrJT>n! z$q;?2Ds5v34!0Pxt&v~3#V`Xj=~and7<&>P0L2YPoXDbrS;cq|eCfDe&yHN7Ia$5F zPjnCoh$6)`;Ym@zdL05$67I)nCjHSLyzqxFq45&eEJxPMHI@@x7|taGtQw8SAHHW? zqRf&*S+OOF$aw_1KGu`>j!Ps1qM%uXy{ML_PU@_JQ2ICq9X zDU|p#+pv5c0g=dGuj>aV&k*oJsW-C=%a;*QiM-+avA@&zEAUPx;AP65YXf>Y9M?Hn zpNf^+3X9o+brpe%MlPg%K`%vdrQyVBIn*HEUAV`t)QV+jX*7+e-(X$c@!022WtoqR zP3+p~=hdlaT-)`qr`N?uA5*PGjt`6M%E4QTa8Iqp7{9XZQO|BG5{R@HX=PMom(Jan zq`P`)--90B6=Y^{P-It*J)=l}`O3z}JiD*Q?CO}%uA6#RklBRmdLQUy`eMi5?q=m! zk}wdsYeron!n&Mm<{0DDbxJJE9s}L!Q!b>xI_>m9hB$Dw49hnR!SCcDm($;ndGahz zKW>8p>$W@{;HmSiL%${a)Nvks*cSF%_BrZR|JgO209AF3VFU z+k}4Wrc)+*_90u*Z`l|BcH*;n9z5L^?A!MoJj~ktwvuiLP1-xb@# z-@{oclcaA4zjY^_%zLE;Tvrg-N`2!#_m|&Rw29P2U?cZc+st24G$tO2Ti%4Lz;8?r zeaTK^a%fA-la9j{g|Uv^yfTPFAHj`Koq_2O}1OY39qsZ%^4Dtc9g8hO+=N`uq-vVv=eKzJHX$sHO= z%}meebcrP)FvWIsq@kBIglm_;(dyLdb3=Zi+ane;0gvq*k4`U6$_>Hl5U|urgm!Ib z8*+6tX)mb?DMkJBjdqh*5dvA2X(X?mV@U_Nwg@;KQj&|E>$KWLJQ8qfWGF6trjuKM zRS@96;o`K&2`+r56I#IEyvaHkOMbsgg!HBB8adkt&1e%aGJ&823q=t|6n=PAXDgM@ z%U!|}0*Ma0qL+IVWqhYw(j%@)fHVkBeD>%Q$lhS5tCSDaE@23Pq!+tRB}p(JY8m8N zmNo$+5eVd!g)#Msc$PBNbF?!ULcr){<&p zO-3ipCP8;Iv8I%A-fd-1sx&upsIHd*7KI{2CU~AXvNpBZYkH{~dhf+sP~&GA^=EZ$ zjFDoK4>0}k@_L=6Qa%TI)!v8G&o3dC^@`t3B!2vr`=n!P4G$cfd_8nh2$+D4|8a{) zI{sksa3$t73Q3E&bfhr#+NLE+Fz(=Hi$=mobdpKfvU#xORH2jcVgiJ{ibC`-av4)r zkGhsE)q5aD;#WFXfUrKLxXMo?iCJEY0Ftzgx^rWK5&l@g}6H5~zX? znbr7-!Ro&-s7^3iFo@vgAprhsMGW|?W3?MigvIlcRxF3`&pS;-%u}v(Z|5#xIRZ*1 zNnsX6915vj-i)%a-!-0Q`he*OM8%YMUgMCx{pcx4FU>u|&=c4g=SO1MADHhJO3CEo z1{Q{bX}_tKQ;)FH@v(F|e2k0G>`fbgS;1Y2*>nz0R2GX}=^X~s79&)n=eHSDM4cu8Sbb7x07b7dts z!qpDW^Hy&oM*A}IV5Z&Sl5RaoJoK7R9l$C`#)-{?P6Ct{WKbe;AGv8I2RN7#WAxac>8K$tCCNB+ zmL>U|X5|u@!g^SvETbl6$Bi+KcOUpe&KlubU@S*w@-ZhYsS1?FOXeDvlWdw<)IZ3j ze#UrB0NSzrqFo7gSfmK$@JjeKBm2qW3kQ6aLp*9j4}7#y!DntGf*=_QLvmFw(M^`) z)zXs!2VYYI55G8wDoz+W5!dy^A%uB_<1rS@3`tD6Ql)cVxrZlPj%6OyYe4Xg0Gvh} zW4;=BJBgG%NsdmIS95kNd3n6m*0cgmkWYS^HJP^|7w6gGCmTb+ysaR&9iKJ;R9eH)bIM4L}rb?5c!AnGo%iTC_xk8;KH^vsd&c zTW)bp<2QP3z>#ocS34{lH(t4LXKz0EsdB})WuDVp9W=b;v**3Jdb8_vL_2A|f!k-q z;ag+0(eO%ZTPr?OZpkv0ZsfI2eN;To-WsVvCWTwOTJdhRrVPa{Dqe)xZ5{fkcpSVK ziN0Z^Pz{X6ifyy5Zmbx*xe}j-D03Q>hG(0&cY+M=rl4R7$a?MS_RKm9zGl`Ana1d( zdUUpj`zOfY3?Yp}F_}!(D_2Qbe67@wkU3dG%`4}YtBaesdtw}%rVx!d!ejMr2~A%L zpd@U3x?i{h6YH7N+@($2KQD5L1mZLtF=n0I+R5uxD9V-E%axU4$J$ozo3AWr1bctU zNSaU>vcCLU6v%EWkpl}X-lKWJBYSx-H_g<6c~%|RjBYZlxnW+r-P>Bshr6Ec35F)x z;4^I%EL^!}eRC_R$T_yO zmAmIW;UfYUfSIylnMk{nM>6Ly~lhbj!HfeTMKQ~TR0?Q0$o8R6`2cIcUWkL%7 zsHNs(YO5T}DG#@r1ZoS!m8h;`Qmf=*T+T(DV^4Dn*|90lwrS?(8#@DuRzR^qule#+ zxBapLG_q{Uv+V|edYHKKKP#EktK}%wZNB_j)ij^-Xs z*BO;QBqW7`A54kjx(@-NyYAXKTMLC8=+yzWWhs8_DPn7+ayF!P?=Znd5)R~JoC^5WGjz}!wCe$Do1)oYYpCV*7oB6Bxm<0OPFfp~9G zMA-VP&0TM&TD+0$^%bzAB+&d)_;$cauBg-7%axsv>nqs8cq5TqEfj)YZ={*GXRhAL z8QUyVZCe5Cst{;C5G%Q>LPjopa;+lQPqti*-QP67YD%#=zXKVI2$W!xzi4kKX;QY6 zi=}x2^o)vM=O<)2FkfM5#!hd5rN=!uucOzVQ~W z38hNTkbxn_{${I*Nod5|%hq7S1o}LQtS`)>Dz}|XsJ$!Q)yt`lb{>2{j+Fha}3qr0V=^jvf z@8|0QEuv&5fcq=6xP^uL>lQNQCP}dD!Fq85kn6<-D%yiy1&|kaq1THGY6-KE9@4UN=%zD+H_Y*BxWz&hV7HmHLqDPIBQR z?M`aRe{OrPo(JwW-d^~!ag(f2_`KT>t<8 literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_11.npy b/tools/forecast/masks/active_grid_cell_mask_11.npy new file mode 100644 index 0000000000000000000000000000000000000000..ecd33e5537fc09ce0f7633fa9cd78adc65ecb05d GIT binary patch literal 186752 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 A#{d8T literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/active_grid_cell_mask_12.npy b/tools/forecast/masks/active_grid_cell_mask_12.npy new file mode 100644 index 0000000000000000000000000000000000000000..193ff1bc1cdb15ea13f13763bdb9b64e487839ab GIT binary patch literal 186752 zcmeHQxw1sbQJr!5itz>_m8eoU_&N_mOzBS3Wh_#^iM6DuxU3V}=) za}-UNn$m|!2uQlL>=0cL0Zoz8E;y}^q!`kQgL63q1VLdop@EpDrSo#bzjGiW3AT?c zB};-M6ZcMhuC+CClX&9E$Os0I+2O+J2p~5FCDHZ+@`Uo~Q~CgeK;kPLWC>?BI$Z*m zD~N)cJPsH^OU~?Y>0|^1UFwF3H$!>xPM+4=xA_V*`SrTk1ESapN--X}3$v>`A%OU% zS|#>Y3*v)M%8kinwo={pIRb<)ocLxoC@JHkj0q^xdm)w@)N%JF1O)t$4{EoLh$oTq zNXN5V#GP9a$QZ{81FUnnkV2-=qPTS)&ub!}F-~D!v~>n+PFEA-{Tz&T5UgVQqn36~ybpq1{X?3<+K0-i&F24_w zqpVJLePWb04ZdBLbzPr9Ktn7u*dvYB6~aIa;uOidPJfB^t!I@H1|l?;?2-;))MCgL z23_fO8Y01X?6^KdT-RjRoDd3#z`ERhq!Vd(-6OI?MAYuJhG?h7ed2iX;0 zVGTgQo*IvawgiZ%Vde>cM@*?4VU~K4F#rMEG9Ck=i%c`u%4!B{M@&sNtIo@8fW$mq z7~DcAfm$C{LLFsVVrp_(b>3zRP=u9DtQy7!LXGHHzs_lgD-R|{OI_oQmSwsCyaFhN zaIF;4sMb-oMFd-A-e+6-^kPgSj0r8aHiZh-W@O=j#AK>!ywkAyZdd?1rB9R}lz6R*bkQe708)tau&T(q zf&f=V4waZ#eY+9$UMd1|^cW9O><7@tO^(W#X4{;UGhNaM@L!k-xFKQ=R}qzyAQ|#= zBM~+wVx}yROTAnYpeBd`&~>^9My!_#ux*MX83dV57va}D6L}K3Nx4~ao%z@?vGwOqZe^wbj%pbf*(~>{RN4WlRLKaX4|tCMRXUl&z@@o2&&upS6tp zL^m-c$7mIXh7uH5!q^kxvu@OA!4It#!tWH?Loxcg+hkyJA)S!z14*ZSs6C!9z{mW} z{Qx)tOyr}CAF!0&W#WhWLoZmy3l)yBD3 zv#R`Fh0m;0Efo!Eq~zUuz`E()9b!G*=Q}-)t}6Hu>(LbOPPpfHfLu2;`$}c%kj}hQ z;kd1u>1gbh1MavZFLlS^jhQ7IX}2QRIp0ODaSfGG9rr^MW0 z19+nq1jN14;z|*ZJqiJM+tWO;cGL*oXAJ@K-7vDYR>-3dLx3+;AsjY zDjt0x0?d&T4jjN+ts$Uz%F0?1k3J58%#o7x0Q4IxZFTJg1Qbg`QKL9t3Xby>K~OE` zv4SrvE}exyO_8?nT+SCmai*ZzX=pGY-Y&mmXL5x!G$=C6={HzsPyFuw1}{U?yAeBg z`yn}vfODsQZTAKn@vC6ViT2k*cIM(OMsTKnm(j08m5PVl@HvQzi&Q_}UG zI`>PPcL^CdUkcfoYY!OdyU!f_oxS^f4889J?YymrgbV}DoBc(d;fo!9yBpFm6YwJ7 z&RKPagteG+_IO9vH6|vb-vQm>TTY_C9Cr93uV}bT!+gRk@H=?PdGt3#9=yxb+ihTA zE#>I|9=hKe^jo42UFX5gwqU;{=fMyjx!M-!x8)qU)pL7o!+uj%`i;qRu{>n6P0(*m zI%K0~H`xmPmb{(+kA|L2jJ@3!*th4{Gq`mYf$hj|%cQyd?>Y#H#d90%+q2?${&zTw zG*S3=;J0Q%XUwG${9Hg_E9x8b)E~bsjOn2XfsME?rHMaL6r#T+Zg~iofZrG#=Auj? zqG`*>@6D0_llg?`H%`fyuK!-_w`{Jl`<@nNvx`T^meJptW25*5EAeF#_fG5=w~in8 zU!bN@GK|;uEYAMM@CJC_G9$!x@i(Hsx^wt-kPY6qtTbRf=D#(Vfwq837I3F&cC)I^}^}>L4IdDe}AE|^9{4Z}Z4??2f?*c;nVn17s7Q!$*gbNvgzylHm7KAAL&`=%CRPL9j zbcqm1=%7k`skfpG-{BI?=qDop90Vo4_wYNAykLj3lpFGtE)W8VFH{vv6d*p-GLT0} zJcJ7gfq*ln$gdUfaEJ%Nn&Qsc~_({{3HZ` z5R#3P_zEr(k35Zu-fIdxMeJ(`B88b5h{)P;Qa#y=EpV~If6V)({)i)loy1psZeGk$ zwK{NC8P#P%P9fyDYb84w!@b#L^CbX_L;*y?c%1lVZmiuczL*WZd+`dg{V2hHueR2B zldbvy;}0***HJ9xKF*i*9M+#-gjnV)em5ce_fMP?+E@d>*j9Z#R8$Z!0XF=%a(+|M zjY;zpFt<=hTtwG5iLq;oksv^*FBe;ABp3mmBm!(%{IaB2fzJLy1ORpu3gSc1B}DRm z>numIXDp+huOr^PX3$tpS1IWb2O`EF@gvXSl0X1?2uze@Ff|m5fq|~07^A*^Dg^kd z1S`wLO+1AIXjM_)Q`D5M5CQ~vNf+7iT`()h@g?7xX;?q)+afNJ5Cv=B7o8dt0TcKT zY1$7on*W7?xxw3vfdowt0HD8CM1zj{E_ZJuVR~QS3h5yHbEdG6c*qHNJ5S+~LV%G; zR0t=L116Ts%Sev>&e%=N;i5tyGRAmkADZO#gLg@IaqKq)?tsF3ek3gW3*&8pkc@mx zKw`j{@L%UUALLXx_Vx;afDng3V!BVs1s46%kZRvYTAV_b;JF?w z$bF;ST^8uE0zxX7u>$^Nt!}WBC5|DAMZ=SSie;rZy>h;4Asw>fk<7B>-ZG7->XPxX zr`k3vxUYGQ6cRBZnUEu}1RZ{)YOz#O_B(hERg$fb71X*y=`S2PN#QXx=f+EhdVcH7 z?p-7vuc*fMrwYZstakV1C=_C75J4!JL?b$M(x)0XEeb{ZbAJ}=vKoEooPurW=Xn;rr-}gh!Siag1U(sL;ZulT!3M_Gr00hcOFeH`v zBHqXyPl+BC`0|+sc+xRKjL8X0C&W1=;SkK|L3^|XF@r=RolxnRSDxW1(h#@2sOA8` z7X)A!6pQgJQfH=mhWUDqG)I@8c5of&f@A2a*vj;7YGD zE@vdExMh|9JBFewO?%UGfD!V9t2F}i+93cGGfr9|rWt5ve{6(y4|$ES0Hk5nR$K{# z8E53TE1DHPGL|#mS)T!-g>58JZEu&u5Ok5$rDt|xS9OigSZgL7dKpcx+0w$ewhQ(sA5trFv@gC}T8U#@7@!`mF|ZorW=!Gi#;PNP z6cn+D+T6@nKp6^FK;q5xfpLnjIJfAnrWW=Ki;M3E(?ppH&J3|%ul0aoyM)c*J^MS@3T=OpcWR zg)Muqr8&8EZ)Fa;qY(FROb$Ra00MI(%P@yB!P+S`<$}VEK(bTsh1n?O7H3+&F>3=H zGB>i?W=h<6V&P71Zv0fb;$xZj)KUi;bm{1^HmW!DLyLCMc!1kS#N}ISw4p&yE3IW8 zl$&uw=|)_u>!a*(_tr`cWRkg+)v|Z1HEAfapmB(8;zAXSja=Esh|hb-+fO zk>5BeWcoB}kV$kVQ}e{w(mOrG-2>y|umy317F?@$B{+N*fD&P$!+pUCgsmsmv9m+m zKQ7`D5s2HMMTj1aHZo^Y#td4Tuc*X)&koSvL;($UR|NL1L$Z9H)4HFXP za9q$po}cBWk-RYO$_tCtMT8VL>?_l?%*C|1t8q>cI8Yj&CWnWS_XGj=j!PyL6%~_} zbu@BrEpfHl)wFPqSR>n>C`Bk}U8E1!L_04$0T{=2gk~9Vq`dgdbR&h6nz1QtK(*sfkLB%@auh6?^+T&ITfWq|{@0;+51Se29w%PGsMvNq8D zqBS1V4|Z5|17G9vQ1_$P{TNuBcpEy2uf<+tGNA4!ujPSmm;m@JqXf9F?K;=yL%tSg zTHD}a(TmSDewkZXVY2-4lauSNKSK-SUoAARJu5A5Fk5(Q-10zoPM8HLJjD2(wPDxU z7MtTX%yie3NsK;PZ?eaRu=uI>(Qr3Qo&*WETK%R$P)zpN5EeRwEb4QkbL-5r7<~lA zY?Jl4+wz5Xtr7zhhxmO_$;by zd^3HTuSt})t{)$p$tHGKYHd5yp401Y-Q(+dVS||~)$OcwyTF9UtT|7Qb>YbvdD-|?-_c11x0{m^lEa=jP%C&Xtb-p%rf>}2uo|{xmjXU5f z&1Jw_ym$p+XWOC>Q51!S;$i~|za?GpE=CK`-&g=Z@b<=bE!6mBfA(eDk`#N_Z?3s; zB`wt#!OaCl%=JV92qqIyEbygv;u6Zbq9BZ1W7m^g$kk}x3a=Rd>1!Y)} zbx?L{`+^2FbD5#EP+5nTqigS#wK%y##^Q?@D+1MK>{Hf7g^LV@#lkZDzH|j1U=j6L z+kC}Hg`$K2i$rf>?y1EM#m=flTj`Y@`R>dY5~g7h?~{7-;>}7s`HieFXEi<=tXJ&n zDf${$*&u|<8?vsF0<#xHAXLVD1%yQec?)w=tC|!JXNZyNShjo2SgH0*>@{NoTJZ1k zWei({6cOYt%t=k&`2K6OqwJ~EZZWj#+Qabc1<)HqIQ1@|mE%?sc0g%Gy~_Q_V73?- ztA>O4OS;1nJXf`l@TH(4gi{N5X%Bjl`H5;&pEl*b(<#>q3=7~ud67a=<7%IBfkJM2 z&x--p)uqS{XVrDh03Fvdt`(ie?*J#})ltF`Md4DR|1{sYa6;OXPh;-N$A-;9>VkJ-pNo-3F~+}% zDJ;N~HgE-h>EfI$PzYPdu=(UlI|}${?pW{eQUOh1BlK0c=C2JYKzTFAd0q;L7ScXA zb7_|Z0*ENHB^)don6pm^9LB}ivPBS;U6cp_m_bjNN)O7`CtDQ+1|}s-Y+4u07H&*H zOSBo3HpmvrvE({RJ;6h*nV8Xws zWk#zQGeSNr5TG))EBjU2KEn~20xuihHljv+F|iR5`(;Owu~CinHYVRIEfbbOR8nUh z6=xA!1SA!1a;OSgr}&C17GBACKx{&@IC_hzP%{uCOwXRiILDq9?@>$PKLdhDXl}UI z9cnfR4xFPG@Jye8KZ!GY?)I)W)69&C!yB8%KPF_e1!Xzb=jd z^M!wfa`9n(JWZG1ud6>1%vTbwKJUBdE7|fd^_PAg6kRZ>^L1&yU-xZiT z@NXG6AjwB`1RMcJz!7i+905nb5pV%TIs(<4^zdPk;W?Z-4yh zU;g;ZfBy3OfB(ln{_=;P!TvYD|F>U%cJ^QY_4ohrv(x|dum18ce*2^Q@n^sN@qd4u zdi}|7_G>e+8Q2VL1~vnmfz7~XU^B28*bHn2HUpc1&A?`0Gq4%h3~UBA1Dk=(z-C}G zuo>74Yz8(1n}N;1W?(b08Q2VL1~vnmfz7~XU^B28*bHn2HUpc1&A?`0Gq4%h3~UBA z1Dk=(z-C}Guo>7400Te#@WUqJF$}nE?R7n7TX?&0V|!^&SQ^|e|9V2V-`*qbiq3E6 zgv5jXhNf2w%1uUxd6ur2D&oBj6< z=K@vibBkv%U{IXX?iZ6TgHywzFsl6ZK21Mkn(Hw?F_o(sD93q8R{LOizoVb6Ju5h->6}b0@2lZE{T7WhNCkD#= zYUXwxW+0Hy-1h*693M&m`+@-<)kjkgx6K&h+gIw_n_QUAZ5pYbpUdX z{}4m9BW;H)V1T*DRduZ^eAfsGcr0k-ZV)|h+!Z}{QqHC1!UnkaY4m;as^Pfn^Mz|h z8r>G(l^_i95LP{uvxAv|@m)bGl8zV??+R&+aJ~ck+J7=}?LOV5n)}qBx{cS)_JL}rvl6Zr9E0P* zrblxIOFS-oF(C%M)BD22#}RsN!;9BteAb9{8Oy9qw_;xoqgLW^zB8u*s^qptsawGH zik07Jr)O>}SrsrhIQe)+S^(E|943YRls-z}V4uVmpJVyQd7dmh*5*ag_^dj$7PG~| zP`Tmjn4xoBCwS+{Z_uoRl?Iq*C0>Rl>GENcTzSOxBo8v4pE+C}RiQ`4n zHBgQdYxo^_j1zuXnOS$w@ew^_c`oRR^9su&jHA>}ELwmM~MQyiVT6 z8IpCBjp(d8OlnDrbUjUmQBLhS;mWIh6+6&W7Su_eqUjP!ND&KuW&rZavA98G-c!&PraLfSP%NB zxBmIi*;O^2B1xmuzo&9l*r%V~Un3jpbUuvi0} zQ_apMjk2cM6yS;=r-(|)up;gMAHcZi}B@R7bz+QFrRBJbpW+>gI2R#C*fJjyhuqB;#P~#G+ zy9gTm(NL8b?OXwRU_J{CQ3n`nkz2?kv%vRI8rO&X@((twHQ!4gjNj(uvpFv9WNIFPm+9} zwZ^Q%E-N#e6OS7+Tx}Q!EM;h1r>2D@gKwtXYoQvX2^XT2L41+Ep90ks`%pTf zGW3p|o{$W8tpQpnGhL_^`X+DB82vGXS~&ZG>X8|IJ-pwW0T&DovEZqh>wOe5LsIb8 zA;!IV(Q%BP3|*%uqc29|7Bvr6blZ+r2R;j<1OR2hLx0tSpD^LXQl}uxDpbf`ypX=cW~u$n$!!FrG`VBx zY0ABHKiArx64t}bNUVIs?+iQu;uJ_Zt>Qz?D;M5y7c z2PD8?r?d$-h=4&r2Ju#r?A^qy_luvDJaq;fpUZtrLES9LO?E~{3N=*lHdG{^akIQF zx6&g`F^)$$gmt8kGjbKN%L*upNRjLzrFhd|HE{2sHbu_&fjDtrvgADI>oWuIVJUiC zYbRiH{~dnsKtX^>U%G>>Nd}PnKwHm^@dW#Y9lT)anpl_pZ8>FNf+m;Jv)iFysyOy*}j{x&raXkP09b+uD`Z= zbsZ%q+P{{vPYzz%2=-KRdiy8a{?^*%jW|ZiPVjsyWgnfpzLKigGb40iWyriz>qxJN$OnTt`p97ZLZFQG1p!7W0`o zzN6~cCOV_PgSyM7>|}p9?D9drqTuiv`V+o_zl(?LXMZB{;#uBaZejytDQ_q6()q@y zpNYP7oEI0H;eICP#S~sS+6?t`Iaf~g-dc0qPi2KaF?lbSmrOQA{cO@D6TQ30EcG*a zpZUKyeAWJDXrIq{6?GpNW}f-EOd4_jyAEPv@SLN4KFhba{~gZYnkamp_}NUTjJedp zJ_i_>WqmSF{o}WRHg{-XV3PNtH1SUqg}5JyGjGBn;wOV$Uz90CG|ja9y*c>*ijSx~ z@s#`0WxkjDnaMSG=V`7t8{9i)TK~=*o5d5Xv@dgU-^u;p)c)iCgw+%vU3+EEVDH}; zULfx*Gg54WelVj!@CD(y=> ziXwfNOVr~&nE}{=Q~a}spFr}0UB0DQkh|OhF%b5Js@f6-h!3?$k-)nMv^4#YX_!Y*@Kt$B8lk(2K*a8>J{b$}U^^Z7Q*oXGz-#1^pqil58S!Pt1 z33m!1*Ig@F$!PAIP4>P7VSZ6SkuY8-e)Ddu*{yx?Hu&!OE6Db%1p8TSjqy#k>;sHH zy!5`V+ET2u_hr2g^Up70EbhyHHzE7^C-wN&cL4Z)dTr9B>XashG2((4hy_H7ZDNQh!<=ZkwaCIcq%F4DAL zXf*%Az?|UQjBW{fJwSl|THy^k>fGIZ+Y-9xg=PqQWKPYfm+8 zRB&GN8Yw1xLNMWuz~XfH5zstTP}UvXhk|75YXvo~Q0fEworG{78gu<6Lp?q^qkDfuh_#|q$Y21P`v{nUn~H_ z0L;g;5Yu5vQG?L)U$Nd>q&8p7ex5xeKTHu_Rk(l!h{&ArZtwJZcVsDkdtp#MfS8Y9pfsg zwx6ujNeG1FxFOOH>ltvSIJ;LWcrlbPD#2@AY3b+cbzaZg(#icZuaYlX_ z(L^|8OnZE?-a|x*Tfam#y$y#U=pw1Oe;dHI4uHT2s?nz^HWS+FX43}J&@3h=dwTj1 zw8rQ(Xk0I~l_S9fvGS9pd^W>USQkdh;f+-IXWJAKC2z692W*||aT8V9&Yanl~(6DObVVlCUSq;Ns2nhOuj3zHd=mbuH{S76+d~$P11`1p3+ZE z!X1QHu=!?0O>emU&w)2I&YJHFW(E}bl0-#WM%3k)b3o$Vc#N>G@R5Zbr)}4px`Vf$ z4(Jx5;RkjuQkU-wD~SSC(e-7tdzsK#b**r_vKAeR zSsu_$$Nh|w2-lrd%^^4dN5L_@Nsq>1nV{n;T9#Qeq>cMnK+m$Qk`QFsZ)l@=LTjY@ zGw)aUg)FBoB!fX1WL^-J4z~dwQ@TD=z)S#!7RJvs5HQ)W z=O&2Et<-R{iDw7U;A#pm1_Sx6Q{^<3j<1NJ1C~Y&049jgvyU zPXmKYqB5C^C&s4U?k1id=m&=>Xh&$lv1(U>!+Qxx85TO+7wkaTYGNMS+r;zZJTIOB zKMh)hXfroDxs3u=L@K)tPm(d(%5&qH3m8G(FB*vh5`p~vX95t_01*ux66SE6(?IU; z<)M*0FwV*Yi_t}d6es4D=~`wmZLVsZ;{*RRQ z!~9WGlCzI-qOw!PK>=R4=WFVR&1`y$4t*j4}SFDppdm zX7977!S`}s<0T$icI0;Sdf_r^$k4O%H{&W8ePf>=HO@^}PnF4QF5ned?W(~+HAM7b zWHYK3z_nf~=-Ip9*p7{feYN2M_t5L5Yl>sdu@m+cVLGK&HNZXc>TKhhu)(KKZ&Sd} zFiXH|Zvh|ISLWcp274NtU^M#VW(MEa_+@TjgiiS3cTU6|e-90`KN^Z}&(WlT_&9d2 zGyv{Hdx2-;h8sFF0f#uTKrcz|O^na1iB@_`Ovg?1bXtlS;*1&sz9t?jT?0Pm^PhT$ znp>SL23idbcmlpAV(HOu8q{wM_?XWRKy_|$%7Mxs^|N0|BzjFgL`4nwnD4Rs!cUiR zQv2AN1P|@_kG;N1!%_Hr7@zyZjAVpLB^O!oTCN1Upjw#&wuxWz3XPP^1I?TtnlxE0 z=whq>#F;=B)Ff$GEx@ib4|jBGLONHyn2W9lNN);l>oQ|hhGuNkh1Sug&W%q8YgB{q zMhLr8Xj|v-K>^#aA=g?5`#L#1pA8F^m(snj9NDzY&gVNcAr~1v5oIQ3J3o|{&&H3K z0=`8rC)+gl(XuipaY zA-dAW^{#6Bg?2Ef;6rm2R5~3YHJrLn0-Yke@_<|K;$m-OP`jJf6b(@2wkvd470NkE zM;BLzGzPWX!6e&-)&?lMD-bQ$6%WLF-7L=tb7u<$+90^#0<(>kTPqMvSB`dyrP;^D z0j(j^cKn(!D|JP^1seP@VM6Mx(Fk*&pRI_A-cC$ubemnHaIGM%*i{oV1QY=(4ycNi zdyCZLG?uD+ph<#!bas^9oxX{Zp_Z4mv%pjRSEeu9S z7*K{)WXc*o9j0ofQlsW-)_vr@a4O%q8s6F$*dSg7mXs!4m4FiFkiCMBt~;))No^P^ ztU-j^`*K#7(L-B{E#c1|TRr%#7=d39pS&+)4Y(C+)V_z6m5Db0)?l~ioz=cKLu*+* zt8f^i>RrU%$?)137sr^R4G9rOqBCKxbV)GsOWTwz3)nKj+TIUZsj=FL?Ky(sf9cC; zJq@vSBhi^cEPZpcQx9A1+GN%6WWC9$n=5I)S$8S&fOL*L!nCAmsGE@|7|r5#2j$Su zcskkO=8Smu1;GfhOlg|at+4erqp&u`lG=U;E%sKIu-9UUnxq(|9rVoexz^Ry@#{3z zt*JmQ)A4a*Hw(~dC0GnniAr3Yl-N&JxolB@28zy)I{K-BHY?F_ib{}jjc2IzSw-LL z*o2Y@Tp916P-7jZYZt#H7D92Fg3mV7{I>TbD-~^=p?V|gdiMw`++86F`~{a~ZoyeO zD{Nf1D&SkQ8u1b55CO0a*QLsdj~8$%S3~jhHiFJ@e}w2oMMwGIZCUlF!A290#9NI& z$2K~-n=GVcGmhOZKq08nCXU5tjX%k7VDvcV0g&rcW7|)Z4)7(%JpeP1>OLz6q6?w2 z?dQpbp}Nds+OGJ+K~V50h-f;94ly_0)pBI~IMWf7{#>lrA8tm*o`dA0GV@&x=1{#x z9;!Fta2(t(R~2yPZW^~tf^Uhm+n<@vgt$pUo2 z%Ke2=(z19QF|(dy$mkp|V?t!c`x?e4&NARvqRe=bK@&M#M5m($7>4<-rs0v(1or`O z*|rj9(^i_gu>{O9>~}RaXKV!ET#cBOZsKtglhXMF99AA6OwJwCV8*CjARQ=|?*seI z&G)q&B^W0=3UX0?zJ)NYv(Oybr;Grc2_DOohMB*e*RPHZdG6}Z&$+fS2k9$I-<(5e7~;;UJn0UNjccERF= zXBN&StO`69tzzu#78vF2g2hOjIeNV<>Z>HKbqtVU-YwW9#hKY3Osp>xsMbM1f_e8@ zASn(_0bn6{?HOtv1&uAgSt_xlW_CfKmtqgvL#cx(ulaqsHb|KxwJ%i4vBx}A$wC=< zixAG6Pm7~q3p?WJE1}0+TtP3#n+KpH&TFLx8*=VO=m9rZVGLMYu(GW!FwTpG1`A^H zM&toE6=4Wxpnb8eLm-?NpG_8McE?{cb~b=(pb#$B#1|aGdGp!gIL_`CcGAjXjZ|uN zr~PU68+AQ#uJO$He$V|2ELd`7%O@5%0u8L}%NkA)WD1a#>X~zkgYRXiaMa`_Y8Ht537pZuHjh3t& zXDEtT|LdMIC3oLwL5DwA1In5IY>Y5C*(DETgHzfskY#Bvw~OXEkF@aj@M8D!-7INQ z3DR-CtV*NOu9@dN(n8m(-8??~Brh^W+Rv9|Wt2NgW;u^Fz_9J+eT*bVlZ15P@PE${3SEFX<89cM$cal(zg-Jq}u# zzz0D+2Mr8EVXjRSsU+BzDQQBZBJnm zv4mH|T!K^uNm_rTnm*JjLpUzL>wo&VDm~rKl~}c+zRrVxhT+@=x zqaMTP$>#2#Duf}$y5dHRnOwM_R%Z3ERg4LhivDE|W29%9hzYTU+QNMJvOR_BL>V5{gRChLLlD>C){jNNr_9# zDdfi~Oa*nG8OQ;TOLm?i#6pR*vRDHBG&NEIox>YkTjD&?zEFZ{Tr$>5q;&-o;OD84 z3hX_qNTi%cxz;ESUc5tOF(t1lR({&5V)s@A71*fI^nZTy7=s&{#&8`r5q9l7P^1R?8ehT>&z0C$sEl0{4pu4R zzR96DqQR7LnL_CHdAy8``z~wz<<;|r(;cVIMkrFZnZYJ% z(Jy-Vn|M+IJ5Ov+R?|(mSyIzFy+G8m0vUdIc4YR)8jz}_`(^ZHKuIC5W>ND7h3{ZM z2Q+1Wu!56h)6DbL%Cx9zHFQUkVNog{JPB5l*pRYTT+z=;Fr6Dy@+IOO+%f3Q8ku8Q!iDJjuLR$C* z`q`8LenQg~C4G&g@N?BLj$`r?@-$JGl*A&s?vu6aID<-xsS8W1x{p>aBu%aB(z^P6 zviCJWejsZgV@2qrSClosf{|VfEZ!TuEG`@Q;f7_z^Z_-#+%(*MHB{8Y*ORxeraM=G z;`Is*cRktk<^w<6VoupG&e4iQRgs(ftY(|ehi@lu-<+~)#7i>1GALcc^{v$+W_-h1HE}}RjqA_ z->7?0`{G`-CLX14a*E=#&PO7~Q*!YfMJ)m)4uSg~F`2Qhs#&KsgzaT2r@JLH$Z zZ;^)x;1!7J%K#KqjzABE;n|2Cb1USRC}Rvjv^BVe65|S=U?L!*T1d0DSl~TUlM5VG zM_JSB2-G+XwvqYSi$2z}xB~_TeUZ?lBh0Ge%=vXhdJUBq6oDbNEbf3olz5BSWP=7( zfn-!?Jwkaw;asKU_b^NBH*ksP36db$eSP?1q;!z1p~#9Set_v!M89rLqa4#>jb}-6 z>ZEh&%48Lrdchel32G`tC8HwUxi?s3&{4TqNPQLy#K{=xK7rj^Hz|2in#rXLa{&w^@?VTaq8BAp<^)3lf5vR$KfpLJt2K>F&&&c6 zeK8_>+}$!VGpNK6^AAf2@?+GI!L6Z8*7+`_8NM3alho6tk2Kqe?W%XfpvG9rZMJVS zAM)}HUZtIvCkTsb=Aa^AA0-ZqU6`v#JVIwlpB>`Je$idHdyu4l z`O=yRCmLO{I@vSepWqiUFm4=>&(N4TTMlv(@2>9ddyu2^JZtP0y3vrOLjXODB(O OX)|8;pMAEk*ZTjfym$%# literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/polarhole1_mask.npy b/tools/forecast/masks/polarhole1_mask.npy new file mode 100644 index 0000000000000000000000000000000000000000..e0f925154fa2271c8f5cd665534f039bfd0101b1 GIT binary patch literal 186752 zcmeIvJC0L95Czb2$|{^0q)GThEPxC|LO_HJV?=-$7Ixn!? zRmrV$|Gs(m`tAG8&Ci=(ySu}~r{nJBcK7?^)7|ZM_xb+#zu=p!n^g z6tzrO?T~^Fo|aN9g5r0JQnXU6x+&${Ta;3)a`JaeQj}7xx+&${Ta;3)a`HDzQgl+T znlZ(kTb5ERV-9E*q^P7^HDii7w=AVt#vITrNKr|-YQ_|EZdppPj5(lLkfM@u)r=|T z+_IEn8FN6hAVnqRsu@$vxn(KEGUkA8L5fa_RX3%ady7(vRZjkHNs3a6RX3%ady7(v zRZjkPNs3mct9D312Tw~W7D4gbMJZ~TuG%3b9XxGix=2djFUz!+>7t)e)Wy?YCX1-> z9m7m|nT$IMW!*d}W-`vo-ZIUknc=u4P}j=%iT87 zpqTE literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/polarhole2_mask.npy b/tools/forecast/masks/polarhole2_mask.npy new file mode 100644 index 0000000000000000000000000000000000000000..f5db99969433eb1ae439cea7696ca58864772086 GIT binary patch literal 186752 zcmeIyJ5Iwu6hKkCWfkrWk{do13!nqhP*5R5j3^L6HlhJm!G%m4{ba*a-RkFEW_bvef1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs N0RjXF5Fij-;0G)R9dZBw literal 0 HcmV?d00001 diff --git a/tools/forecast/masks/polarhole3_mask.npy b/tools/forecast/masks/polarhole3_mask.npy new file mode 100644 index 0000000000000000000000000000000000000000..3322af109702ef011bb298ea06f0821e09d33666 GIT binary patch literal 186752 zcmeIvJ&M9W7zW^^-BV0AE~N3N@&ItFu)&%gZb z&%gY?|M54!{Ql=({`;T){J;PFhkyO2KmGZSzoPu_{_sEl@++19Vd0!x9V zz*1l-uoPGdECrSVOM#`pQeY{t6j%x@1(pI!fu+DwU@5Q^SPCo!mI6zGrNB~PDXVd0!x9Vz*1l-uoPGdECrSVOM$Bt`1ts^YUnlY=?Z*&eEaro0XtpSiv|R(ZwRo3 z>>`Cv9hReuiLDqrRoF`gQ(6*Z>oInT$mb2pdm&iqwf4mG#65qc3fDS!oGQb=PZ!BjY3DRN1c-E znE0-WlZZwd!#cW@nA8d{)U=qj%I^jzx0k*8{^NVPzI?$?x?OmY*jO51#4~^Q9Y+(V z6=jSsRb1}q%H0?kr9z+y&C2FLxy`fcafbr-iZNwBt}^|DK+#4fS`!8$Cv*Tpw>JSn zx$nL+cEoCZjn-zdTd_w3+L4$XX9CgYScC%Z(_LwyP-`bi)-P(Y4S=D`GwBTi`? z4bkvJ3%O#9p%-|%p%%Esac-Sx;i7Hvl#LOZ2Tq(cKZqWYQ|$u*h)J>|M^x!@)s2j$ zA&w-bzuV>Zg#-lu+q!~}X<<)0ZSb^`QEOJ50}R3UA-EdAY>0)E2P1X^Y})Eq?6Act z*5L#KY4&a@Ek;Fo2o6@9K=C~_@zv8l)IP$aj(aWjCwZ092xKU+JHC?Uos^0XQg$;8 z-CWsHpOkt(T(tg%7!{3N#kLB;xC6;l8^wti<#aO~0u~-#*h9BWx?>$Z$_o;M**Rw& z$qgMRnE^wL6>Z$kqgZtn ?L+gsk1@hBf5Yu4k9oF?(LiVxNUY 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 g)`3XpX8hx%ciH^@Ydv>qble9Ys#$egFUf literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_01.npy b/tools/forecast/meta/cos_month_01.npy new file mode 100644 index 0000000000000000000000000000000000000000..17db7aaeca7b3ee9428afef9636e26941d4fdb74 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46alX5n`7+(zXcs( literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_02.npy b/tools/forecast/meta/cos_month_02.npy new file mode 100644 index 0000000000000000000000000000000000000000..910d0ac9eaf31f354ab35edec597c36648a04b8c GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46alXP|Nq+o0Kv>2b^rhX literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_03.npy b/tools/forecast/meta/cos_month_03.npy new file mode 100644 index 0000000000000000000000000000000000000000..da3cfc29b209a1078c650df40e7f09fd63f10e9e GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag;1z1EunyssS; literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_04.npy b/tools/forecast/meta/cos_month_04.npy new file mode 100644 index 0000000000000000000000000000000000000000..3cf8d50465d77a6158330e989a3b8c68f5cd5005 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag+q28R6rxmp|h literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_05.npy b/tools/forecast/meta/cos_month_05.npy new file mode 100644 index 0000000000000000000000000000000000000000..4bd371cc7b3ae502b4fde0c9509d61af9def1821 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46alX5n`8F_0KbJD;Q#;t literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_06.npy b/tools/forecast/meta/cos_month_06.npy new file mode 100644 index 0000000000000000000000000000000000000000..3e502be3a9e3a52929dd8ce2c75c482fe78bf316 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag*g)| literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_07.npy b/tools/forecast/meta/cos_month_07.npy new file mode 100644 index 0000000000000000000000000000000000000000..4bd371cc7b3ae502b4fde0c9509d61af9def1821 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46alX5n`8F_0KbJD;Q#;t literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_08.npy b/tools/forecast/meta/cos_month_08.npy new file mode 100644 index 0000000000000000000000000000000000000000..babc20931369fa82f8f933114fc7e9e2a4048091 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46alWk|Nrj;0K!Bb^Z)<= literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_09.npy b/tools/forecast/meta/cos_month_09.npy new file mode 100644 index 0000000000000000000000000000000000000000..6eb0ef012e423026031e9c57ea2e7973b882813d GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag;1dp<@0yt5p| literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_10.npy b/tools/forecast/meta/cos_month_10.npy new file mode 100644 index 0000000000000000000000000000000000000000..7e1463f31f91674f96fb32d047723d9fc79d3514 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46alWk|Nq+o0Kvo_bN~PV literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_11.npy b/tools/forecast/meta/cos_month_11.npy new file mode 100644 index 0000000000000000000000000000000000000000..ef78f85fdbe125ae782109e1da5b04427fbc6ff6 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46alVln`7+(zX2U! literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/cos_month_12.npy b/tools/forecast/meta/cos_month_12.npy new file mode 100644 index 0000000000000000000000000000000000000000..f9688e61682385cadb11418a069488dd1f4ab976 GIT binary patch literal 132 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 XXCxM+0{I%6ItsN46ag*<1_pZoxYiqb literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_02.npy b/tools/forecast/meta/sin_month_02.npy new file mode 100644 index 0000000000000000000000000000000000000000..5472b522ebcec4df7d5b292df61d8d1fbc201eb6 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46alUqn`7+(zX=^; literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_03.npy b/tools/forecast/meta/sin_month_03.npy new file mode 100644 index 0000000000000000000000000000000000000000..f9688e61682385cadb11418a069488dd1f4ab976 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag*qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46alX5n`7+(zXcs( literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_05.npy b/tools/forecast/meta/sin_month_05.npy new file mode 100644 index 0000000000000000000000000000000000000000..5197c700eb253690cfb1e31d2fd3f6b59b9c90b4 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag+q1_pZoxY`?g literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_06.npy b/tools/forecast/meta/sin_month_06.npy new file mode 100644 index 0000000000000000000000000000000000000000..1315ee137df9042111962dd9ec39fb0677ae2ad1 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46ag;1y}LI90KKmrlmGw# literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_07.npy b/tools/forecast/meta/sin_month_07.npy new file mode 100644 index 0000000000000000000000000000000000000000..4ac3b1c8a2e0fd4e68f5af2fd31e244e5c26ea15 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46alXP|Nrj;0K!Zj_5c6? literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_08.npy b/tools/forecast/meta/sin_month_08.npy new file mode 100644 index 0000000000000000000000000000000000000000..37bf72b4722213260183a7b298b1b72834b1cc43 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46alUqn`8F_0KbVH;s5{u literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_09.npy b/tools/forecast/meta/sin_month_09.npy new file mode 100644 index 0000000000000000000000000000000000000000..3e502be3a9e3a52929dd8ce2c75c482fe78bf316 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag*g)| literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_10.npy b/tools/forecast/meta/sin_month_10.npy new file mode 100644 index 0000000000000000000000000000000000000000..37bf72b4722213260183a7b298b1b72834b1cc43 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= YXCxM+0{I%6ItsN46alUqn`8F_0KbVH;s5{u literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_11.npy b/tools/forecast/meta/sin_month_11.npy new file mode 100644 index 0000000000000000000000000000000000000000..e7cc0f5236c000aed3047a45a2db8718873c8923 GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag+~28R6rxndjr literal 0 HcmV?d00001 diff --git a/tools/forecast/meta/sin_month_12.npy b/tools/forecast/meta/sin_month_12.npy new file mode 100644 index 0000000000000000000000000000000000000000..45bbaf0368e7786f3f4696d088b3334f1f4c8b8f GIT binary patch literal 132 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+l>qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= XXCxM+0{I%6ItsN46ag;1z1AiGyf7S< literal 0 HcmV?d00001 diff --git a/tools/forecast/models.py b/tools/forecast/models.py index 04515fe..a3c59c8 100644 --- a/tools/forecast/models.py +++ b/tools/forecast/models.py @@ -1,10 +1,9 @@ """ -Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted -to fit the galaxy interface. +Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted +to fit the galaxy interface. """ import sys import os -sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) # if using jupyter kernel import config import numpy as np import pandas as pd @@ -14,13 +13,13 @@ from tensorflow.keras.layers import Conv2D, BatchNormalization, UpSampling2D, \ concatenate, MaxPooling2D, Input from tensorflow.keras.optimizers import Adam - +sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) # if using jupyter kernel ''' Defines the Python-based sea ice forecasting models, such as the IceNet architecture and the linear trend extrapolation model. ''' -### Custom layers: +# Custom layers: # -------------------------------------------------------------------- @@ -44,7 +43,7 @@ def get_config(self): return {'temp': self.temp.numpy()} -### Network architectures: +# Network architectures: # -------------------------------------------------------------------- def unet_batchnorm(input_shape, loss, weighted_metrics, learning_rate=1e-4, filter_size=3, @@ -53,53 +52,53 @@ def unet_batchnorm(input_shape, loss, weighted_metrics, learning_rate=1e-4, filt **kwargs): inputs = Input(shape=input_shape) - conv1 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(inputs) - conv1 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv1) + conv1 = Conv2D(np.int(64 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(inputs) + conv1 = Conv2D(np.int(64 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv1) bn1 = BatchNormalization(axis=-1)(conv1) pool1 = MaxPooling2D(pool_size=(2, 2))(bn1) - conv2 = Conv2D(np.int(128*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool1) - conv2 = Conv2D(np.int(128*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv2) + conv2 = Conv2D(np.int(128 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool1) + conv2 = Conv2D(np.int(128 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv2) bn2 = BatchNormalization(axis=-1)(conv2) pool2 = MaxPooling2D(pool_size=(2, 2))(bn2) - conv3 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool2) - conv3 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv3) + conv3 = Conv2D(np.int(256 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool2) + conv3 = Conv2D(np.int(256 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv3) bn3 = BatchNormalization(axis=-1)(conv3) pool3 = MaxPooling2D(pool_size=(2, 2))(bn3) - conv4 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool3) - conv4 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv4) + conv4 = Conv2D(np.int(256 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool3) + conv4 = Conv2D(np.int(256 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv4) bn4 = BatchNormalization(axis=-1)(conv4) pool4 = MaxPooling2D(pool_size=(2, 2))(bn4) - conv5 = Conv2D(np.int(512*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool4) - conv5 = Conv2D(np.int(512*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv5) + conv5 = Conv2D(np.int(512 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(pool4) + conv5 = Conv2D(np.int(512 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv5) bn5 = BatchNormalization(axis=-1)(conv5) - up6 = Conv2D(np.int(256*n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2,2), interpolation='nearest')(bn5)) + up6 = Conv2D(np.int(256 * n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2, 2), interpolation='nearest')(bn5)) merge6 = concatenate([bn4, up6], axis=3) - conv6 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge6) - conv6 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv6) + conv6 = Conv2D(np.int(256 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge6) + conv6 = Conv2D(np.int(256 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv6) bn6 = BatchNormalization(axis=-1)(conv6) - up7 = Conv2D(np.int(256*n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2,2), interpolation='nearest')(bn6)) - merge7 = concatenate([bn3,up7], axis=3) - conv7 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge7) - conv7 = Conv2D(np.int(256*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv7) + up7 = Conv2D(np.int(256 * n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2, 2), interpolation='nearest')(bn6)) + merge7 = concatenate([bn3, up7], axis=3) + conv7 = Conv2D(np.int(256 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge7) + conv7 = Conv2D(np.int(256 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv7) bn7 = BatchNormalization(axis=-1)(conv7) - up8 = Conv2D(np.int(128*n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2,2), interpolation='nearest')(bn7)) - merge8 = concatenate([bn2,up8], axis=3) - conv8 = Conv2D(np.int(128*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge8) - conv8 = Conv2D(np.int(128*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv8) + up8 = Conv2D(np.int(128 * n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2, 2), interpolation='nearest')(bn7)) + merge8 = concatenate([bn2, up8], axis=3) + conv8 = Conv2D(np.int(128 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge8) + conv8 = Conv2D(np.int(128 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv8) bn8 = BatchNormalization(axis=-1)(conv8) - up9 = Conv2D(np.int(64*n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2,2), interpolation='nearest')(bn8)) - merge9 = concatenate([conv1,up9], axis=3) - conv9 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge9) - conv9 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv9) - conv9 = Conv2D(np.int(64*n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv9) + up9 = Conv2D(np.int(64 * n_filters_factor), 2, activation='relu', padding='same', kernel_initializer='he_normal')(UpSampling2D(size=(2, 2), interpolation='nearest')(bn8)) + merge9 = concatenate([conv1, up9], axis=3) + conv9 = Conv2D(np.int(64 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(merge9) + conv9 = Conv2D(np.int(64 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv9) + conv9 = Conv2D(np.int(64 * n_filters_factor), filter_size, activation='relu', padding='same', kernel_initializer='he_normal')(conv9) final_layer_logits = [(Conv2D(n_output_classes, 1, activation='linear')(conv9)) for i in range(n_forecast_months)] final_layer_logits = tf.stack(final_layer_logits, axis=-1) @@ -118,7 +117,7 @@ def unet_batchnorm(input_shape, loss, weighted_metrics, learning_rate=1e-4, filt return model -### Benchmark models: +# Benchmark models: # -------------------------------------------------------------------- @@ -152,7 +151,7 @@ def linear_trend_forecast(forecast_month, n_linear_years='all', da=None, dataset valid_dates = [pd.Timestamp(date) for date in da.time.values] - input_dates = [forecast_month - pd.DateOffset(years=1+lag) for lag in range(n_linear_years)] + input_dates = [forecast_month - pd.DateOffset(years=1 + lag) for lag in range(n_linear_years)] input_dates # Do not use missing months in the linear trend projection diff --git a/tools/forecast/utils.py b/tools/forecast/utils.py index 626c60d..6787625 100644 --- a/tools/forecast/utils.py +++ b/tools/forecast/utils.py @@ -1,12 +1,11 @@ """ -Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted -to fit the galaxy interface. +Code taken from https://github.com/tom-andersson/icenet-paper and slightly adjusted +to fit the galaxy interface. """ import os import sys import numpy as np import tensorflow as tf -sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) # if using jupyter kernel from models import linear_trend_forecast import config import itertools @@ -23,10 +22,11 @@ from mpl_toolkits.axes_grid1 import make_axes_locatable import imageio from tqdm import tqdm +sys.path.insert(0, os.path.join(os.getcwd(), 'icenet')) # if using jupyter kernel ############################################################################### -############### DATA PROCESSING & LOADING +# DATA PROCESSING & LOADING ############################################################################### @@ -455,7 +455,7 @@ def save_variable(self, varname, data_format, dates=None): dates = self.obs_train_dates ######################################################################## - ################# Observational variable + # Observational variable ######################################################################## if self.preproc_obs_data: @@ -488,7 +488,7 @@ def save_variable(self, varname, data_format, dates=None): da = da.groupby("time.month", restore_coord_dims=True) - climatology elif data_format == 'linear_trend': - da = self.build_linear_trend_da(da, dataset='obs') + da = self.build_linear_trend_da(da, dataset='obs') # Realise the array da.data = np.asarray(da.data, dtype=np.float32) @@ -548,7 +548,7 @@ def save_variable(self, varname, data_format, dates=None): print("Done in {:.0f}s.\n".format(time.time() - tic)) ######################################################################## - ################# Transfer variable + # Transfer variable ######################################################################## if self.preproc_transfer_data: @@ -600,7 +600,7 @@ def save_variable(self, varname, data_format, dates=None): da = da.groupby("time.month", restore_coord_dims=True) - climatology elif data_format == 'linear_trend': - da = self.build_linear_trend_da(da, dataset='cmip6') + da = self.build_linear_trend_da(da, dataset='cmip6') # Normalise the array if varname != 'siconca': @@ -839,11 +839,11 @@ def determine_variable_names(self): for data_format in vardict.keys(): if vardict[data_format]['include']: if data_format != 'linear_trend': - for lag in np.arange(1, vardict[data_format]['max_lag']+1): - variable_names.append(varname+'_{}_{}'.format(data_format, lag)) + for lag in np.arange(1, vardict[data_format]['max_lag'] + 1): + variable_names.append(varname + '_{}_{}'.format(data_format, lag)) elif data_format == 'linear_trend': - for leadtime in np.arange(1, self.config['n_forecast_months']+1): - variable_names.append(varname+'_{}_{}'.format(data_format, leadtime)) + for leadtime in np.arange(1, self.config['n_forecast_months'] + 1): + variable_names.append(varname + '_{}_{}'.format(data_format, leadtime)) # Metadata input variables that don't span time elif 'metadata' in vardict.keys() and vardict['include']: @@ -872,7 +872,7 @@ def set_number_of_input_channels_for_each_input_variable(self): # Variables that span time for data_format in vardict.keys(): if vardict[data_format]['include']: - varname_format = varname+'_{}'.format(data_format) + varname_format = varname + '_{}'.format(data_format) if data_format != 'linear_trend': self.num_input_channels_dict[varname_format] = vardict[data_format]['max_lag'] elif data_format == 'linear_trend': @@ -910,7 +910,7 @@ def all_sic_input_dates_from_forecast_start_date(self, forecast_start_date): max_lag = np.max(max_lags) input_dates = [ - forecast_start_date - pd.DateOffset(months=int(lag)) for lag in np.arange(1, max_lag+1) + forecast_start_date - pd.DateOffset(months=int(lag)) for lag in np.arange(1, max_lag + 1) ] return input_dates @@ -1272,7 +1272,7 @@ def data_generation(self, forecast_IDs): input_months = [present_date - relativedelta(months=lb) for lb in lbs] elif data_format == 'linear_trend': input_months = [present_date + relativedelta(months=forecast_leadtime) - for forecast_leadtime in np.arange(1, self.config['n_forecast_months']+1)] + for forecast_leadtime in np.arange(1, self.config['n_forecast_months'] + 1)] variable_idx2 += self.num_input_channels_dict[varname_format] @@ -1337,7 +1337,7 @@ def on_epoch_end(self): self.rng.shuffle(self.all_forecast_IDs) -################## MISC FUNCTIONS +# MISC FUNCTIONS ################################################################################ @@ -1502,7 +1502,7 @@ def make_varname_verbose_any_leadtime(varname): ################################################################################ -################## FUNCTIONS +# FUNCTIONS ################################################################################ @@ -1545,7 +1545,7 @@ def fix_near_real_time_era5_func(latlon_path): ############################################################################### -############### LEARNING RATE SCHEDULER +# LEARNING RATE SCHEDULER ############################################################################### @@ -1570,7 +1570,7 @@ def lr_scheduler_exp_decay(epoch, lr): ############################################################################### -############### REGRIDDING VECTOR DATA +# REGRIDDING VECTOR DATA ############################################################################### @@ -1645,8 +1645,8 @@ def gridcell_angles_from_dim_coords(cube): for yi in [0, 1]: for xi in [0, 1]: xy = np.meshgrid(x_bounds[:, xi], y_bounds[:, yi]) - x[:,:,c[cind]] = xy[0] - y[:,:,c[cind]] = xy[1] + x[:, :, c[cind]] = xy[0] + y[:, :, c[cind]] = xy[1] cind += 1 # convert the X and Y coordinates to longitudes and latitudes @@ -1687,7 +1687,7 @@ def invert_gridcell_angles(angles): ############################################################################### -############### CMIP6 +# CMIP6 ############################################################################### @@ -1699,7 +1699,7 @@ def esgf_search(server="https://esgf-node.llnl.gov/esg-search/search", client = requests.session() payload = search payload["project"] = project - payload["type"]= "File" + payload["type"] = "File" if latest: payload["latest"] = "true" if local_node: @@ -1738,7 +1738,7 @@ def esgf_search(server="https://esgf-node.llnl.gov/esg-search/search", for d in resp: if verbose2: for k in d: - print("{}: {}".format(k,d[k])) + print("{}: {}".format(k, d[k])) url = d["url"] for f in d["url"]: sp = f.split("|") @@ -1785,7 +1785,7 @@ def save_cmip6(cmip6_ease, fpath, compress=True, verbose=False): ############################################################################### -############### PLOTTING +# PLOTTING ############################################################################### @@ -1853,7 +1853,7 @@ def arr_to_ice_edge_rgba_arr(arr, thresh, land_mask, region_mask, rgb): ############################################################################### -############### VIDEOS +# VIDEOS ############################################################################### @@ -1922,7 +1922,7 @@ def make_frame(date): ax.axes.xaxis.set_visible(False) ax.axes.yaxis.set_visible(False) - ax.set_title('{:04d}/{:02d}/{:02d}'.format(date.year, date.month, date.day), fontsize=figsize*4) + ax.set_title('{:04d}/{:02d}/{:02d}'.format(date.year, date.month, date.day), fontsize=figsize * 4) divider = make_axes_locatable(ax) cax = divider.append_axes('right', size='5%', pad=0.05)