Source code for pycif.plugins.transforms.basic.regrid.utils.find_gridcells

from osgeo import ogr

import itertools
import pandas as pd
import copy
import numpy as np
from itertools import zip_longest
from logging import info, debug
from ......utils.check.errclass import CifError, CifValueError

# Ignore future warnings from pyproj
import warnings

new_pyproj = True
try:
    from pyproj import Proj, Transformer
except ImportError:
    from pyproj import Proj, transform
    new_pyproj = False


[docs] def find_gridcells( domain_in, domain_out, forward_direction=True, grid_to_surface=False, chunksize=2e6 ): # Reversing domains if reverse direction if not forward_direction: domain_tmp = domain_in domain_in = domain_out domain_out = domain_tmp zlonc_in = domain_in.zlonc zlatc_in = domain_in.zlatc zlon_in = domain_in.zlon zlat_in = domain_in.zlat # Check for NaNs in domain_out zlon_out = domain_out.zlon zlat_out = domain_out.zlat if np.any(~pd.notnull(zlon_out) | ~pd.notnull(zlat_out)): raise CifError( "There are NaNs in the coordinates to fit in the original grid. " "This may come from NaNs or errors in the way you generated your " "observations. Please check your yml and observations." ) is_unstructured = getattr(domain_in, "unstructured_domain", False) isregular = ( np.sum(zlonc_in[0, np.newaxis] - zlonc_in) == 0 and np.sum(zlatc_in[:, 0, np.newaxis] - zlatc_in) == 0 ) # Force ascending order if is regular discont = ( 180 if getattr(domain_in, "projection", "gps") == "gps" else np.ptp(zlonc_in) ) if isregular: ordered_lon = np.all( np.diff(np.unwrap(zlonc_in[0], discont=discont)) > 0 ) if not ordered_lon: if not np.all(np.diff(zlonc_in[0]) < 0): raise CifValueError( "Longitudes are neither in ascending or descending order. " "I can't apply `find_grid_cells`. Please check your domain" ) zlonc_in = zlonc_in[:, ::-1] ordered_lat = np.all(np.diff(zlatc_in[:, 0]) > 0) if not ordered_lat: if not np.all(np.diff(zlatc_in[:, 0]) < 0): raise CifValueError( "Latitudes are neither in ascending or descending order. " "I can't apply `find_grid_cells`. Please check your domain" ) zlatc_in = zlatc_in[::-1] # Remove duplicates to reduce the number of stations to locate ds = pd.DataFrame({ "lon": np.round(np.asarray(zlon_out).flatten(), 5), "lat": np.round(np.asarray(zlat_out).flatten(), 5), "i": np.nan, "j": np.nan }) locations = ds.groupby(["lon", "lat"]).first() # Check that there is at least one station if len(locations) == 0: # If not data, just returns empty output if domain_out.zlon.size == 0: weights = { "i": np.zeros((0, 0)), "j": np.zeros((0, 0)), "wgt": np.zeros((0, 1)), } if reversed: weights = { "i": np.zeros((zlon_in.size, 0)), "j": np.zeros((zlon_in.size, 0)), "wgt": np.zeros((zlon_in.size, 0)), } return weights # raise Exception("Trying to find gridcells from an empty datastore. " # "Please check your input observations in: $WORKDIR/datavect") raise CifError("There is no valid location (lon/lat) in your observation. " "This error can be caused by, e.g., all lon/lat being NaNs. " "Please check the observation inputs in: $WORKDIR/datavect") # Makes simplified operations if regular lon = locations.index.get_level_values("lon").values lat = locations.index.get_level_values("lat").values if isregular or is_unstructured: listi = [] listj = [] # Loops over chunks to make the code faster for lon_chunk, lat_chunk in zip( zip_longest(*[iter(lon)] * int(chunksize), fillvalue=-999), zip_longest(*[iter(lat)] * int(chunksize), fillvalue=-999), ): lon_chunk = np.array(lon_chunk)[np.array(lon_chunk) != -999] lat_chunk = np.array(lat_chunk)[np.array(lat_chunk) != -999] if isregular: i, j = find_gridcell( lon_chunk, lat_chunk, zlonc_in, zlatc_in, isregular=isregular, discont=discont, ) if not ordered_lon: j = domain_in.nlon - 1 - j if not ordered_lat: i = domain_in.nlat - 1 - i else: # For unstructured domains, take closest point # TODO: generalize dist = (zlon_in.T - lon_chunk) ** 2 \ + (zlat_in.T - lat_chunk) ** 2 i = np.argmin(dist, axis=0) j = np.zeros(len(lon_chunk)) listi.extend(list(i)) listj.extend(list(j)) else: # Loop over (lon, lat) tuples k = 0 nlocs = np.floor(locations.size / 10) + 1 listi = [] listj = [] for lon_tmp, lat_tmp in zip(lon, lat): i, j = find_gridcell( lon_tmp, lat_tmp, zlonc_in, zlatc_in, isregular=isregular, is_unstructured=is_unstructured) listi.append(i) listj.append(j) k += 1 if k % nlocs == 0: info(f"{k * 10.0 / nlocs}%") # Putting i, j to locations datastore locations["i"] = listi locations["j"] = listj # Distribute values back to the full datastore with duplicates ds.loc[:, ["i", "j"]] = \ locations.iloc[ds.groupby( ["lon", "lat"]).ngroup().astype(int), :].values # Produce weights for later use for reprojection weights = { "i": ds["i"].values[:, np.newaxis], "j": ds["j"].values[:, np.newaxis], "wgt": ds["j"].values[:, np.newaxis] * 0 + 1} # Filter out observations outside the domain inside = ~np.isnan(ds["i"]) & ~np.isnan(ds["j"]) if forward_direction: weights = { "i": weights["i"][inside], "j": weights["j"][inside], "wgt": weights["wgt"][inside], "filtered": np.where(inside)[0], "non_filtered": np.where(~inside)[0] } return weights # # Reversing weights if reverse direction # nlat, nlon = zlon_in.shape # ind_out = np.ravel_multi_index( # np.concatenate([weights["i"][inside], # weights["j"][inside]], axis=1).T.astype(int), # (nlat, nlon), order="F") # filtered = np.unique(ind_out) # Reversing weights if reverse direction nlat, nlon = zlon_in.shape groups = ds.groupby(['i', 'j']) filtered = np.ravel_multi_index( groups.count().reset_index().loc[:, ["i", "j"]].values.astype(int).T, (nlat, nlon), order="F") # Initialize areas if needed if grid_to_surface: domain = domain_in if not hasattr(domain, "areas"): domain.calc_areas() # Fill output groups with input coordinates out_weights = { "j": [list(groups.groups[k]) for k in groups.groups if not np.isnan(k[0])], "i": [len(groups.groups[k]) * [0] for k in groups.groups if not np.isnan(k[0])], "wgt": [ len(groups.groups[k]) * [1] if not grid_to_surface else len(groups.groups[k]) * [1 / domain.areas[tuple(np.array(k).astype(int))]] for k in groups.groups if not np.isnan(k[0])], "filtered": filtered, # "non_filtered": filtered } out_weights = { "i": np.array(list( itertools.zip_longest(*out_weights["i"], fillvalue=0))).T, "j": np.array(list( itertools.zip_longest(*out_weights["j"], fillvalue=0))).T, "wgt": np.array(list( itertools.zip_longest(*out_weights["wgt"], fillvalue=np.nan))).T, "filtered": filtered, # "non_filtered": filtered } return out_weights
[docs] def find_gridcell( lon, lat, zlonc, zlatc, orig_proj="epsg:4326" if new_pyproj else Proj(init="epsg:4326"), isregular=False, is_unstructured=False, discont=180, ): """Finds the grid cell corresponding to a coordinate. Args: lon (np.array): longitude of the point to find lat (np.array): latitude of the point to find zlonc (np.array): longitudes of the corners of the grid zlatc (np.array): latitudes of the corners of the grid orig_proj (projection): the projection for reporting coordinates. Default is WSG84 isregular (Boolean): if True, simplified operations are computed. Default is False Returns: i, j the grid cell ID Notes: - For very regular grids, this script could be made more effileient and shorter, but the objective is to be able to deal with any domain - zlonc and zlatc must be in ascending order """ # If regular domain make simplified operations if isregular and not is_unstructured: try: # Catch and ignore divide by zero with np.errstate(divide='ignore', invalid='ignore'): xlon = 1.0 / np.unwrap( np.radians(zlonc[0, np.newaxis] - lon[:, np.newaxis]), discont=np.radians(discont) ) xlat = 1.0 / (zlatc[:, 0] - lat[:, np.newaxis]) xlon[xlon == np.inf] = -np.inf xlat[xlat == np.inf] = -np.inf i = xlat.argmin(axis=1) j = xlon.argmin(axis=1) nlat, nlon = zlonc[:-1, :-1].shape # Put NaNs for points outside the domain maski = (lat < zlatc.min()) | (lat > zlatc.max()) maskj = (lon < np.degrees(np.unwrap(np.radians(zlonc), discont=np.radians(discont)).min())) \ | (lon > np.degrees(np.unwrap(np.radians(zlonc), discont=np.radians(discont)).max())) i = np.where(maski | maskj, np.nan, i) j = np.where(maski | maskj, np.nan, j) # Points in last column or row should be shifted # as corners were used maski = (i >= nlat) maskj = (j >= nlon) i = np.where(maski, nlat - 1, i) j = np.where(maskj, nlon - 1, j) return i, j # If only one (lon, lat) tuple was provided except TypeError as e: info(e) xlon = 1.0 / np.unwrap(np.radians(zlonc[0] - lon), discont=np.radians(discont)) xlat = 1.0 / (zlatc[:, 0] - lat) return xlat.argmin(), xlon.argmin() # Grid shape nlat, nlon = zlonc.shape # Define measurement point geometry in local coordinates point = ogr.Geometry(ogr.wkbPoint) point.AddPoint(0, 0) # Define local projection if new_pyproj: target_proj = \ f"+proj=laea +lat_0={lat} +lon_0={lon} +x_0=0 +y_0=0 "\ "+ellps=WGS84 +units=m +no_defs " t = Transformer.from_crs(orig_proj, target_proj, always_xy=True) zxc, zyc = t.transform((zlonc + 180) % 360 - 180, zlatc) else: target_proj = Proj( f"+proj=laea +lat_0={lat} +lon_0={lon} +x_0=0 +y_0=0 " "+ellps=WGS84 +units=m +no_defs " ) zxc, zyc = transform( orig_proj, target_proj, (zlonc + 180) % 360 - 180, zlatc ) dist = zxc ** 2 + zyc ** 2 imin, jmin = np.unravel_index(dist.argmin(), dist.shape) imin = imin % nlat jmin = jmin % nlon for i in range(imin - 3, imin + 2): for j in range(jmin - 3, jmin + 2): # Looping if the minimum is close to the domain side i = max(min(i, nlat - 2), 0) j = max(min(j, nlon - 2), 0) # Create ring ring = ogr.Geometry(ogr.wkbLinearRing) _ = ring.AddPoint(zxc[i, j], zyc[i, j]) _ = ring.AddPoint(zxc[i + 1, j], zyc[i + 1, j]) _ = ring.AddPoint(zxc[i + 1, j + 1], zyc[i + 1, j + 1]) _ = ring.AddPoint(zxc[i, j + 1], zyc[i, j + 1]) _ = ring.AddPoint(zxc[i, j], zyc[i, j]) # Create polygon poly = ogr.Geometry(ogr.wkbPolygon) _ = poly.AddGeometry(ring) # Buffering the polygon to include points on the edge buffer = 1.0 poly = poly.Buffer(buffer) if point.Within(poly): return i, j # If no grid cell was fund, raise exception # # raise IndexError( # "No index was found for measurement at {}, {}".format(lon, lat)) # TODO: Decide whether we raise exception # when the observation is outside the domain return np.nan, np.nan