def monthly_statistics(cube, operator='mean'): """Compute monthly statistics. Chunks time in monthly periods and computes statistics over them; Parameters ---------- cube: iris.cube.Cube input cube. operator: str, optional Select operator to apply. Available operators: 'mean', 'median', 'std_dev', 'sum', 'min', 'max', 'rms' Returns ------- iris.cube.Cube Monthly statistics cube """ if not cube.coords('month_number'): iris.coord_categorisation.add_month_number(cube, 'time') if not cube.coords('year'): iris.coord_categorisation.add_year(cube, 'time') operator = get_iris_analysis_operation(operator) cube = cube.aggregated_by(['month_number', 'year'], operator) return cube
def test_coord_conversion(self): cube = iris.tests.stock.realistic_4d() # Single string self.assertEquals(len(cube._as_list_of_coords('grid_longitude')), 1) # List of string and unicode self.assertEquals(len(cube._as_list_of_coords(['grid_longitude', u'grid_latitude'], )), 2) # Coord object(s) lat = cube.coords("grid_latitude")[0] lon = cube.coords("grid_longitude")[0] self.assertEquals(len(cube._as_list_of_coords(lat)), 1) self.assertEquals(len(cube._as_list_of_coords([lat, lon])), 2) # Mix of string-like and coord self.assertEquals(len(cube._as_list_of_coords(["grid_latitude", lon])), 2) # Empty list self.assertEquals(len(cube._as_list_of_coords([])), 0) # Invalid coords invalid_choices = [iris.analysis.MEAN, # Caused by mixing up argument order in call to cube.collasped for example None, ['grid_latitude', None], [lat, None], ] for coords in invalid_choices: with self.assertRaises(TypeError): cube._as_list_of_coords(coords)
def _get_plot_defn(cube, mode, ndims=2): """ Return data and plot-axis coords given a cube & a mode of either POINT_MODE or BOUND_MODE. """ if cube.ndim != ndims: msg = 'Cube must be %s-dimensional. Got %s dimensions.' raise ValueError(msg % (ndims, cube.ndim)) # Start by taking the DimCoords from each dimension. coords = [None] * ndims for dim_coord in cube.dim_coords: dim = cube.coord_dims(dim_coord)[0] coords[dim] = dim_coord # When appropriate, restrict to 1D with bounds. if mode == iris.coords.BOUND_MODE: coords = list(map(_valid_bound_coord, coords)) def guess_axis(coord): axis = None if coord is not None: axis = iris.util.guess_coord_axis(coord) return axis # Allow DimCoords in aux_coords to fill in for missing dim_coords. for dim, coord in enumerate(coords): if coord is None: aux_coords = cube.coords(dimensions=dim) aux_coords = [coord for coord in aux_coords if isinstance(coord, iris.coords.DimCoord)] if aux_coords: key_func = lambda coord: coord._as_defn() aux_coords.sort(key=key_func) coords[dim] = aux_coords[0] if mode == iris.coords.POINT_MODE: # Allow multi-dimensional aux_coords to override the dim_coords # along the Z axis. This results in a preference for using the # derived altitude over model_level_number or level_height. # Limit to Z axis to avoid preferring latitude over grid_latitude etc. axes = list(map(guess_axis, coords)) axis = 'Z' if axis in axes: for coord in cube.coords(dim_coords=False): if max(coord.shape) > 1 and \ iris.util.guess_coord_axis(coord) == axis: coords[axes.index(axis)] = coord # Re-order the coordinates to achieve the preferred # horizontal/vertical associations. def sort_key(coord): order = {'X': 2, 'T': 1, 'Y': -1, 'Z': -2} axis = guess_axis(coord) return (order.get(axis, 0), coord and coord.name()) sorted_coords = sorted(coords, key=sort_key) transpose = (sorted_coords != coords) return PlotDefn(sorted_coords, transpose)
def test_coord_conversion(self): cube = iris.tests.stock.realistic_4d() # Single string self.assertEqual(len(cube._as_list_of_coords('grid_longitude')), 1) # List of string and unicode self.assertEqual(len(cube._as_list_of_coords(['grid_longitude', u'grid_latitude'], )), 2) # Coord object(s) lat = cube.coords("grid_latitude")[0] lon = cube.coords("grid_longitude")[0] self.assertEqual(len(cube._as_list_of_coords(lat)), 1) self.assertEqual(len(cube._as_list_of_coords([lat, lon])), 2) # Mix of string-like and coord self.assertEqual(len(cube._as_list_of_coords(['grid_latitude', lon])), 2) # Empty list self.assertEqual(len(cube._as_list_of_coords([])), 0) # Invalid coords invalid_choices = [iris.analysis.MEAN, # Caused by mixing up argument order in call to cube.collasped for example None, ['grid_latitude', None], [lat, None], ] for coords in invalid_choices: with self.assertRaises(TypeError): cube._as_list_of_coords(coords)
def seasonal_statistics(cube, operator='mean'): """ Compute seasonal statistics. Chunks time in 3-month periods and computes statistics over them; Parameters ---------- cube: iris.cube.Cube input cube. operator: str, optional Select operator to apply. Available operators: 'mean', 'median', 'std_dev', 'sum', 'min', 'max', 'rms' Returns ------- iris.cube.Cube Seasonal statistic cube """ if not cube.coords('clim_season'): iris.coord_categorisation.add_season(cube, 'time', name='clim_season') if not cube.coords('season_year'): iris.coord_categorisation.add_season_year(cube, 'time', name='season_year') operator = get_iris_analysis_operation(operator) cube = cube.aggregated_by(['clim_season', 'season_year'], operator) # CMOR Units are days so we are safe to operate on days # Ranging on [90, 92] days makes this calendar-independent def spans_three_months(time): """ Check for three months. Parameters ---------- time: iris.DimCoord cube time coordinate Returns ------- bool truth statement if time bounds are 90+2 days. """ return 90 <= (time.bound[1] - time.bound[0]).days <= 92 three_months_bound = iris.Constraint(time=spans_three_months) return cube.extract(three_months_bound)
def extract_season(cube, season): """Slice cube to get only the data belonging to a specific season. Parameters ---------- cube: iris.cube.Cube Original data season: str Season to extract. Available: DJF, MAM, JJA, SON and all sequentially correct combinations: e.g. JJAS Returns ------- iris.cube.Cube data cube for specified season. Raises ------ ValueError if requested season is not present in the cube """ season = season.upper() allmonths = 'JFMAMJJASOND' * 2 if season not in allmonths: raise ValueError(f"Unable to extract Season {season} " f"combination of months not possible.") sstart = allmonths.index(season) res_season = allmonths[sstart + len(season):sstart + 12] seasons = [season, res_season] coords_to_remove = [] if not cube.coords('clim_season'): iris.coord_categorisation.add_season(cube, 'time', name='clim_season', seasons=seasons) coords_to_remove.append('clim_season') if not cube.coords('season_year'): iris.coord_categorisation.add_season_year(cube, 'time', name='season_year', seasons=seasons) coords_to_remove.append('season_year') result = cube.extract(iris.Constraint(clim_season=season)) for coord in coords_to_remove: cube.remove_coord(coord) if result is None: raise ValueError(f'Season {season!r} not present in cube {cube}') return result
def intersection_of_cubes(cube, other_cube): """ Return the two Cubes of intersection given two Cubes. .. note:: The intersection of cubes function will ignore all single valued coordinates in checking the intersection. Args: * cube: An instance of :class:`iris.cube.Cube`. * other_cube: An instance of :class:`iris.cube.Cube`. Returns: A pair of :class:`iris.cube.Cube` instances in a tuple corresponding to the original cubes restricted to their intersection. """ # Take references of the original cubes (which will be copied when # slicing later). new_cube_self = cube new_cube_other = other_cube # This routine has not been written to cope with multi-dimensional # coordinates. for coord in cube.coords() + other_cube.coords(): if coord.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(coord) coord_comp = iris.analysis._dimensional_metadata_comparison( cube, other_cube) if coord_comp["ungroupable_and_dimensioned"]: raise ValueError("Cubes do not share all coordinates in common, " "cannot intersect.") # cubes must have matching coordinates for coord in cube.coords(): other_coord = other_cube.coord(coord) # Only intersect coordinates which are different, single values # coordinates may differ. if coord.shape[0] > 1 and coord != other_coord: intersected_coord = coord.intersect(other_coord) new_cube_self = new_cube_self.subset(intersected_coord) new_cube_other = new_cube_other.subset(intersected_coord) return new_cube_self, new_cube_other
def _get_period_coord(cube, period): """Get periods.""" if period in ['daily', 'day']: if not cube.coords('day_of_year'): iris.coord_categorisation.add_day_of_year(cube, 'time') return cube.coord('day_of_year') if period in ['monthly', 'month', 'mon']: if not cube.coords('month_number'): iris.coord_categorisation.add_month_number(cube, 'time') return cube.coord('month_number') if period in ['seasonal', 'season']: if not cube.coords('season_number'): iris.coord_categorisation.add_season_number(cube, 'time') return cube.coord('season_number') raise ValueError(f"Period '{period}' not supported")
def intersection_of_cubes(cube, other_cube): """ Return the two Cubes of intersection given two Cubes. .. note:: The intersection of cubes function will ignore all single valued coordinates in checking the intersection. Args: * cube: An instance of :class:`iris.cube.Cube`. * other_cube: An instance of :class:`iris.cube.Cube`. Returns: A pair of :class:`iris.cube.Cube` instances in a tuple corresponding to the original cubes restricted to their intersection. """ # Take references of the original cubes (which will be copied when # slicing later). new_cube_self = cube new_cube_other = other_cube # This routine has not been written to cope with multi-dimensional # coordinates. for coord in cube.coords() + other_cube.coords(): if coord.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(coord) coord_comp = iris.analysis.coord_comparison(cube, other_cube) if coord_comp['ungroupable_and_dimensioned']: raise ValueError('Cubes do not share all coordinates in common, ' 'cannot intersect.') # cubes must have matching coordinates for coord in cube.coords(): other_coord = other_cube.coord(coord) # Only intersect coordinates which are different, single values # coordinates may differ. if coord.shape[0] > 1 and coord != other_coord: intersected_coord = coord.intersect(other_coord) new_cube_self = new_cube_self.subset(intersected_coord) new_cube_other = new_cube_other.subset(intersected_coord) return new_cube_self, new_cube_other
def _get_dim_names(self, cube): """ Determine suitable CF-netCDF data dimension names. Args: * cube (:class:`iris.cube.Cube`) or cubelist (:class:`iris.cube.CubeList`): A :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or list of cubes to be saved to a netCDF file. Returns: List of dimension names with length equal the number of dimensions in the cube. """ dimension_names = [] for dim in xrange(cube.ndim): coords = cube.coords(dimensions=dim, dim_coords=True) if coords: coord = coords[0] dim_name = self._get_coord_variable_name(cube, coord) # Add only dimensions that have not already been added. if coord not in self._dim_coords: # Determine unique dimension name while (dim_name in self._existing_dim or dim_name in self._name_coord_map.names): dim_name = self._increment_name(dim_name) # Update names added, current cube dim names used and # unique coordinates added. self._existing_dim[dim_name] = coord.shape[0] dimension_names.append(dim_name) self._dim_coords.append(coord) else: # Return the dim_name associated with the existing # coordinate. dim_name = self._name_coord_map.name(coord) dimension_names.append(dim_name) else: # No CF-netCDF coordinates describe this data dimension. dim_name = 'dim%d' % dim if dim_name in self._existing_dim: # Increment name if conflicted with one already existing. if self._existing_dim[dim_name] != cube.shape[dim]: while (dim_name in self._existing_dim and self._existing_dim[dim_name] != cube.shape[dim] or dim_name in self._name_coord_map.names): dim_name = self._increment_name(dim_name) # Update dictionary with new entry self._existing_dim[dim_name] = cube.shape[dim] else: # Update dictionary with new entry self._existing_dim[dim_name] = cube.shape[dim] dimension_names.append(dim_name) return dimension_names
def annual_statistics(cube, operator='mean'): """Compute annual statistics. Note that this function does not weight the annual mean if uneven time periods are present. Ie, all data inside the year are treated equally. Parameters ---------- cube: iris.cube.Cube input cube. operator: str, optional Select operator to apply. Available operators: 'mean', 'median', 'std_dev', 'sum', 'min', 'max', 'rms' Returns ------- iris.cube.Cube Annual statistics cube """ # TODO: Add weighting in time dimension. See iris issue 3290 # https://github.com/SciTools/iris/issues/3290 operator = get_iris_analysis_operation(operator) if not cube.coords('year'): iris.coord_categorisation.add_year(cube, 'time') return cube.aggregated_by('year', operator)
def cube_delta(cube, coord): """ Given a cube calculate the difference between each value in the given coord's direction. Args: * coord either a Coord instance or the unique name of a coordinate in the cube. If a Coord instance is provided, it does not necessarily have to exist in the cube. Example usage:: change_in_temperature_wrt_pressure = cube_delta(temperature_cube, 'pressure') .. note:: Missing data support not yet implemented. """ # handle the case where a user passes a coordinate name if isinstance(coord, basestring): coord = cube.coord(coord) if coord.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(coord) # Try and get a coord dim delta_dims = cube.coord_dims(coord) if (coord.shape[0] == 1 and not getattr(coord, 'circular', False)) or not delta_dims: raise ValueError( 'Cannot calculate delta over "%s" as it has length of 1.' % coord.name()) delta_dim = delta_dims[0] # Calculate the actual delta, taking into account whether the given coordinate is circular delta_cube_data = delta(cube.data, delta_dim, circular=getattr(coord, 'circular', False)) # If the coord/dim is circular there is no change in cube shape if getattr(coord, 'circular', False): delta_cube = cube.copy(data=delta_cube_data) else: # Subset the cube to the appropriate new shape by knocking off the last row of the delta dimension subset_slice = [slice(None, None)] * cube.ndim subset_slice[delta_dim] = slice(None, -1) delta_cube = cube[tuple(subset_slice)] delta_cube.data = delta_cube_data # Replace the delta_dim coords with midpoints (no shape change if circular). for cube_coord in cube.coords(dimensions=delta_dim): delta_cube.replace_coord( _construct_midpoint_coord(cube_coord, circular=getattr(coord, 'circular', False))) delta_cube.rename('change_in_%s_wrt_%s' % (delta_cube.name(), coord.name())) return delta_cube
def _create_cf_cell_methods(cube, dimension_names): """Create CF-netCDF string representation of a cube cell methods.""" cell_methods = [] # Identify the collection of coordinates that represent CF-netCDF coordinate variables. cf_coordinates = cube.dim_coords for cm in cube.cell_methods: names = '' for name in cm.coord_names: coord = cube.coords(name) if coord: coord = coord[0] if coord in cf_coordinates: name = dimension_names[cube.coord_dims(coord)[0]] names += '%s: ' % name interval = ' '.join(['interval: %s' % interval for interval in cm.intervals or []]) comment = ' '.join(['comment: %s' % comment for comment in cm.comments or []]) extra = ' '.join([interval, comment]).strip() if extra: extra = ' (%s)' % extra cell_methods.append(names + cm.method + extra) return ' '.join(cell_methods)
def extract_month(cube, month): """Slice cube to get only the data belonging to a specific month. Parameters ---------- cube: iris.cube.Cube Original data month: int Month to extract as a number from 1 to 12 Returns ------- iris.cube.Cube data cube for specified month. Raises ------ ValueError if requested month is not present in the cube """ if month not in range(1, 13): raise ValueError('Please provide a month number between 1 and 12.') if not cube.coords('month_number'): iris.coord_categorisation.add_month_number(cube, 'time', name='month_number') result = cube.extract(iris.Constraint(month_number=month)) if result is None: raise ValueError(f'Month {month!r} not present in cube {cube}') return result
def _get_horizontal_coord(cube, axis): """ Gets the horizontal coordinate on the supplied cube along the specified axis. Args: * cube: An instance of :class:`iris.cube.Cube`. * axis: Locate coordinates on `cube` along this axis. Returns: The horizontal coordinate on the specified axis of the supplied cube. """ coords = cube.coords(axis=axis, dim_coords=False) if len(coords) != 1: raise ValueError( "Cube {!r} must contain a single 1D {} " "coordinate.".format(cube.name()), axis, ) return coords[0]
def vector_coord(cube, coord_name): """Try to find a one-dimensional, multi-valued coord with the given name.""" found_coord = None for coord in cube.coords(coord_name): if len(coord.shape) == 1 and coord.shape[0] > 1: found_coord = coord break return found_coord
def scalar_coord(cube, coord_name): """Try to find a single-valued coord with the given name.""" found_coord = None for coord in cube.coords(name=coord_name): if coord.shape == (1,): found_coord = coord break return found_coord
def vector_coord(cube, coord_name): """Try to find a one-dimensional, multi-valued coord with the given name.""" found_coord = None for coord in cube.coords(name=coord_name): if len(coord.shape) == 1 and coord.shape[0] > 1: found_coord = coord break return found_coord
def scalar_coord(cube, coord_name): """Try to find a single-valued coord with the given name.""" found_coord = None for coord in cube.coords(coord_name): if coord.shape == (1,): found_coord = coord break return found_coord
def cube_delta(cube, coord): """ Given a cube calculate the difference between each value in the given coord's direction. Args: * coord either a Coord instance or the unique name of a coordinate in the cube. If a Coord instance is provided, it does not necessarily have to exist in the cube. Example usage:: change_in_temperature_wrt_pressure = \ cube_delta(temperature_cube, 'pressure') .. note:: Missing data support not yet implemented. """ # handle the case where a user passes a coordinate name if isinstance(coord, six.string_types): coord = cube.coord(coord) if coord.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(coord) # Try and get a coord dim delta_dims = cube.coord_dims(coord.name()) if (coord.shape[0] == 1 and not getattr(coord, "circular", False)) or not delta_dims: raise ValueError("Cannot calculate delta over {!r} as it has " "length of 1.".format(coord.name())) delta_dim = delta_dims[0] # Calculate the actual delta, taking into account whether the given # coordinate is circular. delta_cube_data = delta(cube.data, delta_dim, circular=getattr(coord, "circular", False)) # If the coord/dim is circular there is no change in cube shape if getattr(coord, "circular", False): delta_cube = cube.copy(data=delta_cube_data) else: # Subset the cube to the appropriate new shape by knocking off # the last row of the delta dimension. subset_slice = [slice(None, None)] * cube.ndim subset_slice[delta_dim] = slice(None, -1) delta_cube = cube[tuple(subset_slice)] delta_cube.data = delta_cube_data # Replace the delta_dim coords with midpoints # (no shape change if circular). for cube_coord in cube.coords(dimensions=delta_dim): delta_cube.replace_coord(_construct_midpoint_coord(cube_coord, circular=getattr(coord, "circular", False))) delta_cube.rename("change_in_{}_wrt_{}".format(delta_cube.name(), coord.name())) return delta_cube
def scalar_cell_method(cube, method, coord_name): """Try to find the given type of cell method over a single coord with the given name.""" found_cell_method = None for cell_method in cube.cell_methods: if cell_method.method == method and len(cell_method.coord_names) == 1: name = cell_method.coord_names[0] coords = cube.coords(name) if len(coords) == 1: found_cell_method = cell_method return found_cell_method
def scalar_cell_method(cube, method, coord_name): """Try to find the given type of cell method over a single coord with the given name.""" found_cell_method = None for cell_method in cube.cell_methods: if cell_method.method == method and len(cell_method.coord_names) == 1: name = cell_method.coord_names[0] coords = cube.coords(name=name) if len(coords) == 1: found_cell_method = cell_method return found_cell_method
def hourly_statistics(cube, hours, operator='mean'): """Compute hourly statistics. Chunks time in x hours periods and computes statistics over them. Parameters ---------- cube: iris.cube.Cube input cube. hours: int Number of hours per period. Must be a divisor of 24 (1, 2, 3, 4, 6, 8, 12) operator: str, optional Select operator to apply. Available operators: 'mean', 'median', 'std_dev', 'sum', 'min', 'max' Returns ------- iris.cube.Cube Hourly statistics cube """ if not cube.coords('hour_group'): iris.coord_categorisation.add_categorised_coord( cube, 'hour_group', 'time', lambda coord, value: coord.units.num2date(value).hour // hours, units='1') if not cube.coords('day_of_year'): iris.coord_categorisation.add_day_of_year(cube, 'time') if not cube.coords('year'): iris.coord_categorisation.add_year(cube, 'time') operator = get_iris_analysis_operation(operator) cube = cube.aggregated_by(['hour_group', 'day_of_year', 'year'], operator) cube.remove_coord('hour_group') cube.remove_coord('day_of_year') cube.remove_coord('year') return cube
def extract_season(cube, season): """ Slice cube to get only the data belonging to a specific season. Parameters ---------- cube: iris.cube.Cube Original data season: str Season to extract. Available: DJF, MAM, JJA, SON Returns ------- iris.cube.Cube data cube for specified season. """ if not cube.coords('clim_season'): iris.coord_categorisation.add_season(cube, 'time', name='clim_season') if not cube.coords('season_year'): iris.coord_categorisation.add_season_year(cube, 'time', name='season_year') return cube.extract(iris.Constraint(clim_season=season.lower()))
def test_axis(self): cube = self.t.copy() cube.coord("dim1").rename("latitude") cube.coord("dim2").rename("longitude") coords = cube.coords(axis='y') self.assertEqual([coord.name() for coord in coords], ['latitude']) coords = cube.coords(axis='x') self.assertEqual([coord.name() for coord in coords], ['longitude']) # Renaming shoudn't be enough cube.coord("an_other").rename("time") coords = cube.coords(axis='t') self.assertEqual([coord.name() for coord in coords], []) # Change units to "hours since ..." as it's the presence of a # time unit that identifies a time axis. cube.coord("time").units = 'hours since 1970-01-01 00:00:00' coords = cube.coords(axis='t') self.assertEqual([coord.name() for coord in coords], ['time']) coords = cube.coords(axis='z') self.assertEqual(coords, [])
def _get_xy_dim_coords(cube): """ Return the x and y dimension coordinates from a cube. This function raises a ValueError if the cube does not contain one and only one set of x and y dimension coordinates. It also raises a ValueError if the identified x and y coordinates do not have coordinate systems that are equal. Args: * cube: An instance of :class:`iris.cube.Cube`. Returns: A tuple containing the cube's x and y dimension coordinates. """ x_coords = cube.coords(axis='x', dim_coords=True) if len(x_coords) != 1: raise ValueError('Cube {!r} must contain a single 1D x ' 'coordinate.'.format(cube.name())) x_coord = x_coords[0] y_coords = cube.coords(axis='y', dim_coords=True) if len(y_coords) != 1: raise ValueError('Cube {!r} must contain a single 1D y ' 'coordinate.'.format(cube.name())) y_coord = y_coords[0] if x_coord.coord_system != y_coord.coord_system: raise ValueError("The cube's x ({!r}) and y ({!r}) " "coordinates must have the same coordinate " "system.".format(x_coord.name(), y_coord.name())) return x_coord, y_coord
def _create_cf_cell_methods(self, cube, dimension_names): """ Create CF-netCDF string representation of a cube cell methods. Args: * cube (:class:`iris.cube.Cube`) or cubelist (:class:`iris.cube.CubeList`): A :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or list of cubes to be saved to a netCDF file. * dimension_names (list): Names associated with the dimensions of the cube. Returns: CF-netCDF string representation of a cube cell methods. """ cell_methods = [] # Identify the collection of coordinates that represent CF-netCDF # coordinate variables. cf_coordinates = cube.dim_coords for cm in cube.cell_methods: names = '' for name in cm.coord_names: coord = cube.coords(name) if coord: coord = coord[0] if coord in cf_coordinates: name = dimension_names[cube.coord_dims(coord)[0]] names += '%s: ' % name interval = ' '.join(['interval: %s' % interval for interval in cm.intervals or []]) comment = ' '.join(['comment: %s' % comment for comment in cm.comments or []]) extra = ' '.join([interval, comment]).strip() if extra: extra = ' (%s)' % extra cell_methods.append(names + cm.method + extra) return ' '.join(cell_methods)
def _get_horizontal_coord(cube, axis): """ Gets the horizontal coordinate on the supplied cube along the specified axis. Args: * cube: An instance of :class:`iris.cube.Cube`. * axis: Locate coordinates on `cube` along this axis. Returns: The horizontal coordinate on the specified axis of the supplied cube. """ coords = cube.coords(axis=axis, dim_coords=False) if len(coords) != 1: raise ValueError('Cube {!r} must contain a single 1D {} ' 'coordinate.'.format(cube.name()), axis) return coords[0]
def decadal_statistics(cube, operator='mean'): """ Compute decadal statistics. Note that this function does not weight the decadal mean if uneven time periods are present. Ie, all data inside the decade are treated equally. Parameters ---------- cube: iris.cube.Cube input cube. operator: str, optional Select operator to apply. Available operators: 'mean', 'median', 'std_dev', 'sum', 'min', 'max', 'rms' Returns ------- iris.cube.Cube Decadal statistics cube """ # TODO: Add weighting in time dimension. See iris issue 3290 # https://github.com/SciTools/iris/issues/3290 operator = get_iris_analysis_operation(operator) if not cube.coords('decade'): def get_decade(coord, value): """Categorize time coordinate into decades.""" date = coord.units.num2date(value) return date.year - date.year % 10 iris.coord_categorisation.add_categorised_coord( cube, 'decade', 'time', get_decade) return cube.aggregated_by('decade', operator)
def load_cube(paths, variable_name=None): """Read datasets from paths into Iris cubes. Combines cubes if there are more than one dataset in the same file. Returns a list of lists. Inner lists corresponds to the areas (in order), outer lists corresponds to the paths """ if isinstance(paths, (str, pathlib.Path)): if variable_name: cubes = iris.load_cubes(str(paths), constraints=variable_name) else: cubes = iris.load_cubes(str(paths)) else: if variable_name: cubes = iris.load([str(path) for path in paths], constraints=variable_name) else: cubes = iris.load([str(path) for path in paths]) # Select only the cubes with 3/4D data (time, lat, long, height) cubes = iris.cube.CubeList( [cube for cube in cubes if len(cube.coords()) >= 3]) if len(cubes) == 0: return None equalise_attributes(cubes) unify_time_units(cubes) try: cube = cubes.concatenate_cube() except iris.exceptions.ConcatenateError as exc: logger.warning("%s for %s", exc, str(paths)) logger.warning("Using only the first cube of [%s]", cubes) cube = cubes[ 0] # iris.load always returns a cubelist, so just take the first element return cube
def _broadcast_cube_coord_data(cube, other, operation_name, dim=None): # What dimension are we processing? data_dimension = None if dim is not None: # Ensure the given dim matches the coord if other in cube.coords() and cube.coord_dims(other) != [dim]: raise ValueError("dim provided does not match dim found for coord") data_dimension = dim else: # Try and get a coord dim if other.shape != (1,): try: coord_dims = cube.coord_dims(other) data_dimension = coord_dims[0] if coord_dims else None except iris.exceptions.CoordinateNotFoundError: raise ValueError("Could not determine dimension for %s. " "Use %s(cube, coord, dim=dim)" % (operation_name, operation_name)) if other.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(other) if other.has_bounds(): warnings.warn('Using {!r} with a bounded coordinate is not well ' 'defined; ignoring bounds.'.format(operation_name)) points = other.points # If the `data_dimension` is defined then shape the provided points for # proper array broadcasting if data_dimension is not None: points_shape = [1] * cube.ndim points_shape[data_dimension] = -1 points = points.reshape(points_shape) return points
def linear(cube, sample_points, extrapolation_mode='linear'): """ Return a cube of the linearly interpolated points given the desired sample points. Given a list of tuple pairs mapping coordinates to their desired values, return a cube with linearly interpolated values. If more than one coordinate is specified, the linear interpolation will be carried out in sequence, thus providing n-linear interpolation (bi-linear, tri-linear, etc.). .. note:: By definition, linear interpolation requires all coordinates to be 1-dimensional. Args: * cube The cube to be interpolated. * sample_points List of one or more tuple pairs mapping coordinate to desired points to interpolate. Points may be a scalar or a numpy array of values. Kwargs: * extrapolation_mode - string - one of 'linear', 'nan' or 'error' * If 'linear' the point will be calculated by extending the gradient of closest two points. * If 'nan' the extrapolation point will be put as a NAN. * If 'error' a value error will be raised notifying of the attempted extrapolation. .. note:: The datatype of the resultant cube's data and coordinates will updated to the data type of the incoming cube. """ if not isinstance(cube, iris.cube.Cube): raise ValueError('Expecting a cube instance, got %s' % type(cube)) if isinstance(sample_points, dict): warnings.warn('Providing a dictionary to specify points is deprecated. Please provide a list of (coordinate, values) pairs.') sample_points = sample_points.items() # catch the case where a user passes a single (coord/name, value) pair rather than a list of pairs if sample_points and not (isinstance(sample_points[0], collections.Container) and not isinstance(sample_points[0], basestring)): raise TypeError('Expecting the sample points to be a list of tuple pairs representing (coord, points), got a list of %s.' % type(sample_points[0])) points = [] for (coord, values) in sample_points: if isinstance(coord, basestring): coord = cube.coord(coord) else: coord = cube.coord(coord=coord) points.append((coord, values)) sample_points = points if len(sample_points) == 0: raise ValueError('Expecting a non-empty list of coord value pairs, got %r.' % sample_points) if cube.data.dtype.kind == 'i': raise ValueError("Cannot linearly interpolate a cube which has integer type data. Consider casting the " "cube's data to floating points in order to continue.") bounds_error = (extrapolation_mode == 'error') # Handle an over-specified points_dict or a specification which does not describe a data dimension data_dimensions_requested = [] for coord, values in sample_points: if coord.ndim > 1: raise ValueError('Cannot linearly interpolate over %s as it is multi-dimensional.' % coord.name()) data_dim = cube.coord_dims(coord) if not data_dim: raise ValueError('Requested a point over a coordinate which does not describe a dimension (%s).' % coord.name()) else: data_dim = data_dim[0] if data_dim in data_dimensions_requested: raise ValueError('Requested a point which over specifies a dimension: (%s). ' % coord.name()) data_dimensions_requested.append(data_dim) # Iterate over all of the requested keys in the given points_dict calling this routine repeatedly. if len(sample_points) > 1: result = cube for coord, cells in sample_points: result = linear(result, [(coord, cells)], extrapolation_mode=extrapolation_mode) return result else: # take the single coordinate name and associated cells from the dictionary coord, requested_points = sample_points[0] requested_points = numpy.array(requested_points, dtype=cube.data.dtype) # build up indices so that we can quickly subset the original cube to be of the desired size new_cube_slices = [slice(None, None)] * cube.data.ndim # get this coordinate's index position (which we have already tested is not None) data_dim = cube.coord_dims(coord)[0] if requested_points.ndim > 0: # we want the interested dimension to be of len(requested_points) new_cube_slices[data_dim] = tuple([0] * len(requested_points)) else: new_cube_slices[data_dim] = 0 # Subset the original cube to get an appropriately sized cube. # NB. This operation will convert any DimCoords on the dimension # being sliced into AuxCoords. This removes the value of their # `circular` flags, and there's nowhere left to put it. new_cube = cube[tuple(new_cube_slices)] # now that we have got a cube at the desired location, get the data. if getattr(coord, 'circular', False): coord_slice_in_cube = [slice(None, None)] * cube.data.ndim coord_slice_in_cube[data_dim] = slice(0, 1) points = numpy.append(coord.points, coord.points[0] + numpy.array(coord.units.modulus or 0, dtype=coord.dtype)) data = numpy.append(cube.data, cube.data[tuple(coord_slice_in_cube)], axis=data_dim) else: points = coord.points data = cube.data if len(points) <= 1: raise ValueError('Cannot linearly interpolate a coordinate (%s) with one point.' % coord.name()) monotonic, direction = iris.util.monotonic(points, return_direction=True) if not monotonic: raise ValueError('Unable to linearly interpolate this cube as the coordinate "%s" is not monotonic' % coord.name()) # if the coord is monotonic decreasing, then we need to flip it as SciPy's interp1d is expecting monotonic increasing. if direction == -1: points = iris.util.reverse(points, axes=0) data = iris.util.reverse(data, axes=data_dim) # limit the datatype of the outcoming points to be the datatype of the cube's data # (otherwise, interp1d will up-cast an incoming pair. i.e. (int32, float32) -> float64) if points.dtype.num < data.dtype.num: points = points.astype(data.dtype) # Now that we have subsetted the original cube, we must update all coordinates on the data dimension. for shared_dim_coord in cube.coords(contains_dimension=data_dim): if shared_dim_coord.ndim != 1: raise iris.exceptions.NotYetImplementedError('Linear interpolation of multi-dimensional coordinates.') new_coord = new_cube.coord(coord=shared_dim_coord) new_coord.bounds = None if shared_dim_coord._as_defn() != coord._as_defn(): shared_coord_points = shared_dim_coord.points if getattr(coord, 'circular', False): mod_val = numpy.array(shared_dim_coord.units.modulus or 0, dtype=shared_coord_points.dtype) shared_coord_points = numpy.append(shared_coord_points, shared_coord_points[0] + mod_val) # If the coordinate which we were interpolating over was monotonic decreasing, # we need to flip this coordinate's values if direction == -1: shared_coord_points = iris.util.reverse(shared_coord_points, axes=0) coord_points = points if shared_coord_points.dtype.num < data.dtype.num: shared_coord_points = shared_coord_points.astype(data.dtype) interpolator = interpolate.interp1d(coord_points, shared_coord_points, kind='linear', bounds_error=bounds_error) if extrapolation_mode == 'linear': interpolator = iris.util.Linear1dExtrapolator(interpolator) new_coord.points = interpolator(requested_points) else: new_coord.points = requested_points # now we can go ahead and interpolate the data interpolator = interpolate.interp1d(points, data, axis=data_dim, kind='linear', copy=False, bounds_error=bounds_error) if extrapolation_mode == 'linear': interpolator = iris.util.Linear1dExtrapolator(interpolator) new_cube.data = interpolator(requested_points) return new_cube
def _map_common(draw_method_name, arg_func, mode, cube, data, *args, **kwargs): """ Draw the given cube on a map using its points or bounds. "Mode" parameter will switch functionality between POINT or BOUND plotting. """ # get the 2d lons and 2d lats from the CS if mode == iris.coords.POINT_MODE: lats, lons = iris.analysis.cartography.get_lat_lon_grids(cube) else: lats, lons = iris.analysis.cartography.get_lat_lon_contiguous_bounded_grids(cube) # take a copy of the data so that we can make modifications to it data = data.copy() # if we are global, then append the first column of data the array to the last (and add 360 degrees) # NOTE: if it is found that this block of code is useful in anywhere other than this plotting routine, it # may be better placed in the CS. lon_coord = filter(lambda coord: coord.standard_name in ["longitude", "grid_longitude"], cube.coords())[0] if lon_coord.circular: lats = numpy.append(lats, lats[:, 0:1], axis=1) lons = numpy.append(lons, lons[:, 0:1] + 360, axis=1) data = numpy.ma.concatenate([data, data[:, 0:1]], axis=1) # Do we need to flip the longitude to avoid basemap's "non positive monotonic" warning? # Assume we have a non-scalar longitude coord describing a data dimension. mono, direction = iris.util.monotonic(lons[0, :], return_direction=True) if mono and direction == -1: data = data[:, ::-1] lons = lons[:, ::-1] lats = lats[:, ::-1] # Attempt to mimic the pyplot stateful interface with basemap. # If the current Basemap instance hasn't been registered on the current axes then # we assume we've moved to a new axes and create a new map. bm = _CURRENT_MAP if bm is None or hash(plt.gca()) not in bm._initialized_axes: # Provide lat & lon ranges as we have already calculated our lats and lons. bm = map_setup(cube=cube, lon_range=(numpy.min(lons), numpy.max(lons)), lat_range=(numpy.min(lats), numpy.max(lats)), ) # Convert the lons and lats into the plot coordinates px, py = bm(lons, lats) if mode == iris.coords.POINT_MODE: # TODO #480 Include mdi in this index when it is available invalid_points = numpy.where((px == 1e+30) | (py == 1e+30) | (numpy.isnan(data))) data[invalid_points] = numpy.nan else: # TODO #480 Include mdi in this index invalid_points = numpy.where( (px == 1e+30) | (py == 1e+30) ) px[invalid_points] = numpy.nan py[invalid_points] = numpy.nan # Draw the contour lines/filled contours draw_method = getattr(bm, draw_method_name) if arg_func is not None: new_args, kwargs = arg_func(px, py, data, *args, **kwargs) else: new_args = (px, py, data) + args drawn_object = draw_method(*new_args, **kwargs) # if the range of the data is outside the range of the map, then bring the data back 360 degrees and re-plot if numpy.max(lons) > bm.urcrnrlon: px, py = bm(lons-360, lats) if hasattr(drawn_object, 'levels'): if arg_func is not None: new_args, kwargs = arg_func(px, py, data, drawn_object.levels, *args, **kwargs) else: new_args = (px, py, data, drawn_object.levels) + args drawn_object = draw_method(*new_args, **kwargs) return drawn_object
def _get_xy_coords(cube): """ Return the x and y coordinates from a cube. This function will preferentially return a pair of dimension coordinates (if there are more than one potential x or y dimension coordinates a ValueError will be raised). If the cube does not have a pair of x and y dimension coordinates it will return 1D auxiliary coordinates (including scalars). If there is not one and only one set of x and y auxiliary coordinates a ValueError will be raised. Having identified the x and y coordinates, the function checks that they have equal coordinate systems and that they do not occupy the same dimension on the cube. Args: * cube: An instance of :class:`iris.cube.Cube`. Returns: A tuple containing the cube's x and y coordinates. """ # Look for a suitable dimension coords first. x_coords = cube.coords(axis='x', dim_coords=True) if not x_coords: # If there is no x coord in dim_coords look for scalars or # monotonic coords in aux_coords. x_coords = [coord for coord in cube.coords(axis='x', dim_coords=False) if coord.ndim == 1 and coord.is_monotonic()] if len(x_coords) != 1: raise ValueError('Cube {!r} must contain a single 1D x ' 'coordinate.'.format(cube.name())) x_coord = x_coords[0] # Look for a suitable dimension coords first. y_coords = cube.coords(axis='y', dim_coords=True) if not y_coords: # If there is no y coord in dim_coords look for scalars or # monotonic coords in aux_coords. y_coords = [coord for coord in cube.coords(axis='y', dim_coords=False) if coord.ndim == 1 and coord.is_monotonic()] if len(y_coords) != 1: raise ValueError('Cube {!r} must contain a single 1D y ' 'coordinate.'.format(cube.name())) y_coord = y_coords[0] if x_coord.coord_system != y_coord.coord_system: raise ValueError("The cube's x ({!r}) and y ({!r}) " "coordinates must have the same coordinate " "system.".format(x_coord.name(), y_coord.name())) # The x and y coordinates must describe different dimensions # or be scalar coords. x_dims = cube.coord_dims(x_coord) x_dim = None if x_dims: x_dim = x_dims[0] y_dims = cube.coord_dims(y_coord) y_dim = None if y_dims: y_dim = y_dims[0] if x_dim is not None and y_dim == x_dim: raise ValueError("The cube's x and y coords must not describe the " "same data dimension.") return x_coord, y_coord
def _multiply_divide_common(operation_function, operation_symbol, operation_noun, cube, other, dim=None, in_place=False): """ Function which shares common code between multiplication and division of cubes. operation_function - function which does the operation (e.g. numpy.divide) operation_symbol - the textual symbol of the operation (e.g. '/') operation_noun - the noun of the operation (e.g. 'division') operation_past_tense - the past tense of the operation (e.g. 'divided') .. seealso:: For information on the dim keyword argument see :func:`multiply`. """ if not isinstance(cube, iris.cube.Cube): raise TypeError( 'The "cube" argument must be an instance of iris.Cube.') if isinstance(other, (int, float)): other = np.array(other) other_unit = None if isinstance(other, np.ndarray): _assert_compatible(cube, other) if in_place: new_cube = cube new_cube.data = operation_function(cube.data, other) else: new_cube = cube.copy(data=operation_function(cube.data, other)) other_unit = '1' elif isinstance(other, iris.coords.Coord): # Deal with cube multiplication/division by coordinate # What dimension are we processing? data_dimension = None if dim is not None: # Ensure the given dim matches the coord if other in cube.coords() and cube.coord_dims(other) != [dim]: raise ValueError( "dim provided does not match dim found for coord") data_dimension = dim else: # Try and get a coord dim if other.shape != (1, ): try: coord_dims = cube.coord_dims(other) data_dimension = coord_dims[0] if coord_dims else None except iris.exceptions.CoordinateNotFoundError: raise ValueError( "Could not determine dimension for mul/div. Use mul(coord, dim=dim)" ) if other.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(other) if other.has_bounds(): warnings.warn( '%s by a bounded coordinate not well defined, ignoring bounds.' % operation_noun) points = other.points # If the axis is defined then shape the provided points so that we can do the # division (this is needed as there is no "axis" keyword to numpy's divide/multiply) if data_dimension is not None: points_shape = [1] * cube.ndim points_shape[data_dimension] = -1 points = points.reshape(points_shape) if in_place: new_cube = cube new_cube.data = operation_function(cube.data, points) else: new_cube = cube.copy(data=operation_function(cube.data, points)) other_unit = other.units elif isinstance(other, iris.cube.Cube): # Deal with cube multiplication/division by cube if in_place: new_cube = cube new_cube.data = operation_function(cube.data, other.data) else: new_cube = cube.copy( data=operation_function(cube.data, other.data)) other_unit = other.units else: return NotImplemented # Update the units if operation_function == np.multiply: new_cube.units = cube.units * other_unit elif operation_function == np.divide: new_cube.units = cube.units / other_unit iris.analysis.clear_phenomenon_identity(new_cube) return new_cube
def _add_subtract_common(operation_function, operation_symbol, operation_noun, operation_past_tense, cube, other, dim=None, ignore=True, update_history=True, in_place=False): """ Function which shares common code between addition and subtraction of cubes. operation_function - function which does the operation (e.g. numpy.subtract) operation_symbol - the textual symbol of the operation (e.g. '-') operation_noun - the noun of the operation (e.g. 'subtraction') operation_past_tense - the past tense of the operation (e.g. 'subtracted') """ if not isinstance(cube, iris.cube.Cube): raise TypeError('The "cube" argument must be an instance of iris.Cube.') if isinstance(other, (int, float)): # Promote scalar to a coordinate and associate unit type with cube unit type other = np.array(other) # Check that the units of the cube and the other item are the same, or if the other does not have a unit, skip this test if cube.units != getattr(other, 'units', cube.units) : raise iris.exceptions.NotYetImplementedError('Differing units (%s & %s) %s not implemented' % \ (cube.units, other.units, operation_noun)) history = None if isinstance(other, np.ndarray): _assert_compatible(cube, other) if in_place: new_cube = cube operation_function(new_cube.data, other, new_cube.data) else: new_cube = cube.copy(data=operation_function(cube.data, other)) if update_history: if other.ndim == 0: history = '%s %s %s' % (cube.name(), operation_symbol, other) else: history = '%s %s array' % (cube.name(), operation_symbol) elif isinstance(other, iris.coords.Coord): # Deal with cube addition/subtraction by coordinate # What dimension are we processing? data_dimension = None if dim is not None: # Ensure the given dim matches the coord if other in cube.coords() and cube.coord_dims(other) != [dim]: raise ValueError("dim provided does not match dim found for coord") data_dimension = dim else: # Try and get a coord dim if other.shape != (1,): try: coord_dims = cube.coord_dims(other) data_dimension = coord_dims[0] if coord_dims else None except iris.exceptions.CoordinateNotFoundError: raise ValueError("Could not determine dimension for add/sub. Use add(coord, dim=dim)") if other.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(other) if other.has_bounds(): warnings.warn('%s by a bounded coordinate not well defined, ignoring bounds.' % operation_noun) points = other.points if data_dimension is not None: points_shape = [1] * cube.data.ndim points_shape[data_dimension] = -1 points = points.reshape(points_shape) if in_place: new_cube = cube operation_function(new_cube.data, points, new_cube.data) else: new_cube = cube.copy(data=operation_function(cube.data, points)) if update_history: history = '%s %s %s (coordinate)' % (cube.name(), operation_symbol, other.name()) elif isinstance(other, iris.cube.Cube): # Deal with cube addition/subtraction by cube # get a coordinate comparison of this cube and the cube to do the operation with coord_comp = iris.analysis.coord_comparison(cube, other) if coord_comp['transposable']: raise ValueError('Cubes cannot be %s, differing axes. ' 'cube.transpose() may be required to re-order the axes.' % operation_past_tense) # provide a deprecation warning if the ignore keyword has been set if ignore is not True: warnings.warn('The "ignore" keyword has been deprecated in add/subtract. This functionality is now automatic. ' 'The provided value to "ignore" has been ignored, and has been automatically calculated.') bad_coord_grps = (coord_comp['ungroupable_and_dimensioned'] + coord_comp['resamplable']) if bad_coord_grps: raise ValueError('This operation cannot be performed as there are differing coordinates (%s) remaining ' 'which cannot be ignored.' % ', '.join({coord_grp.name() for coord_grp in bad_coord_grps})) if in_place: new_cube = cube operation_function(new_cube.data, other.data, new_cube.data) else: new_cube = cube.copy(data=operation_function(cube.data, other.data)) # If a coordinate is to be ignored - remove it ignore = filter(None, [coord_grp[0] for coord_grp in coord_comp['ignorable']]) if not ignore: ignore_string = '' else: ignore_string = ' (ignoring %s)' % ', '.join([coord.name() for coord in ignore]) for coord in ignore: new_cube.remove_coord(coord) if update_history: history = '%s %s %s%s' % (cube.name() or 'unknown', operation_symbol, other.name() or 'unknown', ignore_string) else: return NotImplemented iris.analysis.clear_phenomenon_identity(new_cube) if history is not None: new_cube.add_history(history) return new_cube
def save(cube, filename, netcdf_format='NETCDF4'): """ Save a cube to a netCDF file, given the cube and the filename. Args: * cube (:class:`iris.cube.Cube`): The :class:`iris.cube.Cube` to be saved to a netCDF file. * filename (string): Name of the netCDF file to save the cube. * netcdf_format (string): Underlying netCDF file format, one of 'NETCDF4', 'NETCDF4_CLASSIC', 'NETCDF3_CLASSIC' or 'NETCDF3_64BIT'. Default is 'NETCDF4' format. Returns: None. """ if not isinstance(cube, iris.cube.Cube): raise TypeError('Expecting a single cube instance, got %r.' % type(cube)) if netcdf_format not in ['NETCDF4', 'NETCDF4_CLASSIC', 'NETCDF3_CLASSIC', 'NETCDF3_64BIT']: raise ValueError('Unknown netCDF file format, got %r' % netcdf_format) if len(cube.aux_factories) > 1: raise ValueError('Multiple auxiliary factories are not supported.') dataset = netCDF4.Dataset(filename, mode='w', format=netcdf_format) # Create the CF-netCDF data dimension names. dimension_names = [] for dim in xrange(cube.ndim): coords = cube.coords(dimensions=dim, dim_coords=True) if coords is not None: if len(coords) != 1: raise iris.exceptions.IrisError('Cube appears to have multiple dimension coordinates on dimension %d' % dim) dimension_names.append(coords[0].name()) else: # There are no CF-netCDF coordinates describing this data dimension. dimension_names.append('dim%d' % dim) # Create the CF-netCDF data dimensions. # Make the outermost dimension an unlimited dimension. if dimension_names: dataset.createDimension(dimension_names[0]) for dim_name, dim_len in zip(dimension_names, cube.shape)[1:]: dataset.createDimension(dim_name, dim_len) # Identify the collection of coordinates that represent CF-netCDF coordinate variables. cf_coordinates = cube.dim_coords # Create the associated cube CF-netCDF data variable. cf_var_cube = _create_cf_data_variable(dataset, cube, dimension_names) factory_defn = None if cube.aux_factories: factory = cube.aux_factories[0] factory_defn = _FACTORY_DEFNS.get(type(factory), None) # Ensure we create the netCDF coordinate variables first. for coord in cf_coordinates: # Create the associated coordinate CF-netCDF variable. _create_cf_variable(dataset, cube, dimension_names, coord, factory_defn) # List of CF-netCDF auxiliary coordinate variable names. auxiliary_coordinate_names = [] for coord in sorted(cube.aux_coords, key=lambda coord: coord.name()): # Create the associated coordinate CF-netCDF variable. cf_name = _create_cf_variable(dataset, cube, dimension_names, coord, factory_defn) if cf_name is not None: auxiliary_coordinate_names.append(cf_name) # Add CF-netCDF auxiliary coordinate variable references to the CF-netCDF data variable. if auxiliary_coordinate_names: cf_var_cube.coordinates = ' '.join(sorted(auxiliary_coordinate_names)) # Flush any buffered data to the CF-netCDF file before closing. dataset.sync() dataset.close()
def seasonal_statistics(cube, operator='mean', seasons=('DJF', 'MAM', 'JJA', 'SON')): """Compute seasonal statistics. Chunks time seasons and computes statistics over them. Parameters ---------- cube: iris.cube.Cube input cube. operator: str, optional Select operator to apply. Available operators: 'mean', 'median', 'std_dev', 'sum', 'min', 'max', 'rms' seasons: list or tuple of str, optional Seasons to build. Available: ('DJF', 'MAM', 'JJA', SON') (default) and all sequentially correct combinations holding every month of a year: e.g. ('JJAS','ONDJFMAM'), or less in case of prior season extraction. Returns ------- iris.cube.Cube Seasonal statistic cube """ seasons = tuple([sea.upper() for sea in seasons]) if any([len(sea) < 2 for sea in seasons]): raise ValueError( f"Minimum of 2 month is required per Seasons: {seasons}.") if not cube.coords('clim_season'): iris.coord_categorisation.add_season(cube, 'time', name='clim_season', seasons=seasons) else: old_seasons = list(set(cube.coord('clim_season').points)) if not all([osea in seasons for osea in old_seasons]): raise ValueError( f"Seasons {seasons} do not match prior season extraction " f"{old_seasons}.") if not cube.coords('season_year'): iris.coord_categorisation.add_season_year(cube, 'time', name='season_year', seasons=seasons) operator = get_iris_analysis_operation(operator) cube = cube.aggregated_by(['clim_season', 'season_year'], operator) # CMOR Units are days so we are safe to operate on days # Ranging on [29, 31] days makes this calendar-independent # the only season this could not work is 'F' but this raises an # ValueError def spans_full_season(cube): """Check for all month present in the season. Parameters ---------- cube: iris.cube.Cube input cube. Returns ------- bool truth statement if time bounds are within (month*29, month*31) """ time = cube.coord('time') num_days = [(tt.bounds[0, 1] - tt.bounds[0, 0]) for tt in time] seasons = cube.coord('clim_season').points tar_days = [(len(sea) * 29, len(sea) * 31) for sea in seasons] return [dt[0] <= dn <= dt[1] for dn, dt in zip(num_days, tar_days)] full_seasons = spans_full_season(cube) return cube[full_seasons]
def _multiply_divide_common(operation_function, operation_symbol, operation_noun, cube, other, dim=None, update_history=True): """ Function which shares common code between multiplication and division of cubes. operation_function - function which does the operation (e.g. numpy.divide) operation_symbol - the textual symbol of the operation (e.g. '/') operation_noun - the noun of the operation (e.g. 'division') operation_past_tense - the past tesnse of the operation (e.g. 'divided') .. seealso:: For information on the dim keyword argument see :func:`multiply`. """ if not isinstance(cube, iris.cube.Cube): raise TypeError('The "cube" argument must be an instance of iris.Cube.') if isinstance(other, (int, float)): other = np.array(other) other_unit = None history = None if isinstance(other, np.ndarray): _assert_compatible(cube, other) copy_cube = cube.copy(data=operation_function(cube.data, other)) if update_history: if other.ndim == 0: history = '%s %s %s' % (cube.name(), operation_symbol, other) else: history = '%s %s array' % (cube.name(), operation_symbol) other_unit = '1' elif isinstance(other, iris.coords.Coord): # Deal with cube multiplication/division by coordinate # What dimension are we processing? data_dimension = None if dim is not None: # Ensure the given dim matches the coord if other in cube.coords() and cube.coord_dims(other) != [dim]: raise ValueError("dim provided does not match dim found for coord") data_dimension = dim else: # Try and get a coord dim if other.shape != (1,): try: coord_dims = cube.coord_dims(other) data_dimension = coord_dims[0] if coord_dims else None except iris.exceptions.CoordinateNotFoundError: raise ValueError("Could not determine dimension for mul/div. Use mul(coord, dim=dim)") if other.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(other) if other.has_bounds(): warnings.warn('%s by a bounded coordinate not well defined, ignoring bounds.' % operation_noun) points = other.points # If the axis is defined then shape the provided points so that we can do the # division (this is needed as there is no "axis" keyword to numpy's divide/multiply) if data_dimension is not None: points_shape = [1] * cube.data.ndim points_shape[data_dimension] = -1 points = points.reshape(points_shape) copy_cube = cube.copy(data=operation_function(cube.data, points)) if update_history: history = '%s %s %s' % (cube.name(), operation_symbol, other.name()) other_unit = other.units elif isinstance(other, iris.cube.Cube): # Deal with cube multiplication/division by cube copy_cube = cube.copy(data=operation_function(cube.data, other.data)) if update_history: history = '%s %s %s' % (cube.name() or 'unknown', operation_symbol, other.name() or 'unknown') other_unit = other.units else: return NotImplemented # Update the units if operation_function == np.multiply: copy_cube.units = cube.units * other_unit elif operation_function == np.divide: copy_cube.units = cube.units / other_unit iris.analysis.clear_phenomenon_identity(copy_cube) if history is not None: copy_cube.add_history(history) return copy_cube
def _get_xy_coords(cube): """ Return the x and y coordinates from a cube. This function will preferentially return a pair of dimension coordinates (if there are more than one potential x or y dimension coordinates a ValueError will be raised). If the cube does not have a pair of x and y dimension coordinates it will return 1D auxiliary coordinates (including scalars). If there is not one and only one set of x and y auxiliary coordinates a ValueError will be raised. Having identified the x and y coordinates, the function checks that they have equal coordinate systems and that they do not occupy the same dimension on the cube. Args: * cube: An instance of :class:`iris.cube.Cube`. Returns: A tuple containing the cube's x and y coordinates. """ # Look for a suitable dimension coords first. x_coords = cube.coords(axis='x', dim_coords=True) if not x_coords: # If there is no x coord in dim_coords look for scalars or # monotonic coords in aux_coords. x_coords = [ coord for coord in cube.coords(axis='x', dim_coords=False) if coord.ndim == 1 and coord.is_monotonic() ] if len(x_coords) != 1: raise ValueError('Cube {!r} must contain a single 1D x ' 'coordinate.'.format(cube.name())) x_coord = x_coords[0] # Look for a suitable dimension coords first. y_coords = cube.coords(axis='y', dim_coords=True) if not y_coords: # If there is no y coord in dim_coords look for scalars or # monotonic coords in aux_coords. y_coords = [ coord for coord in cube.coords(axis='y', dim_coords=False) if coord.ndim == 1 and coord.is_monotonic() ] if len(y_coords) != 1: raise ValueError('Cube {!r} must contain a single 1D y ' 'coordinate.'.format(cube.name())) y_coord = y_coords[0] if x_coord.coord_system != y_coord.coord_system: raise ValueError("The cube's x ({!r}) and y ({!r}) " "coordinates must have the same coordinate " "system.".format(x_coord.name(), y_coord.name())) # The x and y coordinates must describe different dimensions # or be scalar coords. x_dims = cube.coord_dims(x_coord) x_dim = None if x_dims: x_dim = x_dims[0] y_dims = cube.coord_dims(y_coord) y_dim = None if y_dims: y_dim = y_dims[0] if x_dim is not None and y_dim == x_dim: raise ValueError("The cube's x and y coords must not describe the " "same data dimension.") return x_coord, y_coord
def _add_subtract_common(operation_function, operation_symbol, operation_noun, operation_past_tense, cube, other, dim=None, ignore=True, in_place=False): """ Function which shares common code between addition and subtraction of cubes. operation_function - function which does the operation (e.g. numpy.subtract) operation_symbol - the textual symbol of the operation (e.g. '-') operation_noun - the noun of the operation (e.g. 'subtraction') operation_past_tense - the past tense of the operation (e.g. 'subtracted') """ if not isinstance(cube, iris.cube.Cube): raise TypeError( 'The "cube" argument must be an instance of iris.Cube.') if isinstance(other, (int, float)): # Promote scalar to a coordinate and associate unit type with cube unit type other = np.array(other) # Check that the units of the cube and the other item are the same, or if the other does not have a unit, skip this test if cube.units != getattr(other, 'units', cube.units): raise iris.exceptions.NotYetImplementedError('Differing units (%s & %s) %s not implemented' % \ (cube.units, other.units, operation_noun)) if isinstance(other, np.ndarray): _assert_compatible(cube, other) if in_place: new_cube = cube operation_function(new_cube.data, other, new_cube.data) else: new_cube = cube.copy(data=operation_function(cube.data, other)) elif isinstance(other, iris.coords.Coord): # Deal with cube addition/subtraction by coordinate # What dimension are we processing? data_dimension = None if dim is not None: # Ensure the given dim matches the coord if other in cube.coords() and cube.coord_dims(other) != [dim]: raise ValueError( "dim provided does not match dim found for coord") data_dimension = dim else: # Try and get a coord dim if other.shape != (1, ): try: coord_dims = cube.coord_dims(other) data_dimension = coord_dims[0] if coord_dims else None except iris.exceptions.CoordinateNotFoundError: raise ValueError( "Could not determine dimension for add/sub. Use add(coord, dim=dim)" ) if other.ndim != 1: raise iris.exceptions.CoordinateMultiDimError(other) if other.has_bounds(): warnings.warn( '%s by a bounded coordinate not well defined, ignoring bounds.' % operation_noun) points = other.points if data_dimension is not None: points_shape = [1] * cube.ndim points_shape[data_dimension] = -1 points = points.reshape(points_shape) if in_place: new_cube = cube operation_function(new_cube.data, points, new_cube.data) else: new_cube = cube.copy(data=operation_function(cube.data, points)) elif isinstance(other, iris.cube.Cube): # Deal with cube addition/subtraction by cube # get a coordinate comparison of this cube and the cube to do the operation with coord_comp = iris.analysis.coord_comparison(cube, other) if coord_comp['transposable']: # User does not need to transpose their cubes if numpy # array broadcasting will make the dimensions match broadcast_padding = cube.ndim - other.ndim coord_dims_equal = True for coord_group in coord_comp['transposable']: cube_coord, other_coord = coord_group.coords cube_coord_dims = cube.coord_dims(coord=cube_coord) other_coord_dims = other.coord_dims(coord=other_coord) other_coord_dims_broadcasted = tuple( [dim + broadcast_padding for dim in other_coord_dims]) if cube_coord_dims != other_coord_dims_broadcasted: coord_dims_equal = False if not coord_dims_equal: raise ValueError('Cubes cannot be %s, differing axes. ' 'cube.transpose() may be required to ' 're-order the axes.' % operation_past_tense) # provide a deprecation warning if the ignore keyword has been set if ignore is not True: warnings.warn( 'The "ignore" keyword has been deprecated in add/subtract. This functionality is now automatic. ' 'The provided value to "ignore" has been ignored, and has been automatically calculated.' ) bad_coord_grps = (coord_comp['ungroupable_and_dimensioned'] + coord_comp['resamplable']) if bad_coord_grps: raise ValueError( 'This operation cannot be performed as there are differing coordinates (%s) remaining ' 'which cannot be ignored.' % ', '.join({coord_grp.name() for coord_grp in bad_coord_grps})) if in_place: new_cube = cube operation_function(new_cube.data, other.data, new_cube.data) else: new_cube = cube.copy( data=operation_function(cube.data, other.data)) # If a coordinate is to be ignored - remove it ignore = filter( None, [coord_grp[0] for coord_grp in coord_comp['ignorable']]) if not ignore: ignore_string = '' else: ignore_string = ' (ignoring %s)' % ', '.join( [coord.name() for coord in ignore]) for coord in ignore: new_cube.remove_coord(coord) else: return NotImplemented iris.analysis.clear_phenomenon_identity(new_cube) return new_cube