blob: 7d0dd4b194ac713d2e5b21aaa37a905c1a93fb27 [file] [log] [blame]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import datetime
import logging
import netCDF4
import numpy as np
import numpy.ma as ma
import scipy.ndimage
from matplotlib.path import Path
from scipy.interpolate import griddata
from scipy.ndimage import map_coordinates
import ocw.utils as utils
from ocw import dataset as ds
logger = logging.getLogger(__name__)
def temporal_subset(target_dataset, month_start, month_end,
average_each_year=False):
""" Temporally subset data given month_index.
:param month_start: An integer for beginning month (Jan=1)
:type month_start: :class:`int`
:param month_end: An integer for ending month (Jan=1)
:type month_end: :class:`int`
:param target_dataset: Dataset object that needs temporal subsetting
:type target_dataset: Open Climate Workbench Dataset Object
:param average_each_year: If True, output dataset is averaged for each year
:type average_each_year: :class:'boolean'
:returns: A temporal subset OCW Dataset
:rtype: Open Climate Workbench Dataset Object
"""
if month_start > month_end:
month_index = list(range(month_start, 13))
month_index.extend(list(range(1, month_end + 1)))
else:
month_index = range(month_start, month_end + 1)
dates = target_dataset.times
months = np.array([d.month for d in dates])
time_index = []
for m_value in month_index:
time_index.extend(list(np.where(months == m_value)[0]))
time_index.sort()
new_dataset = ds.Dataset(target_dataset.lats,
target_dataset.lons,
target_dataset.times[time_index],
target_dataset.values[time_index, :],
variable=target_dataset.variable,
units=target_dataset.units,
name=target_dataset.name)
if average_each_year:
new_times = new_dataset.times
nmonth = len(month_index)
ntime = new_times.size
nyear = ntime // nmonth
if ntime % nmonth != 0:
logger.warning("Number of times in dataset ({}) does not "
"divide evenly into {} year(s). Trimming data..."
.format(ntime, nyear))
slc = utils.trim_dataset(new_dataset)
new_dataset.values = new_dataset.values[slc]
new_times = new_times[slc]
nyear = new_times.size // nmonth
averaged_time = []
ny, nx = target_dataset.values.shape[1:]
averaged_values = ma.zeros([nyear, ny, nx])
for iyear in np.arange(nyear):
# centered time index of the season between month_start and
# month_end in each year
center_index = int(nmonth / 2 + iyear * nmonth)
if nmonth == 1:
center_index = iyear
averaged_time.append(new_times[center_index])
averaged_values[iyear, :] = ma.average(new_dataset.values[
nmonth * iyear: nmonth * iyear + nmonth, :], axis=0)
new_dataset = ds.Dataset(target_dataset.lats,
target_dataset.lons,
np.array(averaged_time),
averaged_values,
variable=target_dataset.variable,
units=target_dataset.units,
name=target_dataset.name)
return new_dataset
def temporal_rebin(target_dataset, temporal_resolution):
""" Rebin a Dataset to a new temporal resolution
:param target_dataset: Dataset object that needs temporal rebinned
:type target_dataset: :class:`dataset.Dataset`
:param temporal_resolution: The new temporal resolution
:type temporal_resolution: :mod:`string`
:returns: A new temporally rebinned Dataset
:rtype: :class:`dataset.Dataset`
"""
# Decode the temporal resolution into a string format that
# _rcmes_calc_average_on_new_time_unit_K() can understand
binned_values, binned_dates = _rcmes_calc_average_on_new_time_unit(
target_dataset.values, target_dataset.times, temporal_resolution)
binned_dates = np.array(binned_dates)
new_dataset = ds.Dataset(target_dataset.lats,
target_dataset.lons,
binned_dates,
binned_values,
variable=target_dataset.variable,
units=target_dataset.units,
name=target_dataset.name,
origin=target_dataset.origin)
return new_dataset
def temporal_rebin_with_time_index(target_dataset, nt_average):
""" Rebin a Dataset to a new temporal resolution
:param target_dataset: Dataset object that needs temporal rebinned
:type target_dataset: :class:`dataset.Dataset`
:param nt_average: Time resolution for the output datasets.
It is the same as the number of time indicies to be averaged.
length of time dimension in the rebinned dataset) =
(original time dimension length/nt_average)
:type nt_average: integer
:returns: A new temporally rebinned Dataset
:rtype: :class:`dataset.Dataset`
"""
nt = target_dataset.times.size
if nt % nt_average != 0:
logger.warning('Length of time dimension must '
'be a multiple of nt_average')
# nt2 is the length of time dimension in the rebinned dataset
nt2 = nt // nt_average
binned_dates = target_dataset.times[np.arange(nt2) * nt_average]
binned_values = ma.zeros(
np.insert(target_dataset.values.shape[1:], 0, nt2))
for it in np.arange(nt2):
binned_values[it, :] = ma.average(
target_dataset.values[nt_average * it:
nt_average * it + nt_average,
:],
axis=0)
new_dataset = ds.Dataset(target_dataset.lats,
target_dataset.lons,
binned_dates,
binned_values,
variable=target_dataset.variable,
units=target_dataset.units,
name=target_dataset.name,
origin=target_dataset.origin)
return new_dataset
def spatial_regrid(target_dataset, new_latitudes, new_longitudes,
boundary_check=True):
""" Regrid a Dataset using the new latitudes and longitudes
:param target_dataset: Dataset object that needs spatially regridded
:type target_dataset: :class:`dataset.Dataset`
:param new_latitudes: Array of latitudes
:type new_latitudes: :class:`numpy.ndarray`
:param new_longitudes: Array of longitudes
:type new_longitudes: :class:`numpy.ndarray`
:param boundary_check: Check if the regriding domain's boundaries
are outside target_dataset's domain
:type boundary_check: :class:'bool'
:returns: A new spatially regridded Dataset
:rtype: :class:`dataset.Dataset`
"""
# Create grids of the given lats and lons for the underlying API
# NOTE: np.meshgrid() requires inputs (x, y) and returns data
# of shape(y|lat|rows, x|lon|columns). So we pass in lons, lats
# and get back data.shape(lats, lons)
if target_dataset.lons.ndim == 1 and target_dataset.lats.ndim == 1:
regular_grid = True
lons, lats = np.meshgrid(target_dataset.lons, target_dataset.lats)
else:
regular_grid = False
lons = target_dataset.lons
lats = target_dataset.lats
if new_longitudes.ndim == 1 and new_latitudes.ndim == 1:
new_lons, new_lats = np.meshgrid(new_longitudes, new_latitudes)
else:
new_lons = new_longitudes
new_lats = new_latitudes
ny_old, nx_old = lats.shape
ny_new, nx_new = new_lats.shape
for iy in np.arange(ny_old):
if not all(x < y for x, y in zip(lons[iy, :], lons[iy, 1:])):
lons[iy, :][lons[iy, :] < 0] = lons[iy, :][lons[iy, :] < 0] + 360.
# Make masked array of shape (times, new_latitudes,new_longitudes)
new_values = ma.zeros([len(target_dataset.times),
ny_new, nx_new])+1.e+20
# Boundary vertices of target_dataset
vertices = []
if regular_grid:
vertices.append([lons[0, 0], lats[0, 0]])
vertices.append([lons[-1, 0], lats[-1, 0]])
vertices.append([lons[-1, -1], lats[-1, -1]])
vertices.append([lons[0, -1], lats[0, -1]])
else:
# from south to north along the west boundary
for iy in np.arange(ny_old):
vertices.append([lons[iy, 0], lats[iy, 0]])
# from west to east along the north boundary
for ix in np.arange(nx_old):
vertices.append([lons[-1, ix], lats[-1, ix]])
# from north to south along the east boundary
for iy in np.arange(ny_old)[::-1]:
vertices.append([lons[iy, -1], lats[iy, -1]])
# from east to west along the south boundary
for ix in np.arange(nx_old)[::-1]:
vertices.append([lons[0, ix], lats[0, ix]])
path = Path(vertices)
new_xy_mask = np.ones(new_lats.shape)
for iy in np.arange(ny_new):
for ix in np.arange(nx_new):
if path.contains_point([new_lons[iy, ix],
new_lats[iy, ix]]) or not boundary_check:
new_xy_mask[iy, ix] = 0.
new_index = np.where(new_xy_mask == 0.)
# Regrid the data on each time slice
for i in range(len(target_dataset.times)):
if len(target_dataset.times) == 1 and target_dataset.values.ndim == 2:
values_original = ma.array(target_dataset.values)
else:
values_original = ma.array(target_dataset.values[i])
new_mask = np.copy(values_original.mask)
for shift in (-1, 1):
for axis in (0, 1):
q_shifted = np.roll(values_original, shift=shift, axis=axis)
if (np.where((values_original.mask == True) & (q_shifted.mask == False)))[0].size !=0:
index1 =np.where((values_original.mask == True) & (q_shifted.mask == False))
n_indices = len(index1[0])
values_original.data[index1] = q_shifted[index1]
new_mask[index1] = np.repeat(False, n_indices)
mask_index = np.where(~new_mask)
if new_mask.size != 1:
mask_index = np.where(~new_mask)
else:
mask_index = np.where(~np.isnan(values_original))
new_values_temp = griddata((lons[mask_index], lats[mask_index]), values_original[mask_index],
(new_lons[new_index],
new_lats[new_index]),
method='linear')
# Make a masking map using nearest neighbour interpolation -use this to
# determine locations with MDI and mask these
qmdi = np.zeros_like(values_original)
values_true_indices = np.where(values_original.mask == True)
values_false_indices = np.where(values_original.mask == False)
qmdi[values_true_indices] = 1.
qmdi[values_false_indices] = 0.
qmdi_r = griddata((lons.flatten(), lats.flatten()), qmdi.flatten(),
(new_lons[new_index],
new_lats[new_index]),
method='nearest')
new_values_temp = ma.masked_where(qmdi_r != 0.0, new_values_temp)
# Combine missing data mask, with outside domain mask define above.
new_values[i, new_index[0], new_index[1]] = new_values_temp[:]
new_values[i,:] = ma.masked_equal(new_values[i,:], 1.e+20)
# TODO:
# This will call down to the _congrid() function and the lat and lon
# axis will be adjusted with the time axis being held constant
# Create a new Dataset Object to return using new data
regridded_dataset = ds.Dataset(new_latitudes,
new_longitudes,
target_dataset.times,
new_values,
variable=target_dataset.variable,
units=target_dataset.units,
name=target_dataset.name,
origin=target_dataset.origin)
return regridded_dataset
def ensemble(datasets):
"""
Generate a single dataset which is the mean of the input datasets
An ensemble datasets combines input datasets assuming the all have
similar shape, dimensions, and units.
:param datasets: Datasets to be used to compose the ensemble dataset from.
All Datasets must be the same shape.
:type datasets: :class:`list` of :class:`dataset.Dataset`
:returns: New Dataset with a name of 'Dataset Ensemble'
:rtype: :class:`dataset.Dataset`
"""
_check_dataset_shapes(datasets)
dataset_values = [dataset.values for dataset in datasets]
ensemble_values = ma.mean(dataset_values, axis=0)
# Build new dataset object from the input datasets and
# the ensemble values and return it
ensemble_dataset = ds.Dataset(datasets[0].lats,
datasets[0].lons,
datasets[0].times,
ensemble_values,
units=datasets[0].units,
name="Dataset Ensemble")
return ensemble_dataset
def subset(target_dataset, subregion, subregion_name=None, extract=True, user_mask_values=[1]):
'''Subset given dataset(s) with subregion information
:param subregion: The Bounds with which to subset the target Dataset.
:type subregion: :class:`dataset.Bounds`
:param target_dataset: The Dataset object to subset.
:type target_dataset: :class:`dataset.Dataset`
:param subregion_name: The subset-ed Dataset name
:type subregion_name: :mod:`string`
:param extract: If False, the dataset inside regions will be masked.
:type extract: :mod:`boolean`
:param user_mask_value: grid points where mask_variable == user_mask_value will be extracted or masked .
:type user_mask_value: :mod:`int`
:returns: The subset-ed Dataset object
:rtype: :class:`dataset.Dataset`
:raises: ValueError
'''
if not subregion.start:
time_start = target_dataset.times[0]
time_end = target_dataset.times[-1]
else:
time_start = subregion.start
time_end = subregion.end
if not subregion_name:
subregion_name = target_dataset.name
if subregion.lat_min is not None:
_are_bounds_contained_by_dataset(target_dataset, subregion)
if target_dataset.lats.ndim == 2 and target_dataset.lons.ndim == 2:
temporal_subset = temporal_slice(
target_dataset, time_start, time_end)
nt = temporal_subset.values.shape[0]
y_index, x_index = np.where(
(target_dataset.lats >= subregion.lat_max) | (
target_dataset.lats <= subregion.lat_min) |
(target_dataset.lons >= subregion.lon_max) | (
target_dataset.lons <= subregion.lon_min))
for it in np.arange(nt):
temporal_subset.values[it, y_index, x_index] = 1.e+20
new_values = ma.masked_equal(
temporal_subset.values, 1.e+20)
return ds.Dataset(
target_dataset.lats,
target_dataset.lons,
temporal_subset.times,
new_values,
variable=target_dataset.variable,
units=target_dataset.units,
name=subregion_name,
origin=target_dataset.origin)
elif target_dataset.lats.ndim == 1 and target_dataset.lons.ndim == 1:
# Get subregion indices into subregion data
dataset_slices = _get_subregion_slice_indices(target_dataset,
subregion)
# Slice the values array with our calculated slice indices
if target_dataset.values.ndim == 2:
subset_values = ma.zeros([len(target_dataset.values[
dataset_slices["lat_start"]:dataset_slices["lat_end"]]),
len(target_dataset.values[
dataset_slices["lon_start"]:dataset_slices["lon_end"]])])
subset_values = target_dataset.values[
dataset_slices["lat_start"]:dataset_slices["lat_end"] + 1,
dataset_slices["lon_start"]:dataset_slices["lon_end"] + 1]
elif target_dataset.values.ndim == 3:
subset_values = ma.zeros([len(target_dataset.values[
dataset_slices["time_start"]:dataset_slices["time_end"]]),
len(target_dataset.values[
dataset_slices["lat_start"]:dataset_slices["lat_end"]]),
len(target_dataset.values[
dataset_slices["lon_start"]:dataset_slices["lon_end"]])])
subset_values = target_dataset.values[
dataset_slices["time_start"]:dataset_slices["time_end"] + 1,
dataset_slices["lat_start"]:dataset_slices["lat_end"] + 1,
dataset_slices["lon_start"]:dataset_slices["lon_end"] + 1]
# Build new dataset with subset information
return ds.Dataset(
# Slice the lats array with our calculated slice indices
target_dataset.lats[dataset_slices["lat_start"]:
dataset_slices["lat_end"] + 1],
# Slice the lons array with our calculated slice indices
target_dataset.lons[dataset_slices["lon_start"]:
dataset_slices["lon_end"] + 1],
# Slice the times array with our calculated slice indices
target_dataset.times[dataset_slices["time_start"]:
dataset_slices["time_end"] + 1],
# Slice the values array with our calculated slice indices
subset_values,
variable=target_dataset.variable,
units=target_dataset.units,
name=subregion_name,
origin=target_dataset.origin
)
if subregion.boundary_type == 'us_states' or subregion.boundary_type == 'countries':
temporal_subset = temporal_slice(
target_dataset, time_start, time_end)
spatial_mask = utils.mask_using_shapefile_info(target_dataset.lons, target_dataset.lats,
subregion.masked_regions, extract=extract)
subset_values = utils.propagate_spatial_mask_over_time(
temporal_subset.values, mask=spatial_mask)
return ds.Dataset(
target_dataset.lats,
target_dataset.lons,
temporal_subset.times,
subset_values,
variable=target_dataset.variable,
units=target_dataset.units,
name=subregion_name,
origin=target_dataset.origin)
if subregion.boundary_type == 'user':
temporal_subset = temporal_slice(
target_dataset, time_start, time_end)
spatial_mask = utils.regrid_spatial_mask(target_dataset.lons, target_dataset.lats,
subregion.mask_longitude, subregion.mask_latitude, subregion.mask_variable,
user_mask_values, extract=extract)
subset_values = utils.propagate_spatial_mask_over_time(
temporal_subset.values, mask=spatial_mask)
return ds.Dataset(
target_dataset.lats,
target_dataset.lons,
temporal_subset.times,
subset_values,
variable=target_dataset.variable,
units=target_dataset.units,
name=subregion_name,
origin=target_dataset.origin)
def temporal_slice(target_dataset, start_time, end_time):
'''Temporally slice given dataset(s) with subregion information. This does not
spatially subset the target_Dataset
:param start_time: start time
:type start_time: :class:'int'
:param end_time: end time
:type end_time: :class:'datetime.datetime'
:param target_dataset: The Dataset object to subset.
:type target_dataset: :class:`dataset.Dataset`
:returns: The subset-ed Dataset object
:rtype: :class:`dataset.Dataset`
:raises: ValueError
'''
# https://issues.apache.org/jira/browse/CLIMATE-938
# netCDF datetimes allow for a variety of calendars while Python has
# only one. This would throw an error about a calendar mismatch when
# comparing a Python datetime object to a netcdf datetime object.
# Cast the date as best we can so the comparison will compare like
# data types This will still throw an excdeption if the start / end date are
# not valid in given calendar. February 29th in a DatetimeNoLeap calendar for example.
slice_start_time = start_time
slice_end_time = end_time
if type(target_dataset.times[0]) != datetime.datetime:
slice_start_time =\
type(target_dataset.times.item(0))(start_time.year, start_time.month, start_time.day,
start_time.hour, start_time.minute, start_time.second)
slice_end_time =\
type(target_dataset.times.item(0))(end_time.year, end_time.month, end_time.day,
end_time.hour, end_time.minute, end_time.second)
start_time_index = np.where(
target_dataset.times >= slice_start_time)[0][0]
end_time_index = np.where(
target_dataset.times <= slice_end_time)[0][-1]
new_times = target_dataset.times[start_time_index:end_time_index + 1]
new_values = target_dataset.values[start_time_index:end_time_index + 1, :]
return ds.Dataset(
target_dataset.lats,
target_dataset.lons,
new_times,
new_values,
variable=target_dataset.variable,
units=target_dataset.units,
origin=target_dataset.origin)
def safe_subset(target_dataset, subregion, subregion_name=None):
'''Safely subset given dataset with subregion information
A standard subset requires that the provided subregion be entirely
contained within the datasets bounds. `safe_subset` returns the
overlap of the subregion and dataset without returning an error.
:param subregion: The Bounds with which to subset the target Dataset.
:type subregion: :class:`dataset.Bounds`
:param target_dataset: The Dataset object to subset.
:type target_dataset: :class:`dataset.Dataset`
:param subregion_name: The subset-ed Dataset name
:type subregion_name: :mod:`string`
:returns: The subset-ed Dataset object
:rtype: :class:`dataset.Dataset`
'''
lat_min, lat_max, lon_min, lon_max = target_dataset.spatial_boundaries()
start, end = target_dataset.temporal_boundaries()
if subregion.lat_min:
if subregion.lat_min < lat_min:
subregion.lat_min = lat_min
if subregion.lat_max > lat_max:
subregion.lat_max = lat_max
if subregion.lon_min < lon_min:
subregion.lon_min = lon_min
if subregion.lon_max > lon_max:
subregion.lon_max = lon_max
if subregion.start:
if subregion.start < start:
subregion.start = start
if subregion.end:
if subregion.end > end:
subregion.end = end
return subset(target_dataset, subregion, subregion_name)
def normalize_dataset_datetimes(dataset, timestep):
''' Normalize Dataset datetime values.
Force daily to an hour time value of 00:00:00.
Force monthly data to the first of the month at midnight.
:param dataset: The Dataset which will have its time value normalized.
:type dataset: :class:`dataset.Dataset`
:param timestep: The timestep of the Dataset's values. Either 'daily' or
'monthly'.
:type timestep: :mod:`string`
:returns: A new Dataset with normalized datetime values.
:rtype: :class:`dataset.Dataset`
'''
new_times = _rcmes_normalize_datetimes(dataset.times, timestep)
return ds.Dataset(
dataset.lats,
dataset.lons,
np.array(new_times),
dataset.values,
variable=dataset.variable,
units=dataset.units,
name=dataset.name,
origin=dataset.origin
)
def write_netcdf(dataset, path, compress=True):
''' Write a dataset to a NetCDF file.
:param dataset: The dataset to write.
:type dataset: :class:`dataset.Dataset`
:param path: The output file path.
:type path: :mod:`string`
'''
out_file = netCDF4.Dataset(path, 'w', format='NETCDF4')
# Set attribute lengths
if dataset.lats.ndim == 2:
lat_len = dataset.lats.shape[0]
lon_len = dataset.lons.shape[1]
lat_dim_info = ('y', 'x')
lon_dim_info = ('y', 'x')
else:
lat_len = len(dataset.lats)
lon_len = len(dataset.lons)
lat_dim_info = ('y',)
lon_dim_info = ('x',)
time_len = len(dataset.times)
# Create attribute dimensions
out_file.createDimension('y', lat_len)
out_file.createDimension('x', lon_len)
out_file.createDimension('time', None)
# Create variables
lats = out_file.createVariable('lat', 'f8', lat_dim_info, zlib=compress)
lons = out_file.createVariable('lon', 'f8', lon_dim_info, zlib=compress)
times = out_file.createVariable('time', 'f8', ('time'), zlib=compress)
var_name = dataset.variable if dataset.variable else 'var'
values = out_file.createVariable(var_name,
'f8',
('time', 'y', 'x'),
zlib=compress)
# Set the time variable units
# We don't deal with hourly/minutely/anything-less-than-a-day data so
# we can safely stick with a 'days since' offset here. Note that the
# NetCDF4 helper date2num doesn't support 'months' or 'years' instead
# of days.
times.units = "days since %s" % dataset.times[0]
# Store the dataset's values
lats[:] = dataset.lats
lons[:] = dataset.lons
times[:] = netCDF4.date2num(dataset.times, times.units)
values[:] = dataset.values
values.units = dataset.units
out_file.close()
def write_netcdf_multiple_datasets_with_subregions(ref_dataset, ref_name,
model_dataset_array,
model_names,
path,
subregions=None,
subregion_array=None,
ref_subregion_mean=None,
ref_subregion_std=None,
model_subregion_mean=None,
model_subregion_std=None):
# Write multiple reference and model datasets and their subregional means
# and standard deivations in a NetCDF file.
# :To be updated
#
out_file = netCDF4.Dataset(path, 'w', format='NETCDF4')
dataset = ref_dataset
# Set attribute lenghts
nobs = 1
nmodel = len(model_dataset_array)
lat_len, lon_len = dataset.values.shape[1:]
lat_ndim = dataset.lats.ndim
lon_ndim = dataset.lons.ndim
time_len = len(dataset.times)
if subregions is not None:
nsubregion = len(subregions)
# Create attribute dimensions
out_file.createDimension('y', lat_len)
out_file.createDimension('x', lon_len)
out_file.createDimension('time', time_len)
# Create variables and store the values
if lat_ndim == 2:
lats = out_file.createVariable('lat', 'f8', ('y', 'x'))
else:
lats = out_file.createVariable('lat', 'f8', ('y'))
lats[:] = dataset.lats
if lon_ndim == 2:
lons = out_file.createVariable('lon', 'f8', ('y', 'x'))
else:
lons = out_file.createVariable('lon', 'f8', ('x'))
lons[:] = dataset.lons
times = out_file.createVariable('time', 'f8', ('time',))
times.units = "days since %s" % dataset.times[0]
times[:] = netCDF4.date2num(dataset.times, times.units)
# mask_array = np.zeros([time_len, lat_len, lon_len])
# for iobs in np.arange(nobs):
# index = np.where(ref_dataset_array[iobs].values.mask[:] == True)
# mask_array[index] = 1
out_file.createVariable(ref_name, 'f8', ('time', 'y', 'x'))
out_file.variables[ref_name][:] = ref_dataset.values
out_file.variables[ref_name].units = ref_dataset.units
for imodel in np.arange(nmodel):
out_file.createVariable(model_names[imodel], 'f8', ('time', 'y', 'x'))
# out_file.variables[model_names[imodel]][:] = ma.array(
# model_dataset_array[imodel].values, mask = mask_array)
out_file.variables[model_names[imodel]][:] = model_dataset_array[
imodel].values
out_file.variables[model_names[imodel]].units = model_dataset_array[
imodel].units
if subregions is not None:
out_file.createVariable('subregion_array', 'i4', ('y', 'x'))
out_file.variables['subregion_array'][:] = subregion_array[:]
nsubregion = len(subregions)
out_file.createDimension('nsubregion', nsubregion)
out_file.createDimension('nobs', nobs)
out_file.createDimension('nmodel', nmodel)
out_file.createVariable('obs_subregion_mean', 'f8', ('nobs',
'time',
'nsubregion'))
out_file.variables['obs_subregion_mean'][:] = ref_subregion_mean[:]
out_file.createVariable('obs_subregion_std', 'f8', ('nobs',
'time',
'nsubregion'))
out_file.variables['obs_subregion_std'][:] = ref_subregion_std[:]
out_file.createVariable('model_subregion_mean', 'f8', ('nmodel',
'time',
'nsubregion'))
out_file.variables['model_subregion_mean'][:] = model_subregion_mean[:]
out_file.createVariable('model_subregion_std', 'f8', ('nmodel',
'time',
'nsubregion'))
out_file.variables['model_subregion_std'][:] = model_subregion_std[:]
out_file.close()
def water_flux_unit_conversion(dataset):
''' Convert water flux variables units as necessary
Convert full SI units water flux units to more common units.
:param dataset: The dataset to convert.
:type dataset: :class:`dataset.Dataset`
:returns: A Dataset with values converted to new units.
:rtype: :class:`dataset.Dataset`
'''
water_flux_variables = ['pr', 'prec', 'evspsbl', 'mrro', 'swe']
variable = dataset.variable.lower()
if any(sub_string in variable for sub_string in water_flux_variables):
dataset_units = dataset.units.lower()
if variable in 'swe':
if any(unit in dataset_units for unit in ['m', 'meter']):
dataset.values = 1.e3 * dataset.values
dataset.units = 'km'
else:
if any(unit in dataset_units
for unit in ['kg m-2 s-1', 'mm s-1', 'mm/sec']):
dataset.values = 86400. * dataset.values
dataset.units = 'mm/day'
return dataset
def temperature_unit_conversion(dataset):
''' Convert temperature units as necessary \
Automatically convert Celcius to Kelvin in the given dataset.
:param dataset: The dataset for which units should be updated. \
:type dataset; :class:`dataset.Dataset`
:returns: The dataset with (potentially) updated units. \
:rtype: :class:`dataset.Dataset`
'''
temperature_variables = ['temp', 'tas', 'tasmax', 'taxmin', 'T', 'tg']
variable = dataset.variable.lower()
if any(sub_string in variable for sub_string in temperature_variables):
dataset_units = dataset.units.lower()
if dataset_units == 'c' or dataset_units == 'celsius':
dataset.values = 273.15 + dataset.values
dataset.units = 'K'
return dataset
def variable_unit_conversion(dataset):
''' Convert water flux or temperature variables units as necessary
For water flux variables, convert full SI units water flux units
to more common units.
For temperature, convert Celcius to Kelvin.
:param dataset: The dataset to convert.
:type dataset: :class:`dataset.Dataset`
:returns: A Dataset with values converted to new units.
:rtype: :class:`dataset.Dataset`
'''
dataset = water_flux_unit_conversion(dataset)
dataset = temperature_unit_conversion(dataset)
return dataset
def _rcmes_normalize_datetimes(datetimes, timestep):
""" Normalize Dataset datetime values.
Force daily to an hour time value of 00:00:00.
Force monthly data to the first of the month at midnight.
:param datetimes: The datetimes to normalize.
:type datetimes: List of `datetime` values.
:param timestep: The flag for how to normalize the datetimes.
:type timestep: String
"""
normalDatetimes = []
if timestep.lower() == 'monthly':
for inputDatetime in datetimes:
if inputDatetime.day != 1:
# Clean the inputDatetime
inputDatetimeString = inputDatetime.strftime('%Y%m%d')
normalInputDatetimeString = inputDatetimeString[:6] + '01'
inputDatetime = datetime.datetime.strptime(
normalInputDatetimeString, '%Y%m%d')
normalDatetimes.append(inputDatetime)
elif timestep.lower() == 'daily':
for inputDatetime in datetimes:
if (inputDatetime.hour != 0 or inputDatetime.minute != 0 or
inputDatetime.second != 0):
datetimeString = inputDatetime.strftime('%Y%m%d%H%M%S')
normalDatetimeString = datetimeString[:8] + '000000'
inputDatetime = datetime.datetime.strptime(
normalDatetimeString, '%Y%m%d%H%M%S')
normalDatetimes.append(inputDatetime)
return normalDatetimes
def mask_missing_data(dataset_array):
''' Check missing values in observation and model datasets.
If any of dataset in dataset_array has missing values at a grid point,
the values at the grid point in all other datasets are masked.
:param dataset_array: an array of OCW datasets
'''
mask_array = np.zeros(dataset_array[0].values.shape)
for dataset in dataset_array:
# CLIMATE-797 - Not every array passed in will be a masked array.
# For those that are, action based on the mask passed in.
# For those that are not, take no action (else AttributeError).
if hasattr(dataset.values, 'mask'):
index = np.where(dataset.values.mask == True)
if index[0].size > 0:
mask_array[index] = 1
masked_array = []
for dataset in dataset_array:
dataset.values = ma.array(dataset.values, mask=mask_array)
masked_array.append(dataset)
return [masked_dataset for masked_dataset in masked_array]
def deseasonalize_dataset(dataset):
'''Calculate daily climatology and subtract the climatology from
the input dataset
:param dataset: The dataset to convert.
:type dataset: :class:`dataset.Dataset`
:returns: A Dataset with values deseasonalized.
:rtype: :class:`dataset.Dataset`
'''
days = [d.month * 100. + d.day for d in dataset.times]
days_sorted = np.unique(days)
ndays = days_sorted.size
nt, ny, nx = dataset.values.shape
values_clim = ma.zeros([ndays, ny, nx])
for iday, day in enumerate(days_sorted):
t_index = np.where(days == day)[0]
values_clim[iday, :] = ma.mean(dataset.values[t_index, :], axis=0)
for iday, day in enumerate(days_sorted):
t_index = np.where(days == day)[0]
dataset.values[t_index, :] = dataset.values[
t_index, :] - values_clim[iday, :]
return dataset
def _rcmes_spatial_regrid(spatial_values, lat, lon, lat2, lon2, order=1):
'''
Spatial regrid from one set of lat,lon values onto a new set (lat2,lon2)
:param spatial_values: Values in a spatial grid that need to be regridded
:type spatial_values: 2d masked numpy array. shape (latitude, longitude)
:param lat: Grid of latitude values which map to the spatial values
:type lat: 2d numpy array. shape(latitudes, longitudes)
:param lon: Grid of longitude values which map to the spatial values
:type lon: 2d numpy array. shape(latitudes, longitudes)
:param lat2: Grid of NEW latitude values to regrid the spatial_values onto
:type lat2: 2d numpy array. shape(latitudes, longitudes)
:param lon2: Grid of NEW longitude values to regrid the spatial_values onto
:type lon2: 2d numpy array. shape(latitudes, longitudes)
:param order: Interpolation order flag. 1=bi-linear, 3=cubic spline
:type order: [optional] Integer
:returns: 2d masked numpy array with shape(len(lat2), len(lon2))
:rtype: (float, float)
'''
nlat = spatial_values.shape[0]
nlon = spatial_values.shape[1]
nlat2 = lat2.shape[0]
nlon2 = lon2.shape[1]
# To make our lives easier down the road, let's
# turn these into arrays of x & y coords
loni = lon2.ravel()
lati = lat2.ravel()
loni = loni.copy() # NB. it won't run unless you do this...
lati = lati.copy()
# Now, we'll set points outside the boundaries to lie along an edge
loni_max_indices = np.where(loni > lon.max())
loni_min_indices = np.where(loni < lon.min())
loni[loni_max_indices] = lon.max()
loni[loni_min_indices] = lon.min()
# To deal with the "hard" break, we'll have to treat y differently,
# so we're just setting the min here...
lati_max_indices = np.where(lati > lat.max())
lati_min_indices = np.where(lati < lat.min())
lati[lati_max_indices] = lat.max()
lati[lati_min_indices] = lat.min()
# We need to convert these to (float) indicies
# (xi should range from 0 to (nx - 1), etc)
loni = (nlon - 1) * (loni - lon.min()) / (lon.max() - lon.min())
# Deal with the "hard" break in the y-direction
lati = (nlat - 1) * (lati - lat.min()) / (lat.max() - lat.min())
"""
TODO: Review this docstring and see if it still holds true.
NOTE: This function doesn't use MDI currently. These are legacy comments
Notes on dealing with MDI when regridding data.
Method adopted here:
Use bilinear interpolation of data by default
(but user can specify other order using order=... in call)
Perform bilinear interpolation of data, and of mask.
To be conservative, new grid point which contained some missing data on
the old grid is set to missing data.
-this is achieved by looking for any non-zero interpolated
mask values.
To avoid issues with bilinear interpolation producing strong gradients
leading into the MDI, set values at MDI points to mean data value so
little gradient visible = not ideal, but acceptable for now.
Set values in MDI so that similar to surroundings so don't produce large
gradients when interpolating Preserve MDI mask, by only changing data part
of masked array object.
"""
for shift in (-1, 1):
for axis in (0, 1):
q_shifted = np.roll(spatial_values, shift=shift, axis=axis)
idx = ~q_shifted.mask * spatial_values.mask
indices = np.where(idx)
spatial_values.data[indices] = q_shifted[indices]
# Now we actually interpolate
# map_coordinates does cubic interpolation by default,
# use "order=1" to preform bilinear interpolation instead...
regridded_values = map_coordinates(spatial_values,
[lati, loni],
order=order)
regridded_values = regridded_values.reshape([nlat2, nlon2])
# Set values to missing data outside of original domain
regridded_values = ma.masked_array(regridded_values,
mask=np.logical_or(
np.logical_or(lat2 >= lat.max(),
lat2 <= lat.min()),
np.logical_or(lon2 <= lon.min(),
lon2 >= lon.max())))
# Make second map using nearest neighbour interpolation -use this to
# determine locations with MDI and mask these
qmdi = np.zeros_like(spatial_values)
spatial_indices_true = np.where(spatial_values.mask)
spatial_indices_false = np.where(not spatial_values.mask)
qmdi[spatial_indices_true] = 1.
qmdi[spatial_indices_false] = 0.
qmdi_r = map_coordinates(qmdi, [lati, loni], order=order)
qmdi_r = qmdi_r.reshape([nlat2, nlon2])
mdimask = (qmdi_r != 0.0)
# Combine missing data mask, with outside domain mask define above.
regridded_values.mask = np.logical_or(mdimask, regridded_values.mask)
return regridded_values
def _rcmes_create_mask_using_threshold(masked_array, threshold=0.5):
'''Mask an array if percent of values missing data is above a threshold.
For each value along an axis, if the proportion of steps that are missing
data is above ``threshold`` then the value is marked as missing data.
..note:: The 0th axis is currently always used.
:param masked_array: Masked array of data
:type masked_array: Numpy Masked Array
:param threshold: (optional) Threshold proportion above which a value is
marked as missing data.
:type threshold: Float
:returns: A Numpy array describing the mask for masked_array.
'''
# try, except used as some model files don't have a full mask, but a single
# bool the except catches this situation and deals with it appropriately.
try:
nT = masked_array.mask.shape[0]
# For each pixel, count how many times are masked.
nMasked = masked_array.mask[:, :, :].sum(axis=0)
# Define new mask as when a pixel has over a defined threshold ratio of
# masked data
# e.g. if the threshold is 75%, and there are 10 times,
# then a pixel will be masked if more than 5 times are masked.
mymask = nMasked > (nT * threshold)
except:
mymask = np.zeros_like(masked_array.data[0, :, :])
return mymask
def _rcmes_calc_average_on_new_time_unit(data, dates, unit):
""" Rebin 3d array and list of dates using the provided unit parameter
:param data: Input data that needs to be averaged
:type data: 3D masked numpy array of shape (times, lats, lons)
:param dates: List of dates that correspond to the given data values
:type dates: Python datetime objects
:param unit: Time unit to average the data into
:type unit: String matching one of these values :
full | annual | monthly | daily
:returns: meanstorem, newTimesList
:rtype: 3D numpy masked array the same shape as the input array,
list of python datetime objects
:raises: ValueError
"""
# Check if the user-selected temporal grid is valid. If not, EXIT
acceptable = ((unit == 'full') | (unit == 'annual') |
(unit == 'monthly') | (unit == 'daily'))
if not acceptable:
raise ValueError('Error: unknown unit type selected '
'for time averaging: EXIT')
nt, ny, nx = data.shape
if unit == 'full':
new_data = ma.mean(data, axis=0)
new_date = [dates[dates.size // 2]]
if unit == 'annual':
years = [d.year for d in dates]
years_sorted = np.unique(years)
new_data = ma.zeros([years_sorted.size, ny, nx])
it = 0
new_date = []
for year in years_sorted:
index = np.where(years == year)[0]
new_data[it, :] = ma.mean(data[index, :], axis=0)
new_date.append(datetime.datetime(year=year, month=7, day=2))
it = it + 1
if unit == 'monthly':
years = [d.year for d in dates]
years_sorted = np.unique(years)
months = [d.month for d in dates]
months_sorted = np.unique(months)
new_data = ma.zeros([years_sorted.size * months_sorted.size, ny, nx])
it = 0
new_date = []
for year in years_sorted:
for month in months_sorted:
index = np.where((years == year) & (months == month))[0]
new_data[it, :] = ma.mean(data[index, :], axis=0)
new_date.append(datetime.datetime(year=year,
month=month,
day=15))
it = it + 1
if unit == 'daily':
days = [d.year * 10000. + d.month * 100. + d.day for d in dates]
days_sorted = np.unique(days)
new_data = ma.zeros([days_sorted.size, ny, nx])
it = 0
new_date = []
for day in days_sorted:
index = np.where(days == day)[0]
new_data[it, :] = ma.mean(data[index, :], axis=0)
y = int(day // 10000)
m = int(day % 10000) // 100
d = int(day % 100)
new_date.append(datetime.datetime(year=y, month=m, day=d))
it = it + 1
return new_data, np.array(new_date)
def _rcmes_calc_average_on_new_time_unit_K(data, dates, unit):
""" Rebin 3d array and list of dates using the provided unit parameter
:param data: Input data that needs to be averaged
:type data: 3D masked numpy array of shape (times, lats, lons)
:param dates: List of dates that correspond to the given data values
:type dates: Python datetime objects
:param unit: Time unit to average the data into
:type unit: String matching one of these values :
full | annual | monthly | daily
:returns: meanstorem, newTimesList
:rtype: 3D numpy masked array the same shape as the input array,
list of python datetime objects
:raises: ValueError
"""
# Check if the user-selected temporal grid is valid. If not, EXIT
acceptable = ((unit == 'full') | (unit == 'annual') |
(unit == 'monthly') | (unit == 'daily'))
if not acceptable:
raise ValueError('Error: unknown unit type selected '
'for time averaging: EXIT')
# Calculate arrays of: annual timeseries: year (2007,2007),
# monthly time series: year-month (200701,200702),
# daily timeseries: year-month-day (20070101,20070102)
# depending on user-selected averaging period.
# Year list
if unit == 'annual':
timeunits = np.array([int(d.strftime("%Y")) for d in dates])
unique_times = np.unique(timeunits)
# YearMonth format list
if unit == 'monthly':
timeunits = np.array([int(d.strftime("%Y%m")) for d in dates])
unique_times = np.unique(timeunits)
# YearMonthDay format list
if unit == 'daily':
timeunits = np.array([int(d.strftime("%Y%m%d")) for d in dates])
unique_times = np.unique(timeunits)
# TODO: add pentad setting using Julian days?
# Full list: a special case
if unit == 'full':
# Calculating means data over the entire time range:
# i.e., annual-mean climatology
timeunits = []
for i in np.arange(len(dates)):
# i.e. we just want the same value for all times.
timeunits.append(999)
timeunits = np.array(timeunits, dtype=int)
unique_times = np.unique(timeunits)
# empty list to store new times
newTimesList = []
# Decide whether or not you need to do any time averaging.
# i.e. if data are already on required time unit then just
# pass data through and calculate and return representative
# datetimes.
processing_required = True
if len(timeunits) == (len(unique_times)):
processing_required = False
# 1D data arrays, i.e. time series
if data.ndim == 1:
# Create array to store the resulting data
meanstore = np.zeros(len(unique_times))
# Calculate the means across each unique time unit
i = 0
for myunit in unique_times:
if processing_required:
datam = ma.masked_array(data, timeunits != myunit)
meanstore[i] = ma.average(datam)
# construct new times list
yyyy, mm, dd = _create_new_year_month_day(myunit, dates)
newTimesList.append(datetime.datetime(yyyy, mm, dd, 0, 0, 0, 0))
i = i + 1
# 3D data arrays
if data.ndim == 3:
# Create array to store the resulting data
meanstore = np.zeros([len(unique_times), data.shape[1], data.shape[2]])
# Calculate the means across each unique time unit
i = 0
datamask_store = []
for myunit in unique_times:
if processing_required:
mask = np.zeros_like(data)
mask[timeunits != myunit, :, :] = 1.0
# Calculate missing data mask within each time unit...
datamask_at_this_timeunit = np.zeros_like(data)
datamask_at_this_timeunit[:] = _rcmes_create_mask_using_threshold(
data[timeunits == myunit, :, :], threshold=0.75)
# Store results for masking later
datamask_store.append(datamask_at_this_timeunit[0])
# Calculate means for each pixel in this time unit, ignoring
# missing data (using masked array).
datam = ma.masked_array(data, np.logical_or(mask,
datamask_at_this_timeunit))
meanstore[i, :, :] = ma.average(datam, axis=0)
# construct new times list
yyyy, mm, dd = _create_new_year_month_day(myunit, dates)
newTimesList.append(datetime.datetime(yyyy, mm, dd))
i += 1
if not processing_required:
meanstorem = data
if processing_required:
# Create masked array (using missing data mask defined above)
datamask_store = np.array(datamask_store)
meanstorem = ma.masked_array(meanstore, datamask_store)
return meanstorem, newTimesList
def _create_new_year_month_day(time_unit, dates):
smyunit = str(time_unit)
if len(smyunit) == 4: # YYYY
yyyy = int(smyunit[0:4])
mm = 1
dd = 1
elif len(smyunit) == 6: # YYYYMM
yyyy = int(smyunit[0:4])
mm = int(smyunit[4:6])
dd = 1
elif len(smyunit) == 8: # YYYYMMDD
yyyy = int(smyunit[0:4])
mm = int(smyunit[4:6])
dd = int(smyunit[6:8])
elif len(smyunit) == 3: # Full time range
# Need to set an appropriate time representing the mid-point of the
# entire time span
dt = dates[-1] - dates[0]
halfway = dates[0] + (dt / 2)
yyyy = int(halfway.year)
mm = int(halfway.month)
dd = int(halfway.day)
return (yyyy, mm, dd)
def _congrid(a, newdims, method='linear', centre=False, minusone=False):
'''
This function is from http://wiki.scipy.org/Cookbook/Rebinning - Example 3
It has been refactored and changed a bit, but the original functionality
has been preserved.
Arbitrary resampling of source array to new dimension sizes.
Currently only supports maintaining the same number of dimensions.
To use 1-D arrays, first promote them to shape (x,1).
Uses the same parameters and creates the same co-ordinate lookup points
as IDL''s congrid routine, which apparently originally came from a VAX/VMS
routine of the same name.
method:
neighbour - closest value from original data
nearest and linear - uses n x 1-D interpolations using
scipy.interpolate.interp1d
(see Numerical Recipes for validity of use of n 1-D interpolations)
spline - uses ndimage.map_coordinates
centre:
True - interpolation points are at the centres of the bins
False - points are at the front edge of the bin
minusone:
For example- inarray.shape = (i,j) & new dimensions = (x,y)
False - inarray is resampled by factors of (i/x) * (j/y)
True - inarray is resampled by(i-1)/(x-1) * (j-1)/(y-1)
This prevents extrapolation one element beyond bounds of input array.
'''
if a.dtype not in [np.float64, np.float32]:
a = np.cast[float](a)
# this will merely take the True/False input and convert it to an
# array(1) or array(0)
m1 = np.cast[int](minusone)
# this also casts the True False input into a floating point number
# of 0.5 or 0.0
ofs = np.cast[int](centre) * 0.5
old = np.array(a.shape)
ndims = len(a.shape)
if len(newdims) != ndims:
print("[congrid] dimensions error. "
"This routine currently only supports "
"rebinning to the same number of dimensions.")
return None
newdims = np.asarray(newdims, dtype=float)
dimlist = []
if method == 'neighbour':
newa = _congrid_neighbor(a, newdims, m1, ofs)
elif method in ['nearest', 'linear']:
# calculate new dims
for i in range(ndims):
base = np.arange(newdims[i])
dimlist.append((old[i] - m1) / (newdims[i] - m1) *
(base + ofs) - ofs)
# specify old dims
olddims = [np.arange(i, dtype=np.float) for i in list(a.shape)]
# first interpolation - for ndims = any
mint = scipy.interpolate.interp1d(olddims[-1], a, kind=method)
newa = mint(dimlist[-1])
trorder = [ndims - 1] + range(ndims - 1)
for i in range(ndims - 2, -1, -1):
newa = newa.transpose(trorder)
mint = scipy.interpolate.interp1d(olddims[i], newa, kind=method)
newa = mint(dimlist[i])
if ndims > 1:
# need one more transpose to return to original dimensions
newa = newa.transpose(trorder)
return newa
elif method in ['spline']:
nslices = [slice(0, j) for j in list(newdims)]
newcoords = np.mgrid[nslices]
newcoords_dims = range(np.rank(newcoords))
# make first index last
newcoords_dims.append(newcoords_dims.pop(0))
newcoords_tr = newcoords.transpose(newcoords_dims)
# makes a view that affects newcoords
newcoords_tr += ofs
deltas = (np.asarray(old) - m1) / (newdims - m1)
newcoords_tr *= deltas
newcoords_tr -= ofs
newa = scipy.ndimage.map_coordinates(a, newcoords)
return newa
else:
print("Congrid error: Unrecognized interpolation type.\n",
"Currently only \'neighbour\', \'nearest\',\'linear\',",
"and \'spline\' are supported.")
return None
def _check_dataset_shapes(datasets):
""" If the datasets are not the same shape throw a ValueError Exception
:param datasets: OCW Datasets to check for a consistent shape
:type datasets: List of OCW Dataset Objects
:raises: ValueError
"""
dataset_shape = None
for dataset in datasets:
if dataset_shape is None:
dataset_shape = dataset.values.shape
else:
if dataset.values.shape != dataset_shape:
msg = "%s != %s" % (dataset.values.shape, dataset_shape)
raise ValueError("Input datasets must be the same shape "
"for an ensemble :: ", msg)
else:
pass
def _congrid_neighbor(values, new_dims, minus_one, offset):
""" Use the nearest neighbor to create a new array
:param values: Array of values that need to be interpolated
:type values: Numpy ndarray
:param new_dims: Longitude resolution in degrees
:type new_dims: float
:param lat_resolution: Latitude resolution in degrees
:type lat_resolution: float
:returns: A new spatially regridded Dataset
:rtype: Open Climate Workbench Dataset Object
"""
ndims = len(values.shape)
dimlist = []
old_dims = np.array(values.shape)
for i in range(ndims):
base = np.indices(new_dims)[i]
dimlist.append((old_dims[i] - minus_one) / (new_dims[i] - minus_one) *
(base + offset) - offset)
cd = np.array(dimlist).round().astype(int)
new_values = values[list(cd)]
return new_values
def _are_bounds_contained_by_dataset(dataset, bounds):
'''Check if a Dataset fully contains a bounds.
:param bounds: The Bounds object to check.
:type bounds: Bounds
:param dataset: The Dataset that should be fully contain the
Bounds
:type dataset: Dataset
:returns: True if the Bounds are contained by the Dataset, Raises
a ValueError otherwise
'''
lat_min, lat_max, lon_min, lon_max = dataset.spatial_boundaries()
start, end = dataset.temporal_boundaries()
errors = []
# TODO: THIS IS TERRIBLY inefficent and we need to use a geometry
# lib instead in the future
if (lat_min > bounds.lat_max):
error = ("bounds.lat_max: %s is not between lat_min: %s and"
" lat_max: %s" % (bounds.lat_max, lat_min, lat_max))
errors.append(error)
if (lon_min > bounds.lon_max):
error = ("bounds.lon_max: %s is not between lon_min: %s and"
" lon_max: %s" % (bounds.lon_max, lon_min, lon_max))
errors.append(error)
if not (bounds.start is None):
if not start <= bounds.start <= end:
error = ("bounds.start: %s is not between start: %s and end: %s" %
(bounds.start, start, end))
errors.append(error)
if not (bounds.end is None):
if not start <= bounds.end <= end:
error = ("bounds.end: %s is not between start: %s and end: %s" %
(bounds.end, start, end))
errors.append(error)
if len(errors) == 0:
return True
else:
error_message = '\n'.join(errors)
raise ValueError(error_message)
def _get_subregion_slice_indices(target_dataset, subregion):
'''Get the indices for slicing Dataset values to generate the subregion.
:param subregion: The Bounds that specify the subset of the Dataset
that should be extracted.
:type subregion: Bounds
:param target_dataset: The Dataset to subset.
:type target_dataset: Dataset
:returns: The indices to slice the Datasets arrays as a Dictionary.
'''
latStart = min(np.nonzero(target_dataset.lats >= subregion.lat_min)[0])
latEnd = max(np.nonzero(target_dataset.lats <= subregion.lat_max)[0])
lonStart = min(np.nonzero(target_dataset.lons >= subregion.lon_min)[0])
lonEnd = max(np.nonzero(target_dataset.lons <= subregion.lon_max)[0])
if not (subregion.start is None):
timeStart = min(np.nonzero(target_dataset.times >= subregion.start)[0])
timeEnd = max(np.nonzero(target_dataset.times <= subregion.end)[0])
else:
timeStart = 0
timeEnd = len(target_dataset.times) -1
return {
"lat_start": latStart,
"lat_end": latEnd,
"lon_start": lonStart,
"lon_end": lonEnd,
"time_start": timeStart,
"time_end": timeEnd
}