| |
| |
| |
| |
| |
|
|
| """ |
| Tools for pre-processing model input data into training/validation/testing data. |
| """ |
|
|
| import numpy as np |
| import netCDF4 as nc |
| import xarray as xr |
| import os |
| import warnings |
| from datetime import datetime |
| from ..util import to_bool, insolation |
|
|
| |
| fill_value = np.array(nc.default_fillvals['f4']).astype(np.float32) |
|
|
|
|
| class Preprocessor(object): |
|
|
| def __init__(self, data_obj, predictor_file='.predictors.nc'): |
| """ |
| Initialize an instance of Preprocessor for DLWP modelling. The data_obj is an instance of one of the data |
| processing classes in DLWP.data, and should have data already loaded. |
| |
| :param data_obj: instance of DLWP.data class |
| :param predictor_file: str: file to which to write the predictors and targets |
| """ |
| self.raw_data = data_obj |
| if self.raw_data is None: |
| print('Preprocessor warning: no raw data object provided; acting as wrapper for processed data') |
| else: |
| if self.raw_data.Dataset is None: |
| print('Preprocessor warning: opening data with default args') |
| self.raw_data.open() |
| self._predictor_file = predictor_file |
| self.data = None |
|
|
| @property |
| def spatial_shape(self): |
| """ |
| :return: the shape of the spatial component of ensemble predictors |
| """ |
| return self.data.predictors.shape[1:] |
|
|
| @property |
| def n_features(self): |
| """ |
| :return: int: the number of features in the predictor array |
| """ |
| return int(np.prod(self.spatial_shape)) |
|
|
| @property |
| def convolution_shape(self): |
| """ |
| :return: the shape of the predictors expected by a convolutional layer. Note it is channels_first! |
| """ |
| return (int(np.prod(self.data.predictors.shape[1:-2])),) + self.data.predictors.shape[-2:] |
|
|
| def data_to_samples(self, time_step=1, batch_samples=100, variables='all', levels='all', |
| pairwise=False, scale_variables=False, chunk_size=1, in_memory=False, to_zarr=False, |
| overwrite=False, verbose=False): |
| """ |
| Convert the data referenced by the data_obj in __init__ to samples ready for ingestion in a DLWP model. Write |
| samples in batches of size batch_samples. The parameter scale_variables determines whether individual |
| variable/level combinations are scaled and de-meaned by their spatially-averaged values. |
| |
| :param time_step: int: the number of time steps to take for the predictors and targets |
| :param batch_samples: int: number of samples in the time dimension to read and process at once |
| :param variables: iter: list of variables to process; may be 'all' for all variables available |
| :param levels: iter: list of integer pressure levels (mb); may be 'all' |
| :param pairwise: bool: if True, creates a Dataset with one less dimension and creates a variable at each |
| variable-level pairing specified here. The lists of variables and levels must be the same length. |
| :param scale_variables: bool: if True, apply de-mean and scaling on a variable/level basis |
| :param chunk_size: int: size of the chunks in the sample (time) dimension) |
| :param in_memory: bool: if True, speeds up operations by performing them in memory (may require lots of RAM) |
| :param to_zarr: bool: if True, writes the resulting data structure to a zarr group in addition to the netCDF |
| file. Zarr groups use efficient compression and may be significantly faster in training than netCDF files, |
| and can be read just like netCDF with xarray. |
| :param overwrite: bool: if True, overwrites any existing output files, otherwise, raises an error |
| :param verbose: bool: print progress statements |
| :return: opens Dataset on self.data |
| """ |
| |
| if int(time_step) < 1: |
| raise ValueError("'time_step' must be >= 1") |
| |
| if int(chunk_size) < 1: |
| raise ValueError("'chunk_size' must be >= 1") |
| |
| if self.raw_data is None: |
| raise ValueError('cannot process when no data_obj was supplied at initialization') |
| if self.raw_data.Dataset is None: |
| raise IOError('no data loaded to data_obj') |
|
|
| |
| vars_available = list(self.raw_data.Dataset.data_vars.keys()) |
| if variables == 'all': |
| variables = [v for v in vars_available] |
| elif not(isinstance(variables, list) or isinstance(variables, tuple)): |
| variables = [variables] |
| if levels == 'all': |
| levels = list(self.raw_data.Dataset.level.values) |
| elif not(isinstance(levels, list) or isinstance(levels, tuple)): |
| levels = [levels] |
| |
| var_no_lev = [] |
| for v in variables: |
| if 'level' not in self.raw_data.Dataset[v].coords: |
| var_no_lev.append(v) |
| if not pairwise and len(var_no_lev) > 0: |
| warnings.warn("Some variables (%s) are not on pressure levels. I'm switching to pairwise mode." |
| % var_no_lev) |
| pair_var = [v for v in variables if v not in var_no_lev] * len(levels) |
| new_levels = [] |
| for l in levels: |
| new_levels = new_levels + [l] * (len(variables) - len(var_no_lev)) |
| variables = pair_var + var_no_lev |
| levels = new_levels + [0] * len(var_no_lev) |
| pairwise = True |
| |
| if pairwise: |
| if len(variables) != len(levels): |
| raise ValueError('for pairwise variable/level pairs, len(variables) must equal len(levels)') |
| var_lev = ['/'.join([v, str(l)]) for v, l in zip(variables, levels)] |
|
|
| |
| all_dates = self.raw_data.dataset_dates |
| sel_levels = [] |
| for l in levels: |
| if float(l) in self.raw_data.Dataset.level: |
| sel_levels.append(l) |
| ds = self.raw_data.Dataset.sel(time=all_dates, level=list(set(sel_levels))) |
| if verbose: |
| print('Preprocessor.data_to_samples: opening and formatting raw data') |
| for v in vars_available: |
| if v not in variables: |
| ds = ds.drop(v) |
| n_sample, n_var, n_level, n_lat, n_lon = (len(all_dates) - (2 * time_step - 1), len(variables), len(levels), |
| ds.dims['lat'], ds.dims['lon']) |
| if n_sample < 1: |
| raise ValueError('too many time steps for time dimension') |
|
|
| |
| if pairwise: |
| means = np.zeros((n_var,), dtype=np.float32) |
| stds = np.ones((n_var,), dtype=np.float32) |
| else: |
| means = np.zeros((n_var, n_level), dtype=np.float32) |
| stds = np.ones((n_var, n_level), dtype=np.float32) |
|
|
| |
| if not in_memory: |
| if os.path.isfile(self._predictor_file) and not overwrite: |
| raise IOError("predictor file '%s' already exists" % self._predictor_file) |
| if verbose: |
| print('Preprocessor.data_to_samples: creating output file %s' % self._predictor_file) |
| nc_fid = nc.Dataset(self._predictor_file, 'w') |
| nc_fid.description = 'Training data for DLWP' |
| nc_fid.setncattr('scaling', 'True' if scale_variables else 'False') |
| nc_fid.createDimension('sample', 0) |
| nc_fid.createDimension('time_step', time_step) |
| if pairwise: |
| nc_fid.createDimension('varlev', n_var) |
| else: |
| nc_fid.createDimension('variable', n_var) |
| nc_fid.createDimension('level', n_level) |
| nc_fid.createDimension('lat', n_lat) |
| nc_fid.createDimension('lon', n_lon) |
|
|
| |
| nc_var = nc_fid.createVariable('lat', np.float32, 'lat') |
| nc_var.setncatts({ |
| 'long_name': 'Latitude', |
| 'units': 'degrees_north' |
| }) |
| nc_fid.variables['lat'][:] = ds['lat'].values |
|
|
| nc_var = nc_fid.createVariable('lon', np.float32, 'lon') |
| nc_var.setncatts({ |
| 'long_name': 'Longitude', |
| 'units': 'degrees_east' |
| }) |
| nc_fid.variables['lon'][:] = ds['lon'].values |
|
|
| if pairwise: |
| nc_var = nc_fid.createVariable('varlev', str, 'varlev') |
| nc_var.setncatts({ |
| 'long_name': 'Variable/level pair', |
| }) |
| nc_fid.variables['varlev'][:] = np.array(var_lev, dtype='object') |
| else: |
| nc_var = nc_fid.createVariable('variable', str, 'variable') |
| nc_var.setncatts({ |
| 'long_name': 'Variable name', |
| }) |
| nc_fid.variables['variable'][:] = np.array(variables, dtype='object') |
|
|
| nc_var = nc_fid.createVariable('level', np.float32, 'level') |
| nc_var.setncatts({ |
| 'long_name': 'Pressure level', |
| 'units': 'hPa' |
| }) |
| nc_fid.variables['level'][:] = levels |
|
|
| |
| nc_var = nc_fid.createVariable('sample', np.float32, 'sample') |
| time_units = 'hours since 1970-01-01 00:00:00' |
|
|
| nc_var.setncatts({ |
| 'long_name': 'Sample start time', |
| 'units': time_units |
| }) |
| times = np.array([datetime.utcfromtimestamp(d/1e9) |
| for d in ds['time'].values[time_step-1:n_sample+time_step-1].astype(datetime)]) |
| nc_fid.variables['sample'][:] = nc.date2num(times, time_units) |
|
|
| |
| if pairwise: |
| dims = ('sample', 'time_step', 'varlev', 'lat', 'lon') |
| chunks = (chunk_size, 1, 1, n_lat, n_lon) |
| else: |
| dims = ('sample', 'time_step', 'variable', 'level', 'lat', 'lon') |
| chunks = (chunk_size, 1, 1, 1, n_lat, n_lon) |
| predictors = nc_fid.createVariable('predictors', np.float32, dims, chunksizes=chunks) |
| predictors.setncatts({ |
| 'long_name': 'Predictors', |
| 'units': 'N/A', |
| '_FillValue': fill_value |
| }) |
| targets = nc_fid.createVariable('targets', np.float32, dims, chunksizes=chunks) |
| targets.setncatts({ |
| 'long_name': 'Targets', |
| 'units': 'N/A', |
| '_FillValue': fill_value |
| }) |
|
|
| else: |
| |
| if verbose: |
| print('Preprocessor.data_to_samples: loading data to memory') |
| ds.load() |
| if pairwise: |
| predictors = np.full((n_sample, time_step, n_var, n_lat, n_lon), np.nan, dtype=np.float32) |
| else: |
| predictors = np.full((n_sample, time_step, n_var, n_level, n_lat, n_lon), np.nan, dtype=np.float32) |
| targets = predictors.copy() |
|
|
| |
| if pairwise: |
| for vl, vl_name in enumerate(var_lev): |
| sel_kw = {} if (variables[vl] in var_no_lev) else {'level': levels[vl]} |
| if verbose: |
| print('Preprocessor.data_to_samples: variable/level pair %s of %s (%s)' % |
| (vl + 1, len(var_lev), vl_name)) |
| if scale_variables: |
| if verbose: |
| print('Preprocessor.data_to_samples: calculating mean and std') |
| v_mean = mean_by_batch(ds[variables[vl]].sel(**sel_kw), batch_samples) |
| v_std = std_by_batch(ds[variables[vl]].sel(**sel_kw), batch_samples, mean=v_mean) |
| means[vl] = 1. * v_mean |
| stds[vl] = 1. * v_std |
| else: |
| v_mean = 0.0 |
| v_std = 1.0 |
| for i, s in enumerate(list(range(0, n_sample, batch_samples))): |
| if verbose: |
| print('Preprocessor.data_to_samples: writing batch %s of %s' |
| % (i + 1, n_sample // batch_samples + 1)) |
| idx = slice(s, min(s + batch_samples, n_sample)) |
| for t in range(time_step): |
| idxp = slice(s + t, min(s + t + batch_samples, n_sample + t)) |
| idxt = slice(s + t + time_step, |
| min(s + t + time_step + batch_samples, n_sample + t + time_step)) |
| predictors[idx, t, vl, ...] = (ds[variables[vl]].isel(time=idxp).sel(**sel_kw).values |
| - v_mean) / v_std |
| targets[idx, t, vl, ...] = (ds[variables[vl]].isel(time=idxt).sel(**sel_kw).values |
| - v_mean) / v_std |
| else: |
| for v, var in enumerate(variables): |
| for l, lev in enumerate(levels): |
| if verbose: |
| print('Preprocessor.data_to_samples: variable %s of %s (%s); level %s of %s (%s)' % |
| (v+1, len(variables), var, l+1, len(levels), lev)) |
| if scale_variables: |
| if verbose: |
| print('Preprocessor.data_to_samples: calculating mean and std') |
| v_mean = mean_by_batch(ds[var].sel(level=lev), batch_samples) |
| v_std = std_by_batch(ds[var].sel(level=lev), batch_samples, mean=v_mean) |
| means[v, l] = 1. * v_mean |
| stds[v, l] = 1. * v_std |
| else: |
| v_mean = 0.0 |
| v_std = 1.0 |
| for i, s in enumerate(list(range(0, n_sample, batch_samples))): |
| if verbose: |
| print('Preprocessor.data_to_samples: writing batch %s of %s' |
| % (i+1, n_sample//batch_samples+1)) |
| idx = slice(s, min(s+batch_samples, n_sample)) |
| for t in range(time_step): |
| idxp = slice(s+t, min(s+t+batch_samples, n_sample+t)) |
| idxt = slice(s+t+time_step, min(s+t+time_step+batch_samples, n_sample+t+time_step)) |
| predictors[idx, t, v, l, ...] = (ds[var].isel(time=idxp, level=l).values - v_mean) / v_std |
| targets[idx, t, v, l, ...] = (ds[var].isel(time=idxt, level=l).values - v_mean) / v_std |
|
|
| if not in_memory: |
| |
| if pairwise: |
| nc_var = nc_fid.createVariable('mean', np.float32, ('varlev',)) |
| nc_var.setncatts({ |
| 'long_name': 'Global mean of variables at levels', |
| 'units': 'N/A', |
| }) |
| nc_var[:] = means |
|
|
| nc_var = nc_fid.createVariable('std', np.float32, ('varlev',)) |
| nc_var.setncatts({ |
| 'long_name': 'Global std deviation of variables at levels', |
| 'units': 'N/A', |
| }) |
| nc_var[:] = stds |
| else: |
| nc_var = nc_fid.createVariable('mean', np.float32, ('variable', 'level')) |
| nc_var.setncatts({ |
| 'long_name': 'Global mean of variables at levels', |
| 'units': 'N/A', |
| }) |
| nc_var[:] = means |
|
|
| nc_var = nc_fid.createVariable('std', np.float32, ('variable', 'level')) |
| nc_var.setncatts({ |
| 'long_name': 'Global std deviation of variables at levels', |
| 'units': 'N/A', |
| }) |
| nc_var[:] = stds |
|
|
| |
| nc_fid.close() |
| result_ds = xr.open_dataset(self._predictor_file) |
| else: |
| if pairwise: |
| result_ds = xr.Dataset({ |
| 'predictors': (['sample', 'time_step', 'varlev', 'lat', 'lon'], predictors, { |
| 'long_name': 'Predictors', |
| 'units': 'N/A' |
| }), |
| 'targets': (['sample', 'time_step', 'varlev', 'lat', 'lon'], targets, { |
| 'long_name': 'Targets', |
| 'units': 'N/A' |
| }), |
| 'mean': (['varlev'], means, { |
| 'long_name': 'Global mean of variables at levels', |
| 'units': 'N/A', |
| }), |
| 'std': (['varlev'], stds, { |
| 'long_name': 'Global std deviation of variables at levels', |
| 'units': 'N/A', |
| }) |
| }, coords={ |
| 'sample': ('sample', ds['time'].values[time_step-1:n_sample+time_step-1], { |
| 'long_name': 'Sample start time' |
| }), |
| 'varlev': ('varlev', var_lev), |
| 'lat': ('lat', ds['lat'].values, { |
| 'long_name': 'Latitude', |
| 'units': 'degrees_north' |
| }), |
| 'lon': ('lon', ds['lon'].values, { |
| 'long_name': 'Longitude', |
| 'units': 'degrees_east' |
| }), |
| }, attrs={ |
| 'description': 'Training data for DLWP', |
| 'scaling': 'True' if scale_variables else 'False', |
| 'pairwise': 'True' |
| }) |
| else: |
| result_ds = xr.Dataset({ |
| 'predictors': (['sample', 'time_step', 'variable', 'level', 'lat', 'lon'], predictors, { |
| 'long_name': 'Predictors', |
| 'units': 'N/A' |
| }), |
| 'targets': (['sample', 'time_step', 'variable', 'level', 'lat', 'lon'], targets, { |
| 'long_name': 'Targets', |
| 'units': 'N/A' |
| }), |
| 'mean': (['variable', 'level'], means, { |
| 'long_name': 'Global mean of variables at levels', |
| 'units': 'N/A', |
| }), |
| 'std': (['variable', 'level'], stds, { |
| 'long_name': 'Global std deviation of variables at levels', |
| 'units': 'N/A', |
| }) |
| }, coords={ |
| 'sample': ('sample', ds['time'].values[:n_sample], { |
| 'long_name': 'Sample start time' |
| }), |
| 'variable': ('variable', variables), |
| 'level': ('level', levels, { |
| 'long_name': 'Pressure level', |
| 'units': 'hPa' |
| }), |
| 'lat': ('lat', ds['lat'].values, { |
| 'long_name': 'Latitude', |
| 'units': 'degrees_north' |
| }), |
| 'lon': ('lon', ds['lon'].values, { |
| 'long_name': 'Longitude', |
| 'units': 'degrees_east' |
| }), |
| }, attrs={ |
| 'description': 'Training data for DLWP', |
| 'scaling': 'True' if scale_variables else 'False', |
| 'pairwise': 'False' |
| }) |
|
|
| result_ds = result_ds.chunk({'sample': chunk_size}) |
|
|
| if to_zarr: |
| zarr_file = '.'.join(self._predictor_file.split('.')[:-1]) + '.zarr' |
| if verbose: |
| print('Preprocessor.data_to_samples: writing to zarr group %s...' % zarr_file) |
| try: |
| result_ds.to_zarr(zarr_file, mode='w' if overwrite else 'w-') |
| success = True |
| except AttributeError: |
| warnings.warn("xarray version must be >= 0.12.0 (got %s) to export to zarr; falling back to netCDF" |
| % xr.__version__) |
| success = False |
| except ValueError: |
| raise IOError('zarr group path %s exists' % zarr_file) |
| if success: |
| self._predictor_file = zarr_file |
| result_ds = xr.open_zarr(zarr_file) |
|
|
| self.data = result_ds |
|
|
| def data_to_series(self, batch_samples=100, variables='all', levels='all', pairwise=False, scale_variables=False, |
| chunk_size=1, in_memory=False, to_zarr=False, overwrite=False, verbose=False, |
| no_string_coords=False): |
| """ |
| Convert the data referenced by the data_obj in __init__ to a continuous time series of formatted data. This |
| series of data is appropriate for use in a SeriesDataGenerator object during model training. Write data |
| in batches of size batch_samples (smaller uses less memory but is less efficient). The parameter |
| scale_variables determines whether individual variable/level combinations are scaled and de-meaned by their |
| spatially-averaged values. |
| |
| :param batch_samples: int: number of samples in the time dimension to read and process at once |
| :param variables: iter: list of variables to process; may be 'all' for all variables available |
| :param levels: iter: list of integer pressure levels (mb); may be 'all' |
| :param pairwise: bool: if True, creates a Dataset with one less dimension and creates a variable at each |
| variable-level pairing specified here. The lists of variables and levels must be the same length. |
| :param scale_variables: bool: if True, apply de-mean and scaling on a variable/level basis |
| :param chunk_size: int: size of the chunks in the sample (time) dimension) |
| :param in_memory: bool: if True, speeds up operations by performing them in memory (may require lots of RAM) |
| :param to_zarr: bool: if True, writes the resulting data structure to a zarr group in addition to the netCDF |
| file. Zarr groups use efficient compression and may be significantly faster in training than netCDF files, |
| and can be read just like netCDF with xarray. |
| :param overwrite: bool: if True, overwrites any existing output files, otherwise, raises an error |
| :param verbose: bool: print progress statements |
| :param no_string_coords: bool: if True, do not use string coordinates for the variable/level names. Only applies |
| when in_memory=False |
| :return: opens Dataset on self.data |
| """ |
| |
| if int(chunk_size) < 1: |
| raise ValueError("'chunk_size' must be >= 1") |
| |
| if self.raw_data is None: |
| raise ValueError('cannot process when no data_obj was supplied at initialization') |
| if self.raw_data.Dataset is None: |
| raise IOError('no data loaded to data_obj') |
|
|
| |
| vars_available = list(self.raw_data.Dataset.data_vars.keys()) |
| if variables == 'all': |
| variables = [v for v in vars_available] |
| elif not(isinstance(variables, list) or isinstance(variables, tuple)): |
| variables = [variables] |
| if levels == 'all': |
| levels = list(self.raw_data.Dataset.level.values) |
| elif not(isinstance(levels, list) or isinstance(levels, tuple)): |
| levels = [levels] |
| |
| var_no_lev = [] |
| for v in variables: |
| if 'level' not in self.raw_data.Dataset[v].coords: |
| var_no_lev.append(v) |
| if not pairwise and len(var_no_lev) > 0: |
| warnings.warn("Some variables (%s) are not on pressure levels. I'm switching to pairwise mode." |
| % var_no_lev) |
| pair_var = [v for v in variables if v not in var_no_lev] * len(levels) |
| new_levels = [] |
| for l in levels: |
| new_levels = new_levels + [l] * (len(variables) - len(var_no_lev)) |
| variables = pair_var + var_no_lev |
| levels = new_levels + [0] * len(var_no_lev) |
| pairwise = True |
| |
| if pairwise: |
| if len(variables) != len(levels): |
| raise ValueError('for pairwise variable/level pairs, len(variables) must equal len(levels)') |
| var_lev = ['/'.join([v, str(l)]) for v, l in zip(variables, levels)] |
|
|
| |
| all_dates = self.raw_data.dataset_dates |
| sel_levels = [] |
| ds = self.raw_data.Dataset |
| try: |
| for l in levels: |
| if float(l) in self.raw_data.Dataset.level: |
| sel_levels.append(l) |
| ds = ds.sel(level=list(set(sel_levels))) |
| except AttributeError: |
| pass |
| if verbose: |
| print('Preprocessor.data_to_samples: opening and formatting raw data') |
| for v in vars_available: |
| if v not in variables: |
| ds = ds.drop(v) |
| lat_dim = 'lat' if 'lat' in ds.dims.keys() else 'latitude' |
| lon_dim = 'lon' if 'lon' in ds.dims.keys() else 'longitude' |
| n_sample, n_var, n_level, n_lat, n_lon = (len(all_dates), len(variables), len(levels), |
| ds.dims[lat_dim], ds.dims[lon_dim]) |
| if n_sample < 1: |
| raise ValueError('too many time steps for time dimension') |
|
|
| |
| if pairwise: |
| means = np.zeros((n_var,), dtype=np.float32) |
| stds = np.ones((n_var,), dtype=np.float32) |
| else: |
| means = np.zeros((n_var, n_level), dtype=np.float32) |
| stds = np.ones((n_var, n_level), dtype=np.float32) |
|
|
| |
| if not in_memory: |
| if os.path.isfile(self._predictor_file) and not overwrite: |
| raise IOError("predictor file '%s' already exists" % self._predictor_file) |
| if verbose: |
| print('Preprocessor.data_to_samples: creating output file %s' % self._predictor_file) |
| nc_fid = nc.Dataset(self._predictor_file, 'w') |
| nc_fid.description = 'Training data for DLWP' |
| nc_fid.setncattr('scaling', 'True' if scale_variables else 'False') |
| nc_fid.createDimension('sample', 0) |
| if pairwise: |
| nc_fid.createDimension('varlev', n_var) |
| else: |
| nc_fid.createDimension('variable', n_var) |
| nc_fid.createDimension('level', n_level) |
| nc_fid.createDimension('lat', n_lat) |
| nc_fid.createDimension('lon', n_lon) |
|
|
| |
| nc_var = nc_fid.createVariable('lat', np.float32, 'lat') |
| nc_var.setncatts({ |
| 'long_name': 'Latitude', |
| 'units': 'degrees_north' |
| }) |
| nc_fid.variables['lat'][:] = ds[lat_dim].values |
|
|
| nc_var = nc_fid.createVariable('lon', np.float32, 'lon') |
| nc_var.setncatts({ |
| 'long_name': 'Longitude', |
| 'units': 'degrees_east' |
| }) |
| nc_fid.variables['lon'][:] = ds[lon_dim].values |
|
|
| if pairwise: |
| if no_string_coords: |
| nc_var = nc_fid.createVariable('varlev', int, 'varlev') |
| nc_fid.variables['varlev'][:] = np.arange(len(var_lev)) |
| else: |
| nc_var = nc_fid.createVariable('varlev', str, 'varlev') |
| nc_fid.variables['varlev'][:] = np.array(var_lev, dtype='object') |
| nc_var.setncatts({ |
| 'long_name': 'Variable/level pair', |
| }) |
| else: |
| if no_string_coords: |
| nc_var = nc_fid.createVariable('variable', int, 'variable') |
| nc_fid.variables['variable'][:] = np.arange(len(variables)) |
| else: |
| nc_var = nc_fid.createVariable('variable', str, 'variable') |
| nc_fid.variables['variable'][:] = np.array(variables, dtype='object') |
| nc_var.setncatts({ |
| 'long_name': 'Variable name', |
| }) |
|
|
| nc_var = nc_fid.createVariable('level', np.float32, 'level') |
| nc_var.setncatts({ |
| 'long_name': 'Pressure level', |
| 'units': 'hPa' |
| }) |
| nc_fid.variables['level'][:] = levels |
|
|
| |
| nc_var = nc_fid.createVariable('sample', np.float32, 'sample') |
| time_units = 'hours since 1970-01-01 00:00:00' |
|
|
| nc_var.setncatts({ |
| 'long_name': 'Sample start time', |
| 'units': time_units |
| }) |
| times = np.array([datetime.utcfromtimestamp(d/1e9) |
| for d in ds['time'].values.astype(datetime)]) |
| nc_fid.variables['sample'][:] = nc.date2num(times, time_units) |
|
|
| |
| if pairwise: |
| dims = ('sample', 'varlev', 'lat', 'lon') |
| chunks = (chunk_size, 1, n_lat, n_lon) |
| else: |
| dims = ('sample', 'variable', 'level', 'lat', 'lon') |
| chunks = (chunk_size, 1, 1, n_lat, n_lon) |
| predictors = nc_fid.createVariable('predictors', np.float32, dims, chunksizes=chunks) |
| predictors.setncatts({ |
| 'long_name': 'Predictors', |
| 'units': 'N/A', |
| '_FillValue': fill_value |
| }) |
|
|
| else: |
| |
| if verbose: |
| print('Preprocessor.data_to_samples: loading data to memory') |
| ds.load() |
| if pairwise: |
| predictors = np.full((n_sample, n_var, n_lat, n_lon), np.nan, dtype=np.float32) |
| else: |
| predictors = np.full((n_sample, n_var, n_level, n_lat, n_lon), np.nan, dtype=np.float32) |
|
|
| |
| if pairwise: |
| for vl, vl_name in enumerate(var_lev): |
| sel_kw = {} if (variables[vl] in var_no_lev) else {'level': levels[vl]} |
| if verbose: |
| print('Preprocessor.data_to_samples: variable/level pair %s of %s (%s)' % |
| (vl + 1, len(var_lev), vl_name)) |
| if scale_variables: |
| if verbose: |
| print('Preprocessor.data_to_samples: calculating mean and std') |
| v_mean = mean_by_batch(ds[variables[vl]].sel(**sel_kw), batch_samples) |
| v_std = std_by_batch(ds[variables[vl]].sel(**sel_kw), batch_samples, mean=v_mean) |
| means[vl] = 1. * v_mean |
| stds[vl] = 1. * v_std |
| else: |
| v_mean = 0.0 |
| v_std = 1.0 |
| for i, s in enumerate(list(range(0, n_sample, batch_samples))): |
| if verbose: |
| print('Preprocessor.data_to_samples: writing batch %s of %s' |
| % (i + 1, n_sample // batch_samples + 1)) |
| idx = slice(s, min(s + batch_samples, n_sample)) |
| predictors[idx, vl, ...] = (ds[variables[vl]].isel(time=idx).sel(**sel_kw).values - v_mean) / v_std |
| else: |
| for v, var in enumerate(variables): |
| for l, lev in enumerate(levels): |
| if verbose: |
| print('Preprocessor.data_to_samples: variable %s of %s (%s); level %s of %s (%s)' % |
| (v+1, len(variables), var, l+1, len(levels), lev)) |
| if scale_variables: |
| if verbose: |
| print('Preprocessor.data_to_samples: calculating mean and std') |
| v_mean = mean_by_batch(ds[var].sel(level=lev), batch_samples) |
| v_std = std_by_batch(ds[var].sel(level=lev), batch_samples, mean=v_mean) |
| means[v, l] = 1. * v_mean |
| stds[v, l] = 1. * v_std |
| else: |
| v_mean = 0.0 |
| v_std = 1.0 |
| for i, s in enumerate(list(range(0, n_sample, batch_samples))): |
| if verbose: |
| print('Preprocessor.data_to_samples: writing batch %s of %s' |
| % (i+1, n_sample//batch_samples+1)) |
| idx = slice(s, min(s+batch_samples, n_sample)) |
| predictors[idx, v, l, ...] = (ds[var].isel(time=idx, level=l).values - v_mean) / v_std |
|
|
| if not in_memory: |
| |
| if pairwise: |
| nc_var = nc_fid.createVariable('mean', np.float32, ('varlev',)) |
| nc_var.setncatts({ |
| 'long_name': 'Global mean of variables at levels', |
| 'units': 'N/A', |
| }) |
| nc_var[:] = means |
|
|
| nc_var = nc_fid.createVariable('std', np.float32, ('varlev',)) |
| nc_var.setncatts({ |
| 'long_name': 'Global std deviation of variables at levels', |
| 'units': 'N/A', |
| }) |
| nc_var[:] = stds |
| else: |
| nc_var = nc_fid.createVariable('mean', np.float32, ('variable', 'level')) |
| nc_var.setncatts({ |
| 'long_name': 'Global mean of variables at levels', |
| 'units': 'N/A', |
| }) |
| nc_var[:] = means |
|
|
| nc_var = nc_fid.createVariable('std', np.float32, ('variable', 'level')) |
| nc_var.setncatts({ |
| 'long_name': 'Global std deviation of variables at levels', |
| 'units': 'N/A', |
| }) |
| nc_var[:] = stds |
|
|
| |
| nc_fid.close() |
| result_ds = xr.open_dataset(self._predictor_file) |
| else: |
| if pairwise: |
| result_ds = xr.Dataset({ |
| 'predictors': (['sample', 'varlev', 'lat', 'lon'], predictors, { |
| 'long_name': 'Predictors', |
| 'units': 'N/A' |
| }), |
| 'mean': (['varlev'], means, { |
| 'long_name': 'Global mean of variables at levels', |
| 'units': 'N/A', |
| }), |
| 'std': (['varlev'], stds, { |
| 'long_name': 'Global std deviation of variables at levels', |
| 'units': 'N/A', |
| }) |
| }, coords={ |
| 'sample': ('sample', ds['time'].values, { |
| 'long_name': 'Sample start time' |
| }), |
| 'varlev': ('varlev', var_lev), |
| 'lat': ('lat', ds['lat'].values, { |
| 'long_name': 'Latitude', |
| 'units': 'degrees_north' |
| }), |
| 'lon': ('lon', ds['lon'].values, { |
| 'long_name': 'Longitude', |
| 'units': 'degrees_east' |
| }), |
| }, attrs={ |
| 'description': 'Training data for DLWP', |
| 'scaling': 'True' if scale_variables else 'False', |
| 'pairwise': 'True' |
| }) |
| else: |
| result_ds = xr.Dataset({ |
| 'predictors': (['sample', 'variable', 'level', 'lat', 'lon'], predictors, { |
| 'long_name': 'Predictors', |
| 'units': 'N/A' |
| }), |
| 'mean': (['variable', 'level'], means, { |
| 'long_name': 'Global mean of variables at levels', |
| 'units': 'N/A', |
| }), |
| 'std': (['variable', 'level'], stds, { |
| 'long_name': 'Global std deviation of variables at levels', |
| 'units': 'N/A', |
| }) |
| }, coords={ |
| 'sample': ('sample', ds['time'].values, { |
| 'long_name': 'Sample start time' |
| }), |
| 'variable': ('variable', variables), |
| 'level': ('level', levels, { |
| 'long_name': 'Pressure level', |
| 'units': 'hPa' |
| }), |
| 'lat': ('lat', ds['lat'].values, { |
| 'long_name': 'Latitude', |
| 'units': 'degrees_north' |
| }), |
| 'lon': ('lon', ds['lon'].values, { |
| 'long_name': 'Longitude', |
| 'units': 'degrees_east' |
| }), |
| }, attrs={ |
| 'description': 'Training data for DLWP', |
| 'scaling': 'True' if scale_variables else 'False', |
| 'pairwise': 'False' |
| }) |
|
|
| result_ds = result_ds.chunk({'sample': chunk_size}) |
|
|
| if to_zarr: |
| zarr_file = '.'.join(self._predictor_file.split('.')[:-1]) + '.zarr' |
| if verbose: |
| print('Preprocessor.data_to_samples: writing to zarr group %s...' % zarr_file) |
| try: |
| result_ds.to_zarr(zarr_file, mode='w' if overwrite else 'w-') |
| success = True |
| except AttributeError: |
| warnings.warn("xarray version must be >= 0.12.0 (got %s) to export to zarr; falling back to netCDF" |
| % xr.__version__) |
| success = False |
| except ValueError: |
| raise IOError('zarr group path %s exists' % zarr_file) |
| if success: |
| self._predictor_file = zarr_file |
| result_ds = xr.open_zarr(zarr_file) |
|
|
| self.data = result_ds |
|
|
| def open(self, **kwargs): |
| """ |
| Open the dataset pointed to by the instance's _predictor_file attribute onto self.data |
| |
| :param kwargs: passed to xarray.open_dataset() or xarray.open_zarr() |
| """ |
| if self._predictor_file.endswith('.zarr'): |
| self.data = xr.open_zarr(self._predictor_file, **kwargs) |
| else: |
| self.data = xr.open_dataset(self._predictor_file, **kwargs) |
|
|
| def close(self): |
| """ |
| Close the dataset on self.data |
| """ |
| self.data.close() |
| self.data = None |
|
|
| def to_file(self, predictor_file=None): |
| """ |
| Write the data opened on self.data to the file predictor_file if not None or the instance's _predictor_file |
| attribute. |
| |
| :param predictor_file: str: file path; if None, uses self._predictor_file |
| """ |
| if self.data is None: |
| raise ValueError('cannot save to file with no sample data generated or opened') |
| if predictor_file is None: |
| predictor_file = self._predictor_file |
| if predictor_file.endswith('.zarr'): |
| self.data.to_zarr(predictor_file) |
| else: |
| self.data.to_netcdf(predictor_file) |
|
|
|
|
| def mean_by_batch(da, batch_size, axis=0): |
| """ |
| Loop over batches indexed in axis in an xarray DataArray to take the grand mean of the array in a memory- |
| efficient way. |
| |
| :param da: xarray DataArray |
| :param batch_size: int: number of samples to load and mean at a time |
| :param axis: int: axis along which to index batches |
| :return: float: the mean of the array |
| """ |
| size = da.shape[axis] |
| batches = list(range(0, size, batch_size)) |
| dim = da.dims[axis] |
| total = 0.0 |
| for b in batches: |
| total += da.isel(**{dim: slice(b, min(b+batch_size, size))}).values.sum() |
| return total / da.size |
|
|
|
|
| def std_by_batch(da, batch_size, axis=0, mean=None): |
| """ |
| Loop over batches indexed in axis in an xarray DataArray to take the standard deviation of the array in a memory- |
| efficient way. If mean is provided, assumes the mean of the data is already known to be this value. |
| |
| :param da: xarray DataArray |
| :param batch_size: int: number of samples to load and mean at a time |
| :param axis: int: axis along which to index batches |
| :param mean: float: the (known) mean of the array |
| :return: float: the standard deviation of the array |
| """ |
| if mean is None: |
| mean = mean_by_batch(da, batch_size, axis) |
| size = da.shape[axis] |
| batches = list(range(0, size, batch_size)) |
| dim = da.dims[axis] |
| total = 0.0 |
| for b in batches: |
| total += np.sum((da.isel(**{dim: slice(b, min(b + batch_size, size))}).values - mean) ** 2.) |
| return np.sqrt(total / da.size) |
|
|
|
|
| def get_constants(constants=None, **kwargs): |
| """ |
| Return an array of constants from the files in 'constants'. This latter variable should be an iterable |
| containing length-2 pairs of (file_name, variable_name_in_file). For example, (('land_sea_mask.nc, 'lsm'),). |
| Each variable from the files must have the same shape, and these shapes should match the predictor data to be used |
| in the model training. |
| |
| :param constants: iter: iterable of length-2 (file_name, variable_name) pairs |
| :param kwargs: passed to xr.open_dataset() for each file |
| :return: ndarray: array of constants stacked along a new dimension ('channels') |
| """ |
| if constants is None: |
| return |
|
|
| if not hasattr(constants, '__iter__'): |
| raise TypeError("'constants' must be an iterable of length-2 (file_name, variable) pairs") |
| for c in constants: |
| if len(c) != 2: |
| raise ValueError("each element of 'constants' must have length 2 (file_name, variable)") |
|
|
| result = [] |
| for c in constants: |
| ds_c = xr.open_dataset(c[0], **kwargs) |
| result.append(ds_c.variables[c[1]].values) |
| result = np.stack(result, axis=0) |
|
|
| return result |
|
|
|
|
| def prepare_data_array(ds, input_sel=None, output_sel=None, add_insolation=False, return_data=True): |
| """ |
| Prepare an array of predictor or |
| :param ds: |
| :param input_sel: |
| :param output_sel: |
| :param add_insolation: |
| :param return_data: |
| :return: |
| """ |
| input_sel = input_sel or {} |
| output_sel = output_sel or {} |
| try: |
| add_insolation = to_bool(add_insolation) |
| except ValueError: |
| pass |
| assert isinstance(add_insolation, (bool, str)) |
| if isinstance(add_insolation, str): |
| assert add_insolation in ['hourly', 'daily'] |
| daily_insolation = str(add_insolation) == 'daily' |
|
|
| if 'time_step' in ds.dims: |
| |
| |
| da = ds.predictors.isel(time_step=-1) |
| else: |
| da = ds.predictors |
|
|
| |
| if len(input_sel) == 0: |
| if 'varlev' in ds.variables.keys(): |
| input_sel = {'varlev': ds['varlev'].values} |
| else: |
| input_sel = {'variable': ds['variable'].values, 'level': ds['level'].values} |
| output_sel = output_sel or {} |
| if len(output_sel) == 0: |
| if 'varlev' in ds.variables.keys(): |
| output_sel = {'varlev': ds['varlev'].values} |
| else: |
| output_sel = {'variable': ds['variable'].values, 'level': ds['level'].values} |
|
|
| |
| if 'varlev' in input_sel.keys(): |
| union = [s for s in input_sel['varlev'] if s in output_sel['varlev']] |
| added_in = [s for s in input_sel['varlev'] if s not in union] |
| added_out = [s for s in output_sel['varlev'] if s not in union] |
| if not return_data or (len(added_in) > 0 and len(added_out) > 0): |
| if not return_data: |
| warnings.warn("To avoid returning values inconsistent with the input data, will not reduce indices " |
| "to basic indexing.") |
| else: |
| warnings.warn("Found extra variables in both input and output, could not reduce to basic indexing.") |
| input_ind = [int(np.where(ds['varlev'] == v)[0]) for v in input_sel['varlev']] |
| output_ind = [int(np.where(ds['varlev'] == v)[0]) for v in output_sel['varlev']] |
| else: |
| da = da.sel(varlev=union + added_in + added_out) |
| input_ind = slice(0, len(union) + len(added_in)) |
| output_ind = slice(0, len(union) + len(added_out)) |
| else: |
| raise NotImplementedError("prepare_data_array is not ready for use with variable/level coordinates.") |
|
|
| |
| if add_insolation: |
| sol = insolation(da.sample.values, ds.lat.values, ds.lon.values, daily=daily_insolation) |
| else: |
| sol = None |
|
|
| |
| if return_data: |
| return da.values, input_ind, output_ind, sol |
| else: |
| return input_ind, output_ind, sol |
|
|