from osgeo import gdal, ogr, osr
import pandas as pd
from logging import info, debug
import numpy as np
import itertools
from ......utils.check.errclass import CifError, CifValueError
new_pyproj = True
try:
from pyproj import Proj, Transformer
except ImportError:
from pyproj import Proj, transform
new_pyproj = False
[docs]
def reproject_emissions(
emis_orig,
zlonc_orig,
zlatc_orig,
zlonc_target,
zlatc_target,
resol=10,
option="mean",
wk_proj="wgs84",
orig_regular=True,
return_weight=False,
orig_unstructured=False,
orig_lon_cyclic=False,
target_unstructured=False,
rounding_domain=8
):
# Calculate the number of bands
if len(emis_orig.shape) == 2:
debug("Reprojecting 2D data")
nbands = 1
emis_orig = emis_orig[np.newaxis, ...]
elif len(emis_orig.shape) == 3:
debug("Reprojecting 3D data")
nbands = emis_orig.shape[0]
debug(f"Number of bands: {nbands}")
# Shifting longitudes beyond 180 degree east
# Unwrap and makes sure that target is in same modulo than orig
if wk_proj == "wgs84":
zlonc_orig = (zlonc_orig + 180) % 360 - 180
zlonc_target = (zlonc_target + 180) % 360 - 180
# Unwrap
if not orig_unstructured:
zlonc_orig = np.degrees(np.unwrap(np.radians(zlonc_orig)))
else:
zlonc_orig = np.degrees(np.unwrap(np.radians(zlonc_orig), axis=0))
if not target_unstructured:
zlonc_target = np.degrees(np.unwrap(np.radians(zlonc_target)))
else:
zlonc_target = np.degrees(
np.unwrap(np.radians(zlonc_target), axis=0))
# Put to the same range of degrees
zlonc_target += 360 * (
np.median(np.floor((zlonc_orig - 180) / 360))
- np.median(np.floor((zlonc_target - 180) / 360))
)
# Projection definition
wgs84_pyproj = "epsg:4326"
if wk_proj == "wgs84":
ref_proj = osr.SpatialReference()
ref_proj.ImportFromProj4(
"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
)
ref_pyproj = "epsg:4326"
else:
ref_proj = osr.SpatialReference()
ref_proj.ImportFromProj4(wk_proj)
ref_pyproj = wk_proj
# Convert lon/lat domain coordinates to reference coordinate system
if new_pyproj:
t = Transformer.from_crs(wgs84_pyproj, ref_pyproj, always_xy=True)
xc_orig, yc_orig = t.transform(zlonc_orig, zlatc_orig)
xc_target, yc_target = t.transform(zlonc_target, zlatc_target)
else:
xc_orig, yc_orig = transform(
Proj(init=wgs84_pyproj), Proj(init=ref_pyproj),
zlonc_orig, zlatc_orig)
xc_target, yc_target = transform(
Proj(init=wgs84_pyproj), Proj(init=ref_pyproj),
zlonc_target, zlatc_target)
# Domains dimensions
nmerid_orig, nzonal_orig = zlonc_orig[:-1, :-1].shape
nmerid_target, nzonal_target = zlonc_target[:-1, :-1].shape
# Check that the dimensions of emissions are compatible with coordinates
if not orig_unstructured:
nmerid_emis, nzonal_emis = emis_orig.shape[1:]
if nmerid_emis != nmerid_orig or nzonal_emis != nzonal_orig:
raise CifError(
"Warning: Emission data shape is "
"not compatible of that of coordinates. \n"
"Consider transposing, or shake how the data is read. \n"
f"Emission shape: {emis_orig.shape}\n"
f"Coordinate shape: {zlonc_orig[:-1, :-1].shape}")
else:
nmerid_emis, nzonal_emis = emis_orig.shape[1:]
if nmerid_emis != 1 and nmerid_emis != zlonc_orig[0].size:
raise CifError(
"Warning: Emission data shape is "
"not compatible of that of coordinates. "
"Consider transposing, or shake how the data is read. . \n"
f"Emission shape: {emis_orig.shape}\n"
f"Coordinate shape: {zlonc_orig[0].size}")
# TODO: make it work with unstructured domains
raise CifError(
"Unstructured domains in origin data not yet implemented in reproject")
# Check that the original domain is really regular
if orig_regular:
if len(np.unique(np.diff(xc_orig, axis=0).round(3))) > 1:
orig_regular = False
if len(np.unique(np.diff(xc_orig, axis=1).round(3))) > 1:
orig_regular = False
if len(np.unique(np.diff(yc_orig, axis=0).round(3))) > 1:
orig_regular = False
if len(np.unique(np.diff(yc_orig, axis=1).round(3))) > 1:
orig_regular = False
# Rasterize to regular grid the origin emissions if necessary
xpxl_ref = None
if not orig_regular:
debug("Irregular domain: Reprojecting original domain "
"to a finer regular domain")
# Rasterize to regular grid the origin emissions if necessary
debug("Vectorizing the original domain first")
(
vector_grid_driver,
vector_grid_ds,
vector_grid_layer,
) = domain2polygons(xc_orig, yc_orig, wk_proj, data=emis_orig)
# Rasterize irregular origin data
x_min, x_max, y_min, y_max = vector_grid_layer.GetExtent()
if not target_unstructured:
pixel_size = (
max(
np.median(np.ediff1d(xc_target)),
np.median(np.ediff1d(xc_target.T)),
)
/ resol
)
else:
pixel_size = (
(xc_target.max() - xc_target.min())
/ np.sqrt(xc_target.shape[1])
) / resol
x_res = int((x_max - x_min) / pixel_size)
y_res = int((y_max - y_min) / pixel_size)
geotransform = (x_min, pixel_size, 0, y_max, 0, -pixel_size)
output_raster = gdal.GetDriverByName("MEM").Create(
"", x_res, y_res, nbands + 2, gdal.GDT_Float32
)
output_raster.SetGeoTransform(geotransform)
output_raster.SetProjection(ref_proj.ExportToWkt())
debug("Rasterize vector domain to finer scale:")
debug("Target raster charecteristics:")
debug(f"xmin/xmax/ymin/ymax: {x_min}/{x_max}/{y_min}/{y_max}")
debug(f"resolution x/y: {pixel_size}")
debug(f"Raster x/y size: {x_res}/{y_res}")
for iband in range(nbands):
gdal.RasterizeLayer(
output_raster,
[iband + 1],
vector_grid_layer,
None,
None,
[0],
options=[f"ATTRIBUTE=field_{iband}"],
)
# Rasterize i/j
gdal.RasterizeLayer(
output_raster,
[nbands + 1],
vector_grid_layer,
None,
None,
[0],
options=["ATTRIBUTE=index_i"],
)
gdal.RasterizeLayer(
output_raster,
[nbands + 2],
vector_grid_layer,
None,
None,
[0],
options=["ATTRIBUTE=index_j"],
)
else:
dx = np.unique(np.round(np.diff(xc_orig, axis=1), 6))
if wk_proj == "wgs84":
# Unwrap geometry
xc_bis = np.degrees(np.unwrap(np.radians(xc_orig)))
dx = np.unique(np.round(np.diff(xc_bis, axis=1), rounding_domain))
xc_bis -= 360 * np.round(xc_bis.mean() / 360)
xc_orig = xc_bis[:]
if np.size(dx) > 1:
raise CifValueError(
f"The domain is not regular.\n"
f"The list of possible deltas is: {dx}.\n"
"If all delta are very similar apart from rounding difference, "
"it is possible to force a constant delta by using the parameter "
f"'rounding_domain' (={rounding_domain} presently) in the yml. "
"Please check the documentation for "
"the transform plugin 'regrid/std'"
)
raise CifError(
f"Warning! The domain is not regular. I found the following possible dx values: {dx}")
dx = dx[0]
# Shift original array spanning beyond 180 degrees east
if wk_proj == "wgs84":
if xc_orig[0, 0] + (nzonal_orig - 1) * dx > 180:
xpxl_ref = int(
np.round(
(180 - xc_orig[0, 0]) / dx
)
)
xc_orig = np.concatenate(
(xc_orig[:, xpxl_ref:], xc_orig[:, :xpxl_ref]), axis=1
)
yc_orig = np.concatenate(
(yc_orig[:, xpxl_ref:], yc_orig[:, :xpxl_ref]), axis=1
)
emis_orig = np.concatenate(
(emis_orig[..., xpxl_ref:], emis_orig[..., :xpxl_ref]),
axis=2
)
# Deal with poles
geotransform = (
xc_orig[0, 0],
dx,
0,
yc_orig[0, 0],
0,
yc_orig[1, 1] - yc_orig[0, 0],
)
# Create Raster with all emissions bands
output_raster = gdal.GetDriverByName("MEM").Create(
"", nzonal_orig, nmerid_orig, nbands, gdal.GDT_Float32
)
output_raster.SetGeoTransform(geotransform)
output_raster.SetProjection(ref_proj.ExportToWkt())
# Loop over month to fill the raster
for iband in range(nbands):
# Writes my array to the raster
output_raster.GetRasterBand(iband + 1).WriteArray(emis_orig[iband])
# Create polygons from target domain
vector_grid_driver, vector_grid_ds, vector_grid_layer = domain2polygons(
zlonc_target, zlatc_target, wk_proj, is_regular=not target_unstructured
)
# Compute projection
emis_target = loop_zonal_stats(
vector_grid_layer,
output_raster,
resol=resol,
option=option,
return_weight=return_weight,
orig_lon_cyclic=orig_lon_cyclic
)
# Return only weights if required
if return_weight:
if xpxl_ref is not None:
for k, wgt in enumerate(emis_target):
if wgt[1] == []:
continue
mask = wgt[1] >= xpxl_ref
emis_target[k][1][mask] -= xpxl_ref
emis_target[k][1][~mask] += xpxl_ref
# Reshape weights as dictionary
weights = {
"i": [e[0] for e in emis_target],
"j": [e[1] for e in emis_target],
"wgt": [e[2] for e in emis_target]
}
# Deal with non regular original domain
if not orig_regular:
debug("Reshaping refined original domain to real one when irregular")
bandi = output_raster.GetRasterBand(nbands + 1).ReadAsArray() - 100
bandj = output_raster.GetRasterBand(nbands + 2).ReadAsArray() - 100
out_weights = {"i": [], "j": [], "wgt": []}
ind_target = 0
nmerid_loop = nmerid_target if not target_unstructured else 1
for ii, jj, ww in zip(weights["i"], weights["j"], weights["wgt"]):
j = ind_target // nmerid_loop
ind_target += 1
if j % int(nzonal_target / 10) == 0:
debug(f"Grid cell: {j} out of {nzonal_target}")
tmp_i = bandi[ii, jj][:, np.newaxis]
tmp_j = bandj[ii, jj][:, np.newaxis]
unique, indices_target = np.unique(
np.concatenate([tmp_i, tmp_j], axis=1),
axis=0, return_inverse=True)
wgt_target = np.zeros(len(unique))
np.add.at(wgt_target, indices_target, ww)
out_weights["i"].append(unique[:, 0].flatten().astype(int))
out_weights["j"].append(unique[:, 1].flatten().astype(int))
out_weights["wgt"].append(wgt_target)
weights = out_weights
# Converts weights and i/j to a matrix padded with nans for varying
# lengths of i/j/weights
debug("Reshaping outputs into a full shape matrix before return weights")
weights = {
"i": np.array(list(
itertools.zip_longest(*weights["i"], fillvalue=0))).T,
"j": np.array(list(
itertools.zip_longest(*weights["j"], fillvalue=0))).T,
"wgt": np.array(list(
itertools.zip_longest(*weights["wgt"], fillvalue=np.nan))).T
}
return weights
emis_target = np.reshape(emis_target, (nmerid_target, nzonal_target, -1))
emis_target = np.transpose(emis_target, axes=(2, 1, 0))
# Removes NaNs
emis_target[np.isnan(emis_target)] = 0.0
# if only one band, returns only the 2D dataset,
# otherwise, returns everything
if nbands == 1:
return emis_target[0]
else:
return emis_target
[docs]
def domain2polygons(zlonc_target, zlatc_target,
wk_proj="wgs84", data=None, is_regular=True):
debug(
f"Vectorizing the domain of shape {zlonc_target.shape} and is_regular={is_regular}")
debug("This can take a while...")
if is_regular:
nmerid_target, nzonal_target = zlonc_target[:-1, :-1].shape
else:
nmerid_target = 1
nzonal_target = zlonc_target.shape[1]
if data is not None:
nbands = data.shape[0]
# Reference projections
wgs84_pyproj = "epsg:4326"
if wk_proj == "wgs84":
ref_proj = osr.SpatialReference()
ref_proj.ImportFromProj4(
"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
)
ref_pyproj = "epsg:4326"
else:
ref_proj = osr.SpatialReference()
ref_proj.ImportFromProj4(wk_proj)
ref_pyproj = wk_proj
# Convert lon/lat domain coordinates to reference coordinate system
if new_pyproj:
t = Transformer.from_crs(wgs84_pyproj, ref_pyproj, always_xy=True)
xc, yc = t.transform(zlonc_target, zlatc_target)
else:
xc, yc = transform(
Proj(init=wgs84_pyproj), Proj(init=ref_pyproj),
zlonc_target, zlatc_target)
# define the layer
vector_grid_driver = ogr.GetDriverByName("MEMORY")
vector_grid_ds = vector_grid_driver.CreateDataSource("memData")
vector_grid_layer = vector_grid_ds.CreateLayer(
"grid", ref_proj, geom_type=ogr.wkbPolygon
)
idField = ogr.FieldDefn("index_i", ogr.OFTInteger)
vector_grid_layer.CreateField(idField)
idField = ogr.FieldDefn("index_j", ogr.OFTInteger)
vector_grid_layer.CreateField(idField)
if data is not None:
for iband in range(nbands):
idField = ogr.FieldDefn(f"field_{iband}", ogr.OFTReal)
vector_grid_layer.CreateField(idField)
featureDefn = vector_grid_layer.GetLayerDefn()
for j in range(nzonal_target):
if j % int(nzonal_target / 10) == 0:
debug(f"Vectorizing: {j} out of {nzonal_target}")
for i in range(nmerid_target):
if is_regular:
lon_tmp = [
xc[i, j],
xc[i, j + 1],
xc[i + 1, j + 1],
xc[i + 1, j],
xc[i, j],
]
if wk_proj == "wgs84":
lon_tmp = np.degrees(np.unwrap(np.radians(lon_tmp)))
# Create ring
ring = ogr.Geometry(ogr.wkbLinearRing)
_ = ring.AddPoint(lon_tmp[0], yc[i, j])
_ = ring.AddPoint(lon_tmp[1], yc[i, j + 1])
_ = ring.AddPoint(lon_tmp[2], yc[i + 1, j + 1])
_ = ring.AddPoint(lon_tmp[3], yc[i + 1, j])
_ = ring.AddPoint(lon_tmp[4], yc[i, j])
else:
lon_tmp = xc[:, j]
npoints = len(lon_tmp)
if wk_proj == "wgs84":
lon_tmp = np.degrees(np.unwrap(np.radians(lon_tmp)))
# Create ring
ring = ogr.Geometry(ogr.wkbLinearRing)
for i in range(npoints + 1):
_ = ring.AddPoint(lon_tmp[i % npoints], yc[i % npoints, j])
# Create polygon
poly = ogr.Geometry(ogr.wkbPolygon)
poly.AddGeometry(ring)
# Add feature to layer
feature = ogr.Feature(featureDefn)
feature.SetGeometry(poly)
# Keep in memory original i/j
feature.SetField("index_i", i + 100)
feature.SetField("index_j", j + 100)
# Add data if any
if data is not None:
for iband in range(nbands):
feature.SetField(
f"field_{iband}", np.double(data[iband, i, j])
)
vector_grid_layer.CreateFeature(feature)
feature = None
debug("Vectorizing is done.")
return vector_grid_driver, vector_grid_ds, vector_grid_layer
####################
# ZONAL STATISTICS #
####################
[docs]
def zonal_stats(feat, raster, resol=10, option="mean", return_weight=False,
orig_lon_cyclic=False, **kwargs):
# Create for target raster the same projection as for the value raster
raster_srs = osr.SpatialReference()
raster_srs.ImportFromWkt(raster.GetProjectionRef())
# Create temporary layer from feature
drv = ogr.GetDriverByName("MEMORY")
ds = drv.CreateDataSource("memData")
layer = ds.CreateLayer("grid", raster_srs, geom_type=ogr.wkbPolygon)
layer.CreateFeature(feat)
# Get raster georeference info
geotransform = raster.GetGeoTransform()
xOrigin = geotransform[0]
yOrigin = geotransform[3]
pixelWidth = geotransform[1]
pixelHeight = geotransform[5]
xWest = min(xOrigin, xOrigin + pixelWidth * raster.RasterXSize)
xEast = max(xOrigin, xOrigin + pixelWidth * raster.RasterXSize)
ySouth = min(yOrigin, yOrigin + pixelHeight * raster.RasterYSize)
yNorth = max(yOrigin, yOrigin + pixelHeight * raster.RasterYSize)
# Get extent of feat
geom = feat.GetGeometryRef()
if geom.GetGeometryName() == "MULTIPOLYGON":
count = 0
pointsX = []
pointsY = []
for _ in geom:
geomInner = geom.GetGeometryRef(count)
ring = geomInner.GetGeometryRef(0)
numpoints = ring.GetPointCount()
for p in range(numpoints):
lon, lat, z = ring.GetPoint(p)
pointsX.append(lon)
pointsY.append(lat)
count += 1
elif geom.GetGeometryName() == "POLYGON":
ring = geom.GetGeometryRef(0)
numpoints = ring.GetPointCount()
pointsX = []
pointsY = []
for p in range(numpoints):
lon, lat, z = ring.GetPoint(p)
pointsX.append(lon)
pointsY.append(lat)
else:
raise CifError(
"ERROR: Geometry needs to be either Polygon or Multipolygon"
)
xmin = min(pointsX)
xmax = max(pointsX)
ymin = min(pointsY)
ymax = max(pointsY)
if xmax <= xWest or xmin >= xEast or ymax <= ySouth or ymin >= yNorth:
debug("Outside input domain")
debug(geotransform)
debug([raster.RasterXSize, raster.RasterYSize])
debug([xmin, xmax, ymin, ymax])
debug([xWest, xEast, ySouth, yNorth])
debug(pointsX)
debug(pointsY)
if return_weight:
return [], [], []
else:
return raster.RasterCount * [np.nan]
# Specify offset and rows and columns to read
xoff1 = int(np.floor((xmin - xOrigin) / pixelWidth))
xoff2 = int(np.floor((xmax - xOrigin) / pixelWidth))
xoff = max(0, min(xoff1, xoff2))
yoff1 = int(np.floor((ymin - yOrigin) / pixelHeight))
yoff2 = int(np.floor((ymax - yOrigin) / pixelHeight))
yoff = max(0, min(yoff1, yoff2))
xcount = min(max(xoff1, xoff2) + 1, raster.RasterXSize - 1) - xoff + 1
ycount = min(max(yoff1, yoff2) + 1, raster.RasterYSize - 1) - yoff + 1
# Create memory target raster
target_ds = gdal.GetDriverByName("MEM").Create(
"", xcount * resol, ycount * resol, 1, gdal.GDT_Byte
)
target_ds.SetGeoTransform(
(
xOrigin + xoff * pixelWidth,
pixelWidth / resol,
0,
yOrigin + yoff * pixelHeight,
0,
pixelHeight / resol,
)
)
# Rasterize zone polygon to raster
gdal.RasterizeLayer(target_ds, [1], layer, burn_values=[1])
bandmask = target_ds.GetRasterBand(1)
datamask = bandmask.ReadAsArray().astype(float)
datamask2 = [
[datamask[i::resol, j::resol] for j in range(resol)]
for i in range(resol)
]
datamask2 = np.array(datamask2).mean(axis=(0, 1))
# If asks weights only
if return_weight:
meshj, meshi = np.meshgrid(
np.arange(xoff, xoff + xcount), np.arange(yoff, yoff + ycount)
)
mask = datamask2 > 0
return meshi[mask], meshj[mask], datamask2[mask] / datamask2.sum()
# Equivalent clipped raster
nbands = raster.RasterCount
raster_locmean = []
for iband in range(1, nbands + 1):
banddataraster = raster.GetRasterBand(iband)
dataraster = banddataraster.ReadAsArray(
xoff, yoff, xcount, ycount
).astype(float)
# Averaging over the area corresponding to the pixel
zoneraster = dataraster * datamask2 / datamask2.sum()
if option == "mean":
raster_locmean.append(np.sum(zoneraster))
elif option == "median":
raster_locmean.append(np.median(zoneraster))
elif option == "min":
raster_locmean.append(zoneraster[np.argmin(datamask2)])
elif option == "max":
raster_locmean.append(
dataraster[
np.unravel_index(datamask2.argmax(), datamask2.shape)
]
)
else:
raster_locmean.append(np.nan)
# Calculate statistics of zonal raster
return raster_locmean
[docs]
def loop_zonal_stats(
lyr, raster, resol=10, option="mean", return_weight=False,
orig_lon_cyclic=False
):
featList = range(lyr.GetFeatureCount())
statList = []
debug("Looping on target grid cells and finding proportion from "
"original domain")
for FID in featList:
try:
feat = lyr.GetFeature(FID)
meanValue = zonal_stats(
feat,
raster,
resol=resol,
option=option,
return_weight=return_weight,
FID=FID,
orig_lon_cyclic=orig_lon_cyclic
)
except Exception as e:
info(e)
meanValue = raster.RasterCount * [np.nan]
raise e
if FID % int(len(featList) / 10) == 0:
debug(f"Target grid cell: {FID} out of {len(featList)}")
statList.append(meanValue)
if return_weight:
return statList
else:
return np.array(statList)