yzt15806542928's picture
Upload folder using huggingface_hub
989c6ea verified
Raw
History Blame Contribute Delete
46.4 kB
#
# Copyright (c) 2019 Jonathan Weyn <jweyn@uw.edu>
#
# See the file LICENSE for your rights.
#
"""
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
# netCDF fill value
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
"""
# Check time step parameter
if int(time_step) < 1:
raise ValueError("'time_step' must be >= 1")
# Check chunk size
if int(chunk_size) < 1:
raise ValueError("'chunk_size' must be >= 1")
# Test that data is loaded
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')
# Convert variables and levels to appropriate type
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]
# Check for variables that have no level coordinate, and enforce pairwise if necessary
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 they're pairwise, make sure we have the same length
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)]
# Get the exact dataset we want (index times, variables, and 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')
# Arrays for scaling parameters
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)
# Sort into predictors and targets. If in_memory is false, write to netCDF.
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)
# Create spatial coordinates
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
# Create initialization time reference variable
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)
# Create predictors and targets variables
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:
# Load all the data for speed... better be careful
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()
# Fill in the data. Go through time steps. Iterate by variable and level for scaling.
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:
# Create means and stds variables
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
# Close and re-open as xarray Dataset
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
"""
# Check chunk size
if int(chunk_size) < 1:
raise ValueError("'chunk_size' must be >= 1")
# Test that data is loaded
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')
# Convert variables and levels to appropriate type
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]
# Check for variables that have no level coordinate, and enforce pairwise if necessary
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 they're pairwise, make sure we have the same length
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)]
# Get the exact dataset we want (index times, variables, and 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')
# Arrays for scaling parameters
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)
# Sort into predictors and targets. If in_memory is false, write to netCDF.
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)
# Create spatial coordinates
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
# Create initialization time reference variable
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)
# Create predictors and targets variables
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:
# Load all the data for speed... better be careful
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)
# Fill in the data. Go through time steps. Iterate by variable and level for scaling.
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:
# Create means and stds variables
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
# Close and re-open as xarray Dataset
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:
# Use -1 index because Preprocessor.data_to_samples (which generates a 'time_step' dim), assigns the
# datetime 'sample' dim based on the initialization time, time_step=-1
da = ds.predictors.isel(time_step=-1)
else:
da = ds.predictors
# Check the selections for empty selections
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}
# Try to transpose the axes so we can use basic indexing to return views
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.")
# Pre-generate the insolation data
if add_insolation:
sol = insolation(da.sample.values, ds.lat.values, ds.lon.values, daily=daily_insolation)
else:
sol = None
# Return the data if requested
if return_data:
return da.values, input_ind, output_ind, sol
else:
return input_ind, output_ind, sol