def test_exception_for_degrees_input(self): """Test that an exception is raised if the input cube has spatial coordinates with units that cannot be converted to the default unit of the projection.""" self.cube.coord(axis="x").units = "degrees" msg = "Cube passed to transform_grid_to_lat_lon does not have an x coordinate" with self.assertRaisesRegex(ValueError, msg): transform_grid_to_lat_lon(self.cube)
def calc_lats_lons(cube): """ Calculate the lats and lons of each point from a non-latlon cube, or output a 2d array of lats and lons, if the input cube has latitude and longitude coordinates. Args: cube (iris.cube.Cube): cube containing x and y axis Returns: (tuple) : tuple containing: **lats** (np.array): 2d Array of latitudes for each point. **lons** (np.array): 2d Array of longitudes for each point. """ trg_crs = lat_lon_determine(cube) if trg_crs is not None: xycube = next( cube.slices([cube.coord(axis='y'), cube.coord(axis='x')])) lats, lons = transform_grid_to_lat_lon(xycube) else: lats_row = cube.coord('latitude').points lons_col = cube.coord('longitude').points lats = np.repeat(lats_row[:, np.newaxis], len(lons_col), axis=1) lons = np.repeat(lons_col[np.newaxis, :], len(lats_row), axis=0) return lats, lons
def calc_lats_lons(cube: Cube) -> Tuple[ndarray, ndarray]: """ Calculate the lats and lons of each point from a non-latlon cube, or output a 2d array of lats and lons, if the input cube has latitude and longitude coordinates. Args: cube: cube containing x and y axis Returns: - 2d Array of latitudes for each point. - 2d Array of longitudes for each point. """ trg_crs = lat_lon_determine(cube) if trg_crs is not None: xycube = next( cube.slices([cube.coord(axis="y"), cube.coord(axis="x")])) lats, lons = transform_grid_to_lat_lon(xycube) else: lats_row = cube.coord("latitude").points lons_col = cube.coord("longitude").points lats = np.repeat(lats_row[:, np.newaxis], len(lons_col), axis=1) lons = np.repeat(lons_col[np.newaxis, :], len(lats_row), axis=0) return lats, lons
def _get_coordinate_pairs(cube): """ Create an array containing all the pairs of coordinates that describe y-x points in the grid. Args: cube (iris.cube.Cube): The cube from which the y-x grid is being taken. Returns: numpy.array: A numpy array containing all the pairs of coordinates that describe the y-x points in the grid. This array is 2-dimensional, with shape (2, (len(y-points) * len(x-points))). """ if lat_lon_determine(cube) is not None: yy, xx = transform_grid_to_lat_lon(cube) else: latitudes = cube.coord("latitude").points longitudes = cube.coord("longitude").points.copy() # timezone finder works using -180 to 180 longitudes. if (longitudes > 180).any(): longitudes[longitudes > 180] -= 180 if ((longitudes > 180) | (longitudes < -180)).any(): msg = ( "TimezoneFinder requires longitudes between -180 " "and 180 degrees. Longitude found outside that range." ) raise ValueError(msg) yy, xx = np.meshgrid(latitudes, longitudes, indexing="ij") return np.stack([yy.flatten(), xx.flatten()], axis=1)
def test_transform_grid(self): """Test transformation of grid for equal area grid with spatial coordinates defined in metres.""" result_lats, result_lons = transform_grid_to_lat_lon(self.cube) self.assertIsInstance(result_lats, np.ndarray) self.assertIsInstance(result_lons, np.ndarray) assert_almost_equal(result_lons, self.expected_lons) assert_almost_equal(result_lats, self.expected_lats)
def test_non_metre_input(self): """Test transformation of grid for equal area grid with spatial coordinates defined in kilometres.""" self.cube.coord(axis="x").convert_units("km") self.cube.coord(axis="y").convert_units("km") result_lats, result_lons = transform_grid_to_lat_lon(self.cube) self.assertIsInstance(result_lats, np.ndarray) self.assertIsInstance(result_lons, np.ndarray) assert_almost_equal(result_lons, self.expected_lons) assert_almost_equal(result_lats, self.expected_lats)
def process(self, cube): """ Calculate the daynight mask for the provided cube. Note that only the hours and minutes of the dtval variable are used. To ensure consistent behaviour with changes of second or subsecond precision, the second component is added to the time object. This means that when the hours and minutes are used, we have correctly rounded to the nearest minute, e.g.:: dt(2017, 1, 1, 11, 59, 59) -- +59 --> dt(2017, 1, 1, 12, 0, 58) dt(2017, 1, 1, 12, 0, 1) -- +1 --> dt(2017, 1, 1, 12, 0, 2) dt(2017, 1, 1, 12, 0, 30) -- +30 --> dt(2017, 1, 1, 12, 1, 0) Args: cube (iris.cube.Cube): input cube Returns: iris.cube.Cube: daynight mask cube, daytime set to self.day nighttime set to self.night. The resulting cube will be the same shape as the time, y, and x coordinate, other coordinates will be ignored although they might appear as attributes on the cube as it is extracted from the first slice. """ daynight_mask = self._create_daynight_mask(cube) modified_masks = iris.cube.CubeList() for mask_cube in daynight_mask.slices_over("time"): dtval = mask_cube.coord("time").cell(0).point day_of_year = (dtval - dt.datetime(dtval.year, 1, 1)).days dtval = dtval + dt.timedelta(seconds=dtval.second) utc_hour = (dtval.hour * 60.0 + dtval.minute) / 60.0 trg_crs = lat_lon_determine(mask_cube) # Grids that are not Lat Lon if trg_crs is not None: lats, lons = transform_grid_to_lat_lon(mask_cube) solar_el = calc_solar_elevation(lats, lons, day_of_year, utc_hour) mask_cube.data[np.where(solar_el > 0.0)] = self.day else: mask_cube = self._daynight_lat_lon_cube( mask_cube, day_of_year, utc_hour ) modified_masks.append(mask_cube) return modified_masks.merge_cube()
def process(self, target_grid: Cube, time: datetime, new_title: str = None) -> Cube: """Calculate the local solar time over the specified grid. Args: target_grid: A cube containing the desired spatial grid. time: The valid time at which to evaluate the local solar time. new_title: New title for the output cube attributes. If None, this attribute is left out since it has no prescribed standard. Returns: A cube containing local solar time, on the same spatial grid as target_grid. """ if lat_lon_determine(target_grid) is not None: _, lons = transform_grid_to_lat_lon(target_grid) else: _, lons = get_grid_y_x_values(target_grid) day_of_year = get_day_of_year(time) utc_hour = get_hour_of_day(time) solar_time_data = calc_solar_time(lons, day_of_year, utc_hour, normalise=True) solar_time_cube = self._create_solar_time_cube(solar_time_data, target_grid, time, new_title) return solar_time_cube
def process(self, cube): """ Calculate the daynight mask for the provided cube Args: cube (iris.cube.Cube): input cube Returns: daynight_mask (iris.cube.Cube): daynight mask cube, daytime set to self.day nighttime set to self.night. The resulting cube will be the same shape as the time, y, and x coordinate, other coordinates will be ignored although they might appear as attributes on the cube as it is extracted from the first slice. """ daynight_mask = self._create_daynight_mask(cube) dtvalues = iris_time_to_datetime(daynight_mask.coord('time')) for i, dtval in enumerate(dtvalues): mask_cube = daynight_mask[i] day_of_year = (dtval - dt.datetime(dtval.year, 1, 1)).days utc_hour = (dtval.hour * 60.0 + dtval.minute) / 60.0 trg_crs = lat_lon_determine(mask_cube) # Grids that are not Lat Lon if trg_crs is not None: lats, lons = transform_grid_to_lat_lon(mask_cube) solar_el = calc_solar_elevation(lats, lons, day_of_year, utc_hour) mask_cube.data[np.where(solar_el > 0.0)] = self.day else: mask_cube = self._daynight_lat_lon_cube( mask_cube, day_of_year, utc_hour) daynight_mask.data[i, ::] = mask_cube.data return daynight_mask
def process(self, cube_in: Cube, cube_in_mask: Cube, cube_out_mask: Cube) -> Cube: """ Regridding considering land_sea mask. please note cube_in must use lats/lons rectlinear system(GeogCS). cube_in_mask and cube_in could be different resolution. cube_out could be either in lats/lons rectlinear system or LambertAzimuthalEqualArea system. Grid points in cube_out domain but not in cube_in domain will be masked. Args: cube_in: Cube of data to be regridded. cube_in_mask: Cube of land_binary_mask data ((land:1, sea:0). used to determine where the input model data is representing land and sea points. cube_out_mask: Cube of land_binary_mask data on target grid (land:1, sea:0). Returns: Regridded result cube. """ # if cube_in's coordinate descending, make it assending. # if mask considered, reverse mask cube's coordinate if descending cube_in = ensure_ascending_coord(cube_in) if WITH_MASK in self.regrid_mode: cube_in_mask = ensure_ascending_coord(cube_in_mask) # check if input source grid is on even-spacing, ascending lat/lon system # return grid spacing for latitude and logitude lat_spacing, lon_spacing = calculate_input_grid_spacing(cube_in) # Gather output latitude/longitudes from output template cube if ( cube_out_mask.coord(axis="x").standard_name == "projection_x_coordinate" and cube_out_mask.coord(axis="y").standard_name == "projection_y_coordinate" ): out_latlons = np.dstack(transform_grid_to_lat_lon(cube_out_mask)).reshape( (-1, 2) ) else: out_latlons = latlon_from_cube(cube_out_mask) # Subset the input cube so that extra spatial area beyond the output is removed # This is a performance optimisation to reduce the size of the dataset being processed total_out_point_num = out_latlons.shape[0] lat_max, lon_max = out_latlons.max(axis=0) lat_min, lon_min = out_latlons.min(axis=0) if WITH_MASK in self.regrid_mode: cube_in, cube_in_mask = slice_mask_cube_by_domain( cube_in, cube_in_mask, (lat_max, lon_max, lat_min, lon_min) ) else: # not WITH_MASK cube_in = slice_cube_by_domain( cube_in, (lat_max, lon_max, lat_min, lon_min) ) # group cube_out's grid points into outside or inside cube_in's domain ( outside_input_domain_index, inside_input_domain_index, ) = group_target_points_with_source_domain(cube_in, out_latlons) # exclude out-of-input-domain target point here if len(outside_input_domain_index) > 0: out_latlons = out_latlons[inside_input_domain_index] # Gather input latitude/longitudes from input cube in_latlons = latlon_from_cube(cube_in) # Number of grid points in X dimension is used to work out length of flattened array # stripes for finding surrounding points for bilinear interpolation in_lons_size = cube_in.coord(axis="x").shape[0] # longitude # Reshape input data so that spatial dimensions can be handled as one in_values, lats_index, lons_index = flatten_spatial_dimensions(cube_in) # Locate nearby input points for output points indexes = basic_indexes( out_latlons, in_latlons, in_lons_size, lat_spacing, lon_spacing ) if WITH_MASK in self.regrid_mode: in_classified = classify_input_surface_type(cube_in_mask, in_latlons) out_classified = classify_output_surface_type(cube_out_mask) if len(outside_input_domain_index) > 0: out_classified = out_classified[inside_input_domain_index] # Identify mismatched surface types from input and output classifications surface_type_mask = similar_surface_classify( in_classified, out_classified, indexes ) # Initialise distances and weights to zero. Weights are only used for the bilinear case distances = np.zeros((out_latlons.shape[0], NUM_NEIGHBOURS), dtype=np.float32) weights = np.zeros((out_latlons.shape[0], NUM_NEIGHBOURS), dtype=np.float32) # handle nearest option if NEAREST in self.regrid_mode: for i in range(NUM_NEIGHBOURS): distances[:, i] = np.square( in_latlons[indexes[:, i], 0] - out_latlons[:, 0] ) + np.square(in_latlons[indexes[:, i], 1] - out_latlons[:, 1]) # for nearest-with-mask-2,adjust indexes and distance for mismatched # surface type location if WITH_MASK in self.regrid_mode: distances, indexes = nearest_with_mask_regrid( distances, indexes, surface_type_mask, in_latlons, out_latlons, in_classified, out_classified, self.vicinity, ) # apply nearest distance rule output_flat = nearest_regrid(distances, indexes, in_values) elif BILINEAR in self.regrid_mode: # Assume all four nearby points are same surface type and calculate default weights # These will be updated for mask/mismatched surface type further below index_range = np.arange(weights.shape[0]) weights[index_range] = basic_weights( index_range, indexes, out_latlons, in_latlons, lat_spacing, lon_spacing, ) if WITH_MASK in self.regrid_mode: # For bilinear-with-mask-2, adjust weights and indexes for mismatched # surface type locations weights, indexes = adjust_for_surface_mismatch( in_latlons, out_latlons, in_classified, out_classified, weights, indexes, surface_type_mask, in_lons_size, self.vicinity, lat_spacing, lon_spacing, ) # apply bilinear rule output_flat = apply_weights(indexes, in_values, weights) # check if we need mask cube_out grid points which are out of cube_in range if len(outside_input_domain_index) > 0: output_flat = mask_target_points_outside_source_domain( total_out_point_num, outside_input_domain_index, inside_input_domain_index, output_flat, ) # Un-flatten spatial dimensions and put into output cube output_array = unflatten_spatial_dimensions( output_flat, cube_out_mask, in_values, lats_index, lons_index ) output_cube = create_regrid_cube(output_array, cube_in, cube_out_mask) return output_cube
def _calc_clearsky_solar_radiation_data( self, target_grid: Cube, irradiance_times: List[datetime], surface_altitude: ndarray, linke_turbidity: ndarray, temporal_spacing: int, ) -> ndarray: """Evaluate the gridded clearsky solar radiation data over the specified period, calculated on the same spatial grid points as target_grid. Args: target_grid: Cube containing the target spatial grid on which to evaluate irradiance. irradiance_times: Datetimes at which to evaluate the irradiance data. surface_altitude: Surface altitude data, specified in metres. linke_turbidity: Linke turbidity data. temporal_spacing: The time stepping, specified in mins, used in the integration of solar irradiance to produce the accumulated solar radiation. Returns: Gridded irradiance values evaluated over the specified times. """ if lat_lon_determine(target_grid) is not None: lats, lons = transform_grid_to_lat_lon(target_grid) else: lats, lons = get_grid_y_x_values(target_grid) irradiance_data = np.zeros( shape=( len(irradiance_times), target_grid.coord(axis="Y").shape[0], target_grid.coord(axis="X").shape[0], ), dtype=np.float32, ) for time_index, time_step in enumerate(irradiance_times): day_of_year = get_day_of_year(time_step) utc_hour = get_hour_of_day(time_step) zenith_angle = 90.0 - calc_solar_elevation(lats, lons, day_of_year, utc_hour) irradiance_data[time_index, :, :] = self._calc_clearsky_ineichen( zenith_angle, day_of_year, surface_altitude=surface_altitude, linke_turbidity=linke_turbidity, ) # integrate the irradiance data along the time dimension to get the # accumulated solar irradiance. solar_radiation_data = np.trapz(irradiance_data, dx=SECONDS_IN_MINUTE * temporal_spacing, axis=0) return solar_radiation_data