def create_cube(lon_min, lon_max, bounds=False): n_lons = max(lon_min, lon_max) - min(lon_max, lon_min) data = np.arange(4 * 3 * n_lons, dtype='f4').reshape(4, 3, n_lons) data = biggus.NumpyArrayAdapter(data) cube = Cube(data, standard_name='x_wind', units='ms-1') cube.add_dim_coord(iris.coords.DimCoord([0, 20, 40, 80], long_name='level_height', units='m'), 0) cube.add_aux_coord(iris.coords.AuxCoord([1.0, 0.9, 0.8, 0.6], long_name='sigma'), 0) cube.add_dim_coord(iris.coords.DimCoord([-45, 0, 45], 'latitude', units='degrees'), 1) step = 1 if lon_max > lon_min else -1 circular = (abs(lon_max - lon_min) == 360) cube.add_dim_coord(iris.coords.DimCoord(np.arange(lon_min, lon_max, step), 'longitude', units='degrees', circular=circular), 2) if bounds: cube.coord('longitude').guess_bounds() cube.add_aux_coord(iris.coords.AuxCoord( np.arange(3 * n_lons).reshape(3, n_lons) * 10, 'surface_altitude', units='m'), [1, 2]) cube.add_aux_factory(iris.aux_factory.HybridHeightFactory( cube.coord('level_height'), cube.coord('sigma'), cube.coord('surface_altitude'))) return cube
def setUp(self): # A (3, 2, 4) cube with a masked element. cube = Cube(np.ma.arange(24, dtype=np.int32).reshape((3, 2, 4))) cs = GeogCS(6371229) coord = DimCoord(points=np.array([-1, 0, 1], dtype=np.int32), standard_name='latitude', units='degrees', coord_system=cs) cube.add_dim_coord(coord, 0) coord = DimCoord(points=np.array([-1, 0, 1, 2], dtype=np.int32), standard_name='longitude', units='degrees', coord_system=cs) cube.add_dim_coord(coord, 2) cube.coord('latitude').guess_bounds() cube.coord('longitude').guess_bounds() cube.data[1, 1, 2] = ma.masked self.src_cube = cube # Create (7, 2, 9) grid cube. self.grid_cube = _resampled_grid(cube, 2.3, 2.4)
def latlon_from_cube(cube: Cube) -> ndarray: """ Produce an array of latitude-longitude coordinates used by an Iris cube. Args: cube: Cube with spatial coords. Returns: Latitude-longitude pairs (N x 2). """ lats_name, lons_name = latlon_names(cube) lats_data = cube.coord(lats_name).points lons_data = cube.coord(lons_name).points lats_mesh, lons_mesh = np.meshgrid(lats_data, lons_data, indexing="ij") latlon = np.dstack((lats_mesh, lons_mesh)).reshape((-1, 2)) return latlon
def _check_dim_coords(temperature: Cube, lapse_rate: Cube) -> None: """Throw an error if the dimension coordinates are not the same for temperature and lapse rate cubes Args: temperature lapse_rate """ for crd in temperature.coords(dim_coords=True): try: if crd != lapse_rate.coord(crd.name()): raise ValueError( 'Lapse rate cube coordinate "{}" does not match ' "temperature cube coordinate".format(crd.name())) except CoordinateNotFoundError: raise ValueError("Lapse rate cube has no coordinate " '"{}"'.format(crd.name()))
def _create_output_cube(self, orogenh_data: ndarray, reference_cube: Cube) -> Cube: """Creates a cube containing orographic enhancement values in SI units. Args: orogenh_data: Orographic enhancement value in mm h-1 reference_cube: Cube with the correct time and forecast period coordinates on the UK standard grid Returns: Orographic enhancement cube (m s-1) """ # create cube containing high resolution data in mm/h x_coord = self.topography.coord(axis="x") y_coord = self.topography.coord(axis="y") for coord in [x_coord, y_coord]: coord.points = coord.points.astype(np.float32) if coord.bounds is not None: coord.bounds = coord.bounds.astype(np.float32) aux_coords = [] for coord in ["time", "forecast_reference_time", "forecast_period"]: aux_coords.append((reference_cube.coord(coord), None)) attributes = generate_mandatory_attributes([reference_cube]) attributes["title"] = "unknown" # remove possible wrong grid info. for key in MOSG_GRID_ATTRIBUTES: try: attributes[key] = self.topography.attributes[key] except KeyError: pass orog_enhance_cube = iris.cube.Cube( orogenh_data, long_name="orographic_enhancement", units="mm h-1", attributes=attributes, dim_coords_and_dims=[(y_coord, 0), (x_coord, 1)], aux_coords_and_dims=aux_coords, ) orog_enhance_cube.convert_units("m s-1") return orog_enhance_cube
def process(self, cube: Cube, coord_name: str, midpoint: float) -> Cube: """Calculate triangular weights for a given cube and coord. Args: cube: Cube to blend across the coord. coord_name: Name of coordinate in the cube to be blended. midpoint: The centre point of the triangular function. This is assumed to be provided in the same units as "self.width", ie "self.parameter_units" as initialised. Returns: 1D cube of normalised (sum = 1.0) weights matching length of input dimension to be blended. Raises: TypeError : input is not a cube """ if not isinstance(cube, iris.cube.Cube): msg = ( "The first argument must be an instance of " "iris.cube.Cube but is" " {0:s}".format(str(type(cube))) ) raise TypeError(msg) cube_coord = cube.coord(coord_name) coord_vals = cube_coord.points coord_units = cube_coord.units # Rescale width and midpoint if in different units to the coordinate if coord_units != self.parameters_units: width_in_coord_units = self.parameters_units.convert( self.width, coord_units ) midpoint = self.parameters_units.convert(midpoint, coord_units) else: width_in_coord_units = copy.deepcopy(self.width) weights = self.triangular_weights(coord_vals, midpoint, width_in_coord_units) weights_cube = WeightsUtilities.build_weights_cube(cube, weights, coord_name) return weights_cube
def _update_metadata(self, cube: Cube) -> None: """Rename the cube and add attributes to the threshold coordinate after merging """ threshold_coord = cube.coord(self.threshold_coord_name) threshold_coord.attributes.update( {"spp__relative_to_threshold": self.comparison_operator.spp_string} ) if cube.cell_methods: format_cell_methods_for_probability(cube, self.threshold_coord_name) cube.rename( "probability_of_{parameter}_{relative_to}_threshold".format( parameter=self.threshold_coord_name, relative_to=probability_is_above_or_below(cube), ) ) cube.units = Unit(1)
def _gradient_from_diff(diff: Cube, axis: str) -> ndarray: """ Calculate the gradient along the x or y axis from differences between adjacent grid squares. Args: diff: Cube containing differences along the x or y axis axis: Short-hand reference for the x or y coordinate, as allowed by iris.util.guess_coord_axis. Returns: Array of the gradients in the coordinate direction specified. """ grid_spacing = np.diff(diff.coord(axis=axis).points)[0] gradient = diff.data / grid_spacing return gradient
def _run_recursion( cube: Cube, smoothing_coefficients_x: Cube, smoothing_coefficients_y: Cube, iterations: int, ) -> Cube: """ Method to run the recursive filter. Args: cube: 2D cube containing the input data to which the recursive filter will be applied. smoothing_coefficients_x: 2D cube containing array of smoothing_coefficient values that will be used when applying the recursive filter along the x-axis. smoothing_coefficients_y: 2D cube containing array of smoothing_coefficient values that will be used when applying the recursive filter along the y-axis. iterations: The number of iterations of the recursive filter Returns: Cube containing the smoothed field after the recursive filter method has been applied to the input cube. """ (x_index, ) = cube.coord_dims(cube.coord(axis="x").name()) (y_index, ) = cube.coord_dims(cube.coord(axis="y").name()) output = cube.data for _ in range(iterations): output = RecursiveFilter._recurse_forward( output, smoothing_coefficients_x.data, x_index) output = RecursiveFilter._recurse_backward( output, smoothing_coefficients_x.data, x_index) output = RecursiveFilter._recurse_forward( output, smoothing_coefficients_y.data, y_index) output = RecursiveFilter._recurse_backward( output, smoothing_coefficients_y.data, y_index) cube.data = output return cube
def _fill_timezones(self, input_cube: Cube) -> None: """ Populates the output cube data with data from input_cube. This is done by multiplying the inverse of the timezone_cube.data with the input_cube.data and summing along the time axis. Because timezone_cube.data is a mask of 1 and 0, inverting it gives 1 where we WANT data and 0 where we don't. Summing these up produces the result. The same logic can be used for times. Modifies self.output_cube and self.time_points. Assumes that input_cube and self.timezone_cube have been arranged so that time or UTC_offset are the inner-most coord (dim=-1). Args: input_cube: Cube of data to extract timezone-offsets from. Must contain a time coord spanning all the timezones. Raises: TypeError: If combining the timezone_cube and input_cube results in float64 data. (Hint: timezone_cube should be int8 and input cube should be float32) """ # Get the output_data result = input_cube.data * (1 - self.timezone_cube.data) self.output_data = result.sum(axis=-1) # Check resulting dtype enforce_dtype("multiply", [input_cube, self.timezone_cube], result) # Sort out the time points input_time_points = input_cube.coord("time").points # Add scalar coords to allow broadcast to spatial coords. times = input_time_points.reshape((1, 1, len(input_time_points))) * ( 1 - self.timezone_cube.data ) self.time_points = times.sum(axis=-1) # Sort out the time bounds (if present) bounds_offsets = self._get_time_bounds_offset(input_cube) if bounds_offsets is not None: # Add scalar coords to allow broadcast to spatial coords. self.time_bounds = bounds_offsets.reshape( (1, 1, 2) ) + self.time_points.reshape(list(self.time_points.shape) + [1])
def to_threshold_inequality(cube: Cube, above: bool = True) -> Cube: """Takes a cube and a target relative to threshold inequality; above or not above. The function returns probabilities in relation to the threshold values with the target inequality. The threshold inequality is limited to being above (above=True) or below (above=False) a threshold, rather than more specific targets such as "greater_than_or_equal_to". It is not possible to flip probabilities from e.g. "less_than_or_equal_to" to "greater_than_or_equal_to", only to "greater_than". As such the operation will use the valid inversion that achieves the broader target inequality. Args: cube: A probability cube with a threshold coordinate. above: Targets an above (gt, ge) threshold inequality if True, otherwise targets a below (lt, le) threshold inequality if False. Returns: A cube with the probabilities relative to the threshold values with the target inequality. Raised: ValueError: If the input cube has no threshold coordinate. """ try: threshold = cube.coord(var_name="threshold") except CoordinateNotFoundError: raise ValueError( "Cube does not have a threshold coordinate, probabilities " "cannot be inverted if present.") inequality = threshold.attributes["spp__relative_to_threshold"] spp_lookup = comparison_operator_dict() above_attr = [spp_lookup[ineq].spp_string for ineq in ["ge", "gt"]] below_attr = [spp_lookup[ineq].spp_string for ineq in ["le", "lt"]] if (inequality in below_attr and above) or (inequality in above_attr and not above): return invert_probabilities(cube) return cube
def check_radius_against_distance(cube: Cube, radius: float) -> None: """Check required distance isn't greater than the size of the domain. Args: cube: The cube to check. radius: The radius, which cannot be more than half of the size of the domain. """ axes = [] for axis in ["x", "y"]: coord = cube.coord(axis=axis).copy() coord.convert_units("metres") axes.append((max(coord.points) - min(coord.points))) max_allowed = np.sqrt(axes[0]**2 + axes[1]**2) * 0.5 if radius > max_allowed: raise ValueError(f"Distance of {radius}m exceeds max domain " f"distance of {max_allowed}m")
def remove_cube_halo(cube: Cube, halo_radius: float) -> Cube: """ Remove halo of halo_radius from a cube. This function converts the halo radius into the number of grid points in the x and y coordinate that need to be removed. It then calls remove_halo_from_cube which only acts on a cube with x and y coordinates so we need to slice the cube and them merge the cube back together ensuring the resulting cube has the same dimension coordinates. Args: cube: Cube on extended grid halo_radius: Size of border to remove, in metres Returns: New cube with the halo removed. """ halo_size_x = distance_to_number_of_grid_cells(cube, halo_radius, axis="x") halo_size_y = distance_to_number_of_grid_cells(cube, halo_radius, axis="y") result_slices = iris.cube.CubeList() for cube_slice in cube.slices([cube.coord(axis="y"), cube.coord(axis="x")]): cube_halo = remove_halo_from_cube(cube_slice, halo_size_x, halo_size_y) result_slices.append(cube_halo) result = result_slices.merge_cube() # re-promote any scalar dimensions lost in slice / merge req_dims = get_dim_coord_names(cube) present_dims = get_dim_coord_names(result) for coord in req_dims: if coord not in present_dims: result = iris.util.new_axis(result, coord) # re-order (needed if scalar dimensions have been re-added) enforce_coordinate_ordering(result, req_dims) return result
def create_difference_cube( cube: Cube, coord_name: str, diff_along_axis: ndarray ) -> Cube: """ Put the difference array into a cube with the appropriate metadata. Args: cube: Cube from which the differences have been calculated. coord_name: The name of the coordinate over which the difference have been calculated. diff_along_axis: Array containing the differences. Returns: Cube containing the differences calculated along the specified axis. """ points = cube.coord(coord_name).points mean_points = (points[1:] + points[:-1]) / 2 # Copy cube metadata and coordinates into a new cube. # Create a new coordinate for the coordinate along which the # difference has been calculated. metadata_dict = copy.deepcopy(cube.metadata._asdict()) diff_cube = Cube(diff_along_axis, **metadata_dict) for coord in cube.dim_coords: dims = cube.coord_dims(coord) if coord.name() in [coord_name]: coord = coord.copy(points=mean_points) diff_cube.add_dim_coord(coord.copy(), dims) for coord in cube.aux_coords: dims = cube.coord_dims(coord) diff_cube.add_aux_coord(coord.copy(), dims) for coord in cube.derived_coords: dims = cube.coord_dims(coord) diff_cube.add_aux_coord(coord.copy(), dims) return diff_cube
def _update_metadata(self, output_cube: Cube, original_units: Unit) -> None: """ Update output cube name and threshold coordinate Args: output_cube: Cube containing new "between_thresholds" probabilities original_units: Required threshold-type coordinate units """ new_name = self.cube.name().replace( "{}_threshold".format(probability_is_above_or_below(self.cube)), "between_thresholds", ) output_cube.rename(new_name) new_thresh_coord = output_cube.coord(self.thresh_coord.name()) new_thresh_coord.convert_units(original_units) new_thresh_coord.attributes[ "spp__relative_to_threshold"] = "between_thresholds"
def check_cell_methods(self, cube: Cube) -> None: """Checks cell methods are permitted and correct""" if any([substr in cube.name() for substr in PRECIP_ACCUM_NAMES]): msg = f"Expected sum over time cell method for {cube.name()}" if not cube.cell_methods: self.errors.append(msg) else: found_cm = False for cm in cube.cell_methods: if ( cm.method == PRECIP_ACCUM_CM.method and cm.coord_names == PRECIP_ACCUM_CM.coord_names ): found_cm = True if not found_cm: self.errors.append(msg) for cm in cube.cell_methods: if cm.method in COMPLIANT_CM_METHODS: self.methods += f" {cm.method} over {cm.coord_names[0]}" if self.field_type == self.PROB: if not cm.comments or cm.comments[0] != f"of {self.diagnostic}": self.errors.append( f"Cell method {cm} on probability data should have comment " f"'of {self.diagnostic}'" ) # check point and bounds on method coordinate if "time" in cm.coord_names: if cube.coord("time").bounds is None: self.errors.append(f"Cube of{self.methods} has no time bounds") elif cm in NONCOMP_CMS or cm.method in NONCOMP_CM_METHODS: self.errors.append(f"Non-standard cell method {cm}") else: # flag method which might be invalid, but we can't be sure self.warnings.append( f"Unexpected cell method {cm}. Please check the standard to " "ensure this is valid" )
def invert_probabilities(cube: Cube) -> Cube: """Given a cube with a probability threshold, invert the probabilities relative to the existing thresholding inequality. Update the coordinate metadata to indicate the new threshold inequality. Args: cube: A probability cube with a threshold coordinate. Returns: Cube with the probabilities inverted relative to the input thresholding inequality. Raises: ValueError: If no threshold coordinate is found. """ try: threshold = cube.coord(var_name="threshold") except CoordinateNotFoundError: raise ValueError( "Cube does not have a threshold coordinate, probabilities " "cannot be inverted if present.") comparison_operator_lookup = comparison_operator_dict() inequality = threshold.attributes["spp__relative_to_threshold"] (inverse, ) = set([ value.inverse for key, value in comparison_operator_lookup.items() if value.spp_string == inequality ]) new_inequality = comparison_operator_lookup[inverse].spp_string inverted_probabilities = cube.copy(data=(1.0 - cube.data)) inverted_probabilities.coord( threshold).attributes["spp__relative_to_threshold"] = new_inequality new_name = (cube.name().replace("above", "below") if "above" in cube.name() else cube.name().replace("below", "above")) inverted_probabilities.rename(new_name) return inverted_probabilities
def _update_time(input_time: Coord, advected_cube: Cube, timestep: timedelta) -> None: """Increment validity time on the advected cube Args: input_time: Time coordinate from source cube advected_cube: Cube containing advected data (modified in place) timestep: Time difference between the advected output and the source """ original_datetime = next(input_time.cells())[0] new_datetime = original_datetime + timestep new_time = input_time.units.date2num(new_datetime) time_coord_name = "time" time_coord_spec = TIME_COORDS[time_coord_name] time_coord = advected_cube.coord(time_coord_name) time_coord.points = new_time time_coord.convert_units(time_coord_spec.units) time_coord.points = round_close(time_coord.points, dtype=time_coord_spec.dtype)
def extract_coordinates(self, neighbour_cube: Cube) -> Cube: """ Extract the desired set of grid coordinates that correspond to spot sites from the neighbour cube. Args: neighbour_cube: A cube containing information about the spot data sites and their grid point neighbours. Returns: A cube containing only the x and y grid coordinates for the grid point neighbours given the chosen neighbour selection method. The neighbour cube contains the indices stored as floating point values, so they are converted to integers in this cube. Raises: ValueError if the neighbour_selection_method expected is not found in the neighbour cube. """ method = iris.Constraint( neighbour_selection_method_name=self.neighbour_selection_method ) index_constraint = iris.Constraint(grid_attributes_key=["x_index", "y_index"]) coordinate_cube = neighbour_cube.extract(method & index_constraint) if coordinate_cube: coordinate_cube.data = np.rint(coordinate_cube.data).astype(int) return coordinate_cube available_methods = neighbour_cube.coord( "neighbour_selection_method_name" ).points raise ValueError( 'The requested neighbour_selection_method "{}" is not available in' " this neighbour_cube. Available methods are: {}.".format( self.neighbour_selection_method, available_methods ) )
def _get_cycletime_point(cube: Cube, cycletime: str) -> int64: """ For cycle and model blending, establish the current cycletime to set on the cube after blending. Args: blended_cube cycletime: Current cycletime in YYYYMMDDTHHmmZ format Returns: Cycle time point in units matching the input cube forecast reference time coordinate """ frt_coord = cube.coord("forecast_reference_time") frt_units = frt_coord.units.origin frt_calendar = frt_coord.units.calendar # raises TypeError if cycletime is None cycletime_point = cycletime_to_number(cycletime, time_unit=frt_units, calendar=frt_calendar) return round_close(cycletime_point, dtype=np.int64)
def find_threshold_coordinate(cube: Cube) -> Coord: """Find threshold coordinate in cube. Compatible with both the old (cube.coord("threshold")) and new (cube.coord.var_name == "threshold") IMPROVER metadata standards. Args: cube: Cube containing thresholded probability data Returns: Threshold coordinate Raises: TypeError: If cube is not of type iris.cube.Cube. CoordinateNotFoundError: If no threshold coordinate is found. """ if not isinstance(cube, iris.cube.Cube): msg = ( "Expecting data to be an instance of " "iris.cube.Cube but is {0}.".format(type(cube)) ) raise TypeError(msg) threshold_coord = None try: threshold_coord = cube.coord("threshold") except CoordinateNotFoundError: for coord in cube.coords(): if coord.var_name == "threshold": threshold_coord = coord break if threshold_coord is None: msg = "No threshold coord found on {0:s} data".format(cube.name()) raise CoordinateNotFoundError(msg) return threshold_coord
def _set_time(self, cube: Cube) -> None: """ Set self.time to a datetime object specifying the date and time for which the masks should be created. self.time is set in UTC. Args: cube: The cube from which the validity time should be taken if one has not been explicitly provided by the user. """ if self.time: self.time = datetime.strptime(self.time, "%Y%m%dT%H%MZ") self.time = pytz.utc.localize(self.time) else: try: self.time = cube.coord("time").cell(0).point self.time = pytz.utc.localize(self.time) except CoordinateNotFoundError: msg = ( "The input cube does not contain a 'time' coordinate. " "As such a time must be provided by the user." ) raise ValueError(msg)
def find_heightgrid(self, wind: Cube) -> ndarray: """Setup the height grid. Setup the height grid either from the 1D or 3D height grid that was supplied to the plugin or from the z-axis information from the wind grid. Args: wind: 3D or 4D - representing the wind data. Returns: 1D or 3D array - representing the height grid. """ if self.height_levels is None: hld = wind.coord(self.z_name).points else: hld = iris.util.squeeze(self.height_levels) if np.isnan(hld.data).any() or (hld.data == RMDI).any(): raise ValueError("height grid contains invalid points") if hld.ndim == 3: try: xap, yap, zap, _ = self.find_coord_order(hld) hld.transpose([yap, xap, zap]) except KeyError: raise ValueError("height grid different from wind grid") elif hld.ndim == 1: try: hld = next(hld.slices([self.z_name])) except CoordinateNotFoundError: raise ValueError("height z coordinate differs from wind z") else: raise ValueError("hld must have a dimension length of " "either 3 or 1" "hld.ndim is {}".format(hld.ndim)) hld = hld.data return hld
def build_KDTree(self, land_mask: Cube) -> Tuple[cKDTree, ndarray]: """ Build a KDTree for extracting the nearest point or points to a site. The tree can be built with a constrained set of grid points, e.g. only land points, if required. Args: land_mask: A land mask cube for the model/grid from which grid point neighbours are being selected. Returns: - A KDTree containing the required nodes, built using the scipy cKDTree method. - An array of shape (n_nodes, 2) that contains the x and y indices that correspond to the selected node, e.g. node=100 --> x_coord_index=10, y_coord_index=300, index_nodes[100] = [10, 300] """ if self.land_constraint: included_points = np.nonzero(land_mask.data) else: included_points = np.where(np.isfinite(land_mask.data.data)) x_indices = included_points[0] y_indices = included_points[1] x_coords = land_mask.coord(axis="x").points[x_indices] y_coords = land_mask.coord(axis="y").points[y_indices] if self.global_coordinate_system: nodes = self.geocentric_cartesian(land_mask, x_coords, y_coords) else: nodes = list(zip(x_coords, y_coords)) index_nodes = np.array(list(zip(x_indices, y_indices))) return cKDTree(nodes), index_nodes
def calculate_input_grid_spacing(cube_in: Cube) -> Tuple[float, float]: """ Calculate grid spacing in latitude and logitude. Check if input source grid is on even-spacing and ascending lat/lon system. Args: cube_in: Input source cube. Returns: - Grid spacing in latitude, in degree. - Grid spacing in logitude, in degree. Raises: ValueError: If input grid is not on a latitude/longitude system or input grid coordinates are not ascending. """ # check if in lat/lon system if lat_lon_determine(cube_in) is not None: raise ValueError("Input grid is not on a latitude/longitude system") # calculate grid spacing lon_spacing = calculate_grid_spacing(cube_in, "degree", axis="x", rtol=4.0e-5) lat_spacing = calculate_grid_spacing(cube_in, "degree", axis="y", rtol=4.0e-5) y_coord = cube_in.coord(axis="y").points x_coord = cube_in.coord(axis="x").points if x_coord[-1] < x_coord[0] or y_coord[-1] < y_coord[0]: raise ValueError("Input grid coordinates are not ascending.") return lat_spacing, lon_spacing
def apply_circular_kernel(self, cube: Cube, ranges: int) -> Cube: """ Method to apply a circular kernel to the data within the input cube in order to smooth the resulting field. Args: cube: Cube containing to array to apply CircularNeighbourhood processing to. ranges: Number of grid cells in the x and y direction used to create the kernel. Returns: Cube containing the smoothed field after the kernel has been applied. """ data = cube.data full_ranges = np.zeros([np.ndim(data)]) axes = [] for axis in ["x", "y"]: coord_name = cube.coord(axis=axis).name() axes.append(cube.coord_dims(coord_name)[0]) for axis in axes: full_ranges[axis] = ranges self.kernel = circular_kernel(full_ranges, ranges, self.weighted_mode) # Smooth the data by applying the kernel. if self.sum_or_fraction == "sum": total_area = 1.0 else: # sum_or_fraction is in fraction mode total_area = np.sum(self.kernel) cube.data = correlate(data, self.kernel, mode="nearest") / total_area return cube
def percentile_weighted_mean(self, cube: Cube, weights: Optional[Cube]) -> Cube: """ Blend percentile data using the weights provided. Args: cube: The cube which is being blended over self.blend_coord. Assumes self.blend_coord and percentile are leading coordinates (enforced in process). weights: Cube of blending weights. Returns: The cube with percentile values blended over self.blend_coord, with suitable weightings applied. """ non_perc_slice = next(cube.slices_over(PERC_COORD)) weights_array = self.get_weights_array(non_perc_slice, weights) weights_array = self._normalise_weights(weights_array) # Set up aggregator PERCENTILE_BLEND = Aggregator( "mean", # Use CF-compliant cell method. PercentileBlendingAggregator.aggregate, ) cube_new = collapsed( cube, self.blend_coord, PERCENTILE_BLEND, percentiles=cube.coord(PERC_COORD).points, arr_weights=weights_array, ) return cube_new
def ensure_monotonic_increase_in_chosen_direction(self, cube: Cube) -> Cube: """Ensure that the chosen coordinate is monotonically increasing in the specified direction. Args: cube: The cube containing the coordinate to check. Note that the input cube will be modified by this method. Returns: The cube containing a coordinate that is monotonically increasing in the desired direction. """ coord_name = self.coord_name_to_integrate increasing_order = np.all(np.diff(cube.coord(coord_name).points) > 0) if increasing_order and not self.positive_integration: cube = sort_coord_in_cube(cube, coord_name, descending=True) if not increasing_order and self.positive_integration: cube = sort_coord_in_cube(cube, coord_name) return cube
def get_weights_array(self, cube: Cube, weights: Optional[Cube]) -> ndarray: """ Given a 1 or multidimensional cube of weights, reshape and broadcast these to the shape of the data cube. If no weights are provided, an array of weights is returned that equally weights all slices across the blending coordinate. Args: cube: Template cube to reshape weights, with a leading blend coordinate weights: Cube of initial blending weights or None Returns: An array of weights that matches the template cube shape. """ if weights: weights_array = self.shape_weights(cube, weights) else: (number_of_fields,) = cube.coord(self.blend_coord).shape weight = FLOAT_DTYPE(1.0 / number_of_fields) weights_array = np.broadcast_to(weight, cube.shape) return weights_array
def process(self, cube: Cube) -> Tuple[Cube, Cube]: """ Calculate the difference along the x and y axes and return the result in separate cubes. The difference along each axis is calculated using numpy.diff. Args: cube: Cube from which the differences will be calculated. Returns: - Cube after the differences have been calculated along the x axis. - Cube after the differences have been calculated along the y axis. """ diffs = [] for axis in ["x", "y"]: coord_name = cube.coord(axis=axis).name() diff_cube = self.create_difference_cube( cube, coord_name, self.calculate_difference(cube, coord_name)) self._update_metadata(diff_cube, coord_name, cube.name()) diffs.append(diff_cube) return tuple(diffs)
def calc_true_north_offset(reference_cube: Cube) -> ndarray: """ Calculate the angles between grid North and true North, as a matrix of values on the grid of the input reference cube. Args: reference_cube: 2D cube on grid for which "north" is required. Provides both coordinate system (reference_cube.coord_system()) and template spatial grid on which the angle adjustments should be provided. Returns: Angle in radians by which wind direction wrt true North at each point must be rotated to be relative to grid North. """ reference_x_coord = reference_cube.coord(axis="x") reference_y_coord = reference_cube.coord(axis="y") # find corners of reference_cube grid in lat / lon coordinates latlon = [ GLOBAL_CRS.as_cartopy_crs().transform_point( reference_x_coord.points[i], reference_y_coord.points[j], reference_cube.coord_system().as_cartopy_crs(), ) for i in [0, -1] for j in [0, -1] ] latlon = np.array(latlon).T.tolist() # define lat / lon coordinates to cover the reference_cube grid at an # equivalent resolution lat_points = np.linspace( np.floor(min(latlon[1])), np.ceil(max(latlon[1])), len(reference_y_coord.points), ) lon_points = np.linspace( np.floor(min(latlon[0])), np.ceil(max(latlon[0])), len(reference_x_coord.points), ) lat_coord = DimCoord( lat_points, "latitude", units="degrees", coord_system=GLOBAL_CRS ) lon_coord = DimCoord( lon_points, "longitude", units="degrees", coord_system=GLOBAL_CRS ) # define a unit vector wind towards true North over the lat / lon grid udata = np.zeros(reference_cube.shape, dtype=np.float32) vdata = np.ones(reference_cube.shape, dtype=np.float32) ucube_truenorth = Cube( udata, "grid_eastward_wind", dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)], ) vcube_truenorth = Cube( vdata, "grid_northward_wind", dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)], ) # rotate unit vector onto reference_cube coordinate system ucube, vcube = rotate_winds( ucube_truenorth, vcube_truenorth, reference_cube.coord_system() ) # unmask and regrid rotated winds onto reference_cube grid ucube.data = ucube.data.data ucube = ucube.regrid(reference_cube, Linear()) vcube.data = vcube.data.data vcube = vcube.regrid(reference_cube, Linear()) # ratio of u to v winds is the tangent of the angle which is the # true North to grid North rotation angle_adjustment = np.arctan2(ucube.data, vcube.data) return angle_adjustment
def make_mock_cube(lat_dim_length=5, lon_dim_length=3, lon_range=None, alt_dim_length=0, pres_dim_length=0, time_dim_length=0, horizontal_offset=0, altitude_offset=0, pressure_offset=0, time_offset=0, data_offset=0, surf_pres_offset=0, hybrid_ht_len=0, hybrid_pr_len=0, geopotential_height=False, dim_order=None, mask=False): """ Makes a cube of any shape required, with coordinate offsets from the default available. If no arguments are given get a 5x3 cube of the form: array([[1,2,3], [4,5,6], [7,8,9], [10,11,12], [13,14,15]]) and coordinates in latitude: array([ -10, -5, 0, 5, 10 ]) longitude: array([ -5, 0, 5 ]) :param lat_dim_length: Latitude grid length :param lon_dim_length: Longitude grid length :param alt_dim_length: Altitude grid length :param pres_dim_length: Pressure grid length :param time_dim_length: Time grid length :param horizontal_offset: Offset from the default grid, in degrees, in lat and lon :param altitude_offset: Offset from the default grid in altitude :param pressure_offset: Offset from the default grid in pressure :param time_offset: Offset from the default grid in time :param data_offset: Offset from the default data values :param surf_pres_offset: Offset for the optional surface pressure field :param hybrid_ht_len: Hybrid height grid length :param hybrid_pr_len: Hybrid pressure grid length :param geopotential_height: Include a geopotential height field when calcluting a hybrid pressure? (default False) :param dim_order: List of 'lat', 'lon', 'alt', 'pres', 'time' in the order in which the dimensions occur :param mask: A mask to apply to the data, this should be either a scalar or the same shape as the data :return: A cube with well defined data. """ import iris from iris.aux_factory import HybridHeightFactory, HybridPressureFactory data_size = 1 DIM_NAMES = ['lat', 'lon', 'alt', 'pres', 'time', 'hybrid_ht', 'hybrid_pr'] dim_lengths = [lat_dim_length, lon_dim_length, alt_dim_length, pres_dim_length, time_dim_length, hybrid_ht_len, hybrid_pr_len] lon_range = lon_range or (-5., 5.) if dim_order is None: dim_order = list(DIM_NAMES) if any([True for d in dim_order if d not in DIM_NAMES]): raise ValueError("dim_order contains unrecognised name") for idx, dim in enumerate(DIM_NAMES): if dim_lengths[idx] == 0 and dim in dim_order: del dim_order[dim_order.index(dim)] coord_map = {} for idx, dim in enumerate(dim_order): coord_map[dim] = dim_order.index(dim) coord_list = [None] * len(coord_map) if lat_dim_length: coord_list[coord_map['lat']] = (DimCoord(np.linspace(-10., 10., lat_dim_length) + horizontal_offset, standard_name='latitude', units='degrees', var_name='lat'), coord_map['lat']) data_size *= lat_dim_length if lon_dim_length: coord_list[coord_map['lon']] = ( DimCoord(np.linspace(lon_range[0], lon_range[1], lon_dim_length) + horizontal_offset, standard_name='longitude', units='degrees', var_name='lon'), coord_map['lon']) data_size *= lon_dim_length if alt_dim_length: coord_list[coord_map['alt']] = (DimCoord(np.linspace(0., 7., alt_dim_length) + altitude_offset, standard_name='altitude', units='metres', var_name='alt'), coord_map['alt']) data_size *= alt_dim_length if pres_dim_length: coord_list[coord_map['pres']] = (DimCoord(np.linspace(0., 7., pres_dim_length) + pressure_offset, standard_name='air_pressure', units='hPa', var_name='pres'), coord_map['pres']) data_size *= pres_dim_length if time_dim_length: t0 = datetime.datetime(1984, 8, 27) times = np.array([t0 + datetime.timedelta(days=d + time_offset) for d in range(time_dim_length)]) time_nums = convert_datetime_to_std_time(times) time_bounds = None if time_dim_length == 1: time_bounds = convert_datetime_to_std_time(np.array([times[0] - datetime.timedelta(days=0.5), times[0] + datetime.timedelta(days=0.5)])) coord_list[coord_map['time']] = (DimCoord(time_nums, standard_name='time', units='days since 1600-01-01 00:00:00', var_name='time', bounds=time_bounds), coord_map['time']) data_size *= time_dim_length if hybrid_ht_len: coord_list[coord_map['hybrid_ht']] = (DimCoord(np.arange(hybrid_ht_len, dtype='i8') + 10, "model_level_number", units="1"), coord_map['hybrid_ht']) data_size *= hybrid_ht_len if hybrid_pr_len: coord_list[coord_map['hybrid_pr']] = (DimCoord(np.arange(hybrid_pr_len, dtype='i8'), "atmosphere_hybrid_sigma_pressure_coordinate", units="1"), coord_map['hybrid_pr']) data_size *= hybrid_pr_len data = np.reshape(np.arange(data_size) + data_offset + 1., tuple(len(i[0].points) for i in coord_list)) if mask: data = np.ma.asarray(data) data.mask = mask return_cube = Cube(data, dim_coords_and_dims=coord_list, var_name='rain', standard_name='rainfall_rate', long_name="TOTAL RAINFALL RATE: LS+CONV KG/M2/S", units="kg m-2 s-1") if hybrid_ht_len: return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_ht_len, dtype='i8') + 40, long_name="level_height", units="m", var_name='hybrid_ht'), coord_map['hybrid_ht']) return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_ht_len, dtype='i8') + 50, long_name="sigma", units="1", var_name='sigma'), coord_map['hybrid_ht']) return_cube.add_aux_coord(iris.coords.AuxCoord( np.arange(lat_dim_length * lon_dim_length, dtype='i8').reshape(lat_dim_length, lon_dim_length) + 100, long_name="surface_altitude", units="m"), [coord_map['lat'], coord_map['lon']]) return_cube.add_aux_factory(HybridHeightFactory( delta=return_cube.coord("level_height"), sigma=return_cube.coord("sigma"), orography=return_cube.coord("surface_altitude"))) elif hybrid_pr_len: return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_pr_len, dtype='i8') + 40, long_name="hybrid A coefficient at layer midpoints", units="Pa", var_name='a'), coord_map['hybrid_pr']) return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_pr_len, dtype='f8') + 50, long_name="hybrid B coefficient at layer midpoints", units="1", var_name='b'), coord_map['hybrid_pr']) return_cube.add_aux_coord( iris.coords.AuxCoord(np.arange(lat_dim_length * lon_dim_length * time_dim_length, dtype='i8') .reshape(lat_dim_length, lon_dim_length, time_dim_length) * 100000 + surf_pres_offset, "surface_air_pressure", units="Pa"), [coord_map['lat'], coord_map['lon'], coord_map['time']]) if geopotential_height: return_cube.add_aux_coord(iris.coords.AuxCoord( np.arange(lat_dim_length * lon_dim_length * time_dim_length * hybrid_pr_len, dtype='i8') .reshape(lat_dim_length, lon_dim_length, time_dim_length, hybrid_pr_len) + 10, "altitude", long_name="Geopotential height at layer midpoints", units="meter"), [coord_map['lat'], coord_map['lon'], coord_map['time'], coord_map['hybrid_pr']]) return_cube.add_aux_factory(HybridPressureFactory( delta=return_cube.coord("hybrid A coefficient at layer midpoints"), sigma=return_cube.coord("hybrid B coefficient at layer midpoints"), surface_air_pressure=return_cube.coord("surface_air_pressure"))) for coord in return_cube.coords(dim_coords=True): if coord.bounds is None: coord.guess_bounds() return return_cube
def process(self, spot_data_cube: Cube, neighbour_cube: Cube, gridded_lapse_rate_cube: Cube) -> Cube: """ Extract lapse rates from the appropriate grid points and apply them to the spot extracted temperatures. The calculation is:: lapse_rate_adjusted_temperatures = temperatures + lapse_rate * vertical_displacement Args: spot_data_cube: A spot data cube of temperatures for the spot data sites, extracted from the gridded temperature field. These temperatures will have been extracted using the same neighbour_cube and neighbour_selection_method that are being used here. neighbour_cube: The neighbour_cube that contains the grid coordinates at which lapse rates should be extracted and the vertical displacement between those grid points on the model orography and the spot data sites actual altitudes. This cube is only updated when a new site is added. gridded_lapse_rate_cube: A cube of temperature lapse rates on the same grid as that from which the spot data temperatures were extracted. Returns: A copy of the input spot_data_cube with the data modified by the lapse rates to give a better representation of the site's temperatures. Raises: ValueError: If the lapse rate cube was provided but the diagnostic being processed is not air temperature. ValueError: If the lapse rate cube provided does not have the name "air_temperature_lapse_rate" ValueError: If the lapse rate cube does not contain a single valued height coordinate. Warns: warning: If a lapse rate cube was provided, but the height of the temperature does not match that of the data used. """ if is_probability(spot_data_cube): msg = ( "Input cube has a probability coordinate which cannot be lapse " "rate adjusted. Input data should be in percentile or " "deterministic space only.") raise ValueError(msg) # Check that we are dealing with temperature data. if spot_data_cube.name() not in [ "air_temperature", "feels_like_temperature" ]: msg = ( "The diagnostic being processed is not air temperature " "or feels like temperature and therefore cannot be adjusted.") raise ValueError(msg) if not gridded_lapse_rate_cube.name() == "air_temperature_lapse_rate": msg = ("A cube has been provided as a lapse rate cube but does " "not have the expected name air_temperature_lapse_rate: " "{}".format(gridded_lapse_rate_cube.name())) raise ValueError(msg) try: lapse_rate_height_coord = gridded_lapse_rate_cube.coord("height") except (CoordinateNotFoundError): msg = ("Lapse rate cube does not contain a single valued height " "coordinate. This is required to ensure it is applied to " "equivalent temperature data.") raise CoordinateNotFoundError(msg) # Check the height of the temperature data matches that used to # calculate the lapse rates. If so, adjust temperatures using the lapse # rate values. if not spot_data_cube.coord("height") == lapse_rate_height_coord: raise ValueError( "A lapse rate cube was provided, but the height of the " "temperature data does not match that of the data used " "to calculate the lapse rates. As such the temperatures " "were not adjusted with the lapse rates.") # Check the cubes are compatible. check_grid_match( [neighbour_cube, spot_data_cube, gridded_lapse_rate_cube]) # Extract the lapse rates that correspond to the spot sites. spot_lapse_rate = SpotExtraction( neighbour_selection_method=self.neighbour_selection_method)( neighbour_cube, gridded_lapse_rate_cube) # Extract vertical displacements between the model orography and sites. method_constraint = iris.Constraint( neighbour_selection_method_name=self.neighbour_selection_method) data_constraint = iris.Constraint( grid_attributes_key="vertical_displacement") vertical_displacement = neighbour_cube.extract(method_constraint & data_constraint) # Apply lapse rate adjustment to the temperature at each site. new_spot_lapse_rate = iris.util.broadcast_to_shape( spot_lapse_rate.data, spot_data_cube.shape, [-1]) new_temperatures = ( spot_data_cube.data + (new_spot_lapse_rate * vertical_displacement.data)).astype( np.float32) new_spot_cube = spot_data_cube.copy(data=new_temperatures) return new_spot_cube
def test_multidim(self): # Testing with >2D data to demonstrate correct operation over # additional non-XY dimensions (including data masking), which is # handled by the PointInCell wrapper class. # Define a simple target grid first, in plain latlon coordinates. plain_latlon_cs = GeogCS(EARTH_RADIUS) grid_x_coord = DimCoord(points=[15.0, 25.0, 35.0], bounds=[[10.0, 20.0], [20.0, 30.0], [30.0, 40.0]], standard_name='longitude', units='degrees', coord_system=plain_latlon_cs) grid_y_coord = DimCoord(points=[-30.0, -50.0], bounds=[[-20.0, -40.0], [-40.0, -60.0]], standard_name='latitude', units='degrees', coord_system=plain_latlon_cs) grid_cube = Cube(np.zeros((2, 3))) grid_cube.add_dim_coord(grid_y_coord, 0) grid_cube.add_dim_coord(grid_x_coord, 1) # Define some key points in true-lat/lon thta have known positions # First 3x2 points in the centre of each output cell. x_centres, y_centres = np.meshgrid(grid_x_coord.points, grid_y_coord.points) # An extra point also falling in cell 1, 1 x_in11, y_in11 = 26.3, -48.2 # An extra point completely outside the target grid x_out, y_out = 70.0, -40.0 # Define a rotated coord system for the source data pole_lon, pole_lat = -125.3, 53.4 src_cs = RotatedGeogCS(grid_north_pole_latitude=pole_lat, grid_north_pole_longitude=pole_lon, ellipsoid=plain_latlon_cs) # Concatenate all the testpoints in a flat array, and find the rotated # equivalents. xx = list(x_centres.flat[:]) + [x_in11, x_out] yy = list(y_centres.flat[:]) + [y_in11, y_out] xx, yy = rotate_pole(lons=np.array(xx), lats=np.array(yy), pole_lon=pole_lon, pole_lat=pole_lat) # Define handy index numbers for all these. i00, i01, i02, i10, i11, i12, i_in, i_out = range(8) # Build test data in the shape Z,YX = (3, 8) data = [[1, 2, 3, 11, 12, 13, 7, 99], [1, 2, 3, 11, 12, 13, 7, 99], [7, 6, 5, 51, 52, 53, 12, 1]] mask = [[0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0]] src_data = np.ma.array(data, mask=mask, dtype=float) # Make the source cube. src_cube = Cube(src_data) src_x = AuxCoord(xx, standard_name='grid_longitude', units='degrees', coord_system=src_cs) src_y = AuxCoord(yy, standard_name='grid_latitude', units='degrees', coord_system=src_cs) src_z = DimCoord(np.arange(3), long_name='z') src_cube.add_dim_coord(src_z, 0) src_cube.add_aux_coord(src_x, 1) src_cube.add_aux_coord(src_y, 1) # Add in some extra metadata, to ensure it gets copied over. src_cube.add_aux_coord(DimCoord([0], long_name='extra_scalar_coord')) src_cube.attributes['extra_attr'] = 12.3 # Define what the expected answers should be, shaped (3, 2, 3). expected_result = [ [[1.0, 2.0, 3.0], [11.0, 0.5 * (12 + 7), 13.0]], [[1.0, -999, 3.0], [11.0, 12.0, 13.0]], [[7.0, 6.0, 5.0], [51.0, 0.5 * (52 + 12), 53.0]], ] expected_result = np.ma.masked_less(expected_result, 0) # Perform the calculation with the regridder. regridder = Regridder(src_cube, grid_cube) # Check all is as expected. result = regridder(src_cube) self.assertEqual(result.coord('z'), src_cube.coord('z')) self.assertEqual(result.coord('extra_scalar_coord'), src_cube.coord('extra_scalar_coord')) self.assertEqual(result.coord('longitude'), grid_cube.coord('longitude')) self.assertEqual(result.coord('latitude'), grid_cube.coord('latitude')) self.assertMaskedArrayAlmostEqual(result.data, expected_result)