Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework DEM.vcrs to support any 3D transformation (and fix with PROJ update) #350

Merged
merged 41 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f14f0b4
Rework vref into vcrs, clean behaviour with ccrs and fix with pyproj …
rhugonnet Apr 4, 2023
2a7373d
Incremental commit, waiting for pyproj to_2d solution
rhugonnet Apr 5, 2023
d8feb9c
Merge remote-tracking branch 'upstream/main' into fix_vref
rhugonnet Apr 17, 2023
2bbb6d9
Incremental commit on tests
rhugonnet Apr 18, 2023
c32b55c
Move vcrs to its own module, write more tests
rhugonnet Apr 19, 2023
bb0ff61
Linting
rhugonnet Apr 19, 2023
eb64577
Add future import of annotations for python 3.8
rhugonnet Apr 19, 2023
8d64bd6
Add pyproj version condition to tests
rhugonnet Apr 19, 2023
a0b2fd7
Linting
rhugonnet Apr 19, 2023
ad871ba
Skip vertical transform is dest compound CRS is the same as source, a…
rhugonnet Apr 19, 2023
a4570e9
Remove proj-data dependency
rhugonnet Apr 19, 2023
7f4d49e
Add automatic download of PROJ grids with grid user input, and throug…
rhugonnet Apr 19, 2023
ff7a260
Linting
rhugonnet Apr 19, 2023
6b94bfd
Fix test return for pyproj < 3.5.1
rhugonnet Apr 19, 2023
c90e1fa
Add error to build_ccrs and tests
rhugonnet Apr 19, 2023
3ccf89e
Make axis length checks robust to older pyproj versions
rhugonnet Apr 20, 2023
d2d212b
Linting
rhugonnet Apr 20, 2023
6ac85bd
Make URL error more user-friendly, remove Windows test skipping
rhugonnet Apr 20, 2023
e8cedc6
Add a couple useful functions and VCRS setting based on CRS during DE…
rhugonnet Apr 20, 2023
83728b8
Linting
rhugonnet Apr 20, 2023
45a3b54
Remove vcrs_equals and override from_array
rhugonnet Apr 20, 2023
8b26e8a
Linting
rhugonnet Apr 20, 2023
34b3af8
Merge remote-tracking branch 'upstream/main' into fix_vref
rhugonnet Apr 20, 2023
0b5ba43
Comment other test based on speed
rhugonnet Apr 20, 2023
8753dd5
Linting
rhugonnet Apr 20, 2023
ce37d0d
Incremental commit on doc
rhugonnet Apr 21, 2023
8311f2d
Linting
rhugonnet Apr 21, 2023
77fad3d
Finalize documentation page
rhugonnet Apr 21, 2023
4e3acbe
Fix tests and refactor vcrs_from_user_input to not be public
rhugonnet Apr 21, 2023
a65849f
Linting
rhugonnet Apr 21, 2023
0fa2ae1
Try to fix PROJ path on readthedocs
rhugonnet Apr 21, 2023
73f2c38
Is it a problem with the datadir then?
rhugonnet Apr 21, 2023
d3839d4
Comment the pre_job
rhugonnet Apr 21, 2023
4100cc6
Retry with unset PROJ
rhugonnet Apr 21, 2023
bde4222
Try to unset during post-build instead
rhugonnet Apr 21, 2023
e8fdc90
Revert to default proj data dir in transformer group
rhugonnet Apr 21, 2023
dc4bd3a
Fix re-initiation of trans_group object after grid downloading in tra…
rhugonnet Apr 22, 2023
fd48ec8
Linting
rhugonnet Apr 22, 2023
58d66a3
Accout for eriks comments
rhugonnet Apr 25, 2023
e9ae7cc
Merge remote-tracking branch 'upstream/main' into fix_vref
rhugonnet Apr 25, 2023
d81405a
Linting
rhugonnet Apr 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def setup(app):
"notebook_interface": "jupyterlab",
# For launching Binder in Jupyterlab to open MD files as notebook (downloads them otherwise)
},
"show_toc_level": 2 # To show more levels on the right sidebar TOC
"show_toc_level": 2, # To show more levels on the right sidebar TOC
}

# For dark mode
Expand Down
208 changes: 94 additions & 114 deletions tests/test_dem.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
""" Functions to test the DEM tools."""
from __future__ import annotations

import os
import warnings
from typing import Any

import geoutils.raster as gr
import geoutils.raster.satimg as si
import numpy as np
import pyproj
import pytest
import rasterio as rio
from geoutils.raster.raster import _default_rio_attrs
from pyproj import CRS
from pyproj.transformer import Transformer

import xdem
import xdem.vcrs
from xdem.dem import DEM

DO_PLOT = False
Expand Down Expand Up @@ -97,7 +102,7 @@ def test_copy(self) -> None:
# raster_attrs = ['bounds', 'count', 'crs', 'dtypes', 'height', 'indexes', 'nodata',
# 'res', 'shape', 'transform', 'width']
# satimg_attrs = ['satellite', 'sensor', 'product', 'version', 'tile_name', 'datetime']
# dem_attrs = ['vref', 'vref_grid', 'ccrs']
# dem_attrs = ['vcrs', 'vcrs_grid', 'vcrs_name', 'ccrs']

# using list directly available in Class
attrs = [at for at in _default_rio_attrs if at not in ["name", "dataset_mask", "driver"]]
Expand All @@ -120,132 +125,107 @@ def test_copy(self) -> None:

assert np.array_equal(r3.data, r2.data)

def test_set_vref(self) -> None:
"""Tests to set the vertical reference"""
def test_set_vcrs(self) -> None:
"""Tests to set the vertical CRS."""

fn_img = xdem.examples.get_path("longyearbyen_ref_dem")
img = DEM(fn_img)
fn_dem = xdem.examples.get_path("longyearbyen_ref_dem")
dem = DEM(fn_dem)

# Check setting WGS84
img.set_vref(vref_name="WGS84")
assert img.vref == "WGS84"
assert img.vref_grid is None
# -- Test 1: we check with names --

# Check setting ellipsoid
dem.set_vcrs(new_vcrs="Ellipsoid")
assert dem.vcrs_name is not None
assert "Ellipsoid (No vertical CRS)." in dem.vcrs_name
assert dem.vcrs_grid is None

# Check setting EGM96
img.set_vref(vref_name="EGM96")
assert img.vref == "EGM96"
assert img.vref_grid == "us_nga_egm96_15.tif"
# The grid argument should have priority over name and parse the right vref name
img.set_vref(vref_name="WGS84", vref_grid="us_nga_egm96_15.tif")
assert img.vref == "EGM96"
dem.set_vcrs(new_vcrs="EGM96")
assert dem.vcrs_name == "EGM96 height"
assert dem.vcrs_grid == "us_nga_egm96_15.tif"

# Check setting EGM08
img.set_vref(vref_name="EGM08")
assert img.vref == "EGM08"
assert img.vref_grid == "us_nga_egm08_25.tif"
# The grid argument should have priority over name and parse the right vref name
img.set_vref(vref_name="best ref in the entire world, or any string", vref_grid="us_nga_egm08_25.tif")
assert img.vref == "EGM08"
dem.set_vcrs(new_vcrs="EGM08")
assert dem.vcrs_name == "EGM2008 height"
assert dem.vcrs_grid == "us_nga_egm08_25.tif"

# -- Test 2: we check with grids --
dem.set_vcrs(new_vcrs="us_nga_egm96_15.tif")
assert dem.vcrs_name == "unknown"
assert dem.vcrs_grid == "us_nga_egm96_15.tif"

dem.set_vcrs(new_vcrs="us_nga_egm08_25.tif")
assert dem.vcrs_name == "unknown"
assert dem.vcrs_grid == "us_nga_egm08_25.tif"

# Check that other existing grids are well detected in the pyproj.datadir
# TODO: Figure out why CI cannot get the grids on Windows
if os.name != "nt":
img.set_vref(vref_grid="is_lmi_Icegeoid_ISN93.tif")
dem.set_vcrs(new_vcrs="is_lmi_Icegeoid_ISN93.tif")
else:
with pytest.raises(ValueError):
img.set_vref(vref_grid="is_lmi_Icegeoid_ISN93.tif")
dem.set_vcrs(new_vcrs="is_lmi_Icegeoid_ISN93.tif")

# Check that non-existing grids raise errors
with pytest.raises(ValueError):
img.set_vref(vref_grid="the best grid in the entire world, or any non-existing string")
dem.set_vcrs(new_vcrs="the best grid in the entire world, or any non-existing string")

def test_to_vcrs(self) -> None:
"""Tests the conversion of vertical CRS."""

fn_dem = xdem.examples.get_path("longyearbyen_ref_dem")
dem = DEM(fn_dem)

dem = dem.reproject(dst_crs=4979)
dem_before_trans = dem.copy()

dem.set_vcrs(new_vcrs="Ellipsoid")
ccrs_init = dem.ccrs
median_before = np.nanmean(dem)
dem.to_vcrs(dst_vcrs="EGM96")
median_after = np.nanmean(dem)

# About 32 meters of difference in Svalbard between EGM96 geoid and ellipsoid
assert median_after - median_before == pytest.approx(-32, rel=0.1)

# Check that the results are consistent with the operation done independently
ccrs_dest = xdem.vcrs._build_ccrs_from_crs_and_vcrs(dem.crs, xdem.vcrs._vcrs_from_user_input("EGM96"))
transformer = Transformer.from_crs(crs_from=ccrs_init, crs_to=ccrs_dest, always_xy=True)

xx, yy = dem.coords()
x = xx[5, 5]
y = yy[5, 5]
z = dem_before_trans.data[5, 5]
z_out = transformer.transform(xx=x, yy=y, zz=z)[2]

assert z_out == pytest.approx(dem.data[5, 5])

# Compare to manually-extracted shifts at specific coordinates for the geoid grids
egm96_chile = {"grid": "us_nga_egm96_15.tif", "lon": -68, "lat": -20, "shift": 42}
egm08_chile = {"grid": "us_nga_egm08_25.tif", "lon": -68, "lat": -20, "shift": 42}
geoid96_alaska = {"grid": "us_noaa_geoid06_ak.tif", "lon": -145, "lat": 62, "shift": 17}
isn93_iceland = {"grid": "is_lmi_Icegeoid_ISN93.tif", "lon": -18, "lat": 65, "shift": 68}

@pytest.mark.parametrize("grid_shifts", [egm08_chile, egm08_chile, geoid96_alaska, isn93_iceland]) # type: ignore
def test_to_vcrs__grids(self, grid_shifts: dict[str, Any]) -> None:
"""Tests grids to convert vertical CRS."""

# Using an arbitrary elevation of 100 m (no influence on the transformation)
dem = DEM.from_array(
data=np.array([[100]]),
transform=rio.transform.from_bounds(
grid_shifts["lon"], grid_shifts["lat"], grid_shifts["lon"] + 0.01, grid_shifts["lat"] + 0.01, 0.01, 0.01
),
crs=CRS.from_epsg(4326),
nodata=None,
)
dem.set_vcrs("Ellipsoid")

@pytest.mark.skip("This fails on Windows because the grids are not found") # type: ignore
def test_to_vref(self) -> None:
"""Tests to convert vertical references"""
# Transform to the vertical CRS of the grid
dem.to_vcrs(grid_shifts["grid"])

# First, we use test points to test the vertical transform
# Let's start with Chile
lat = 43.70012234
lng = -79.41629234
z = 100
with warnings.catch_warnings():
warnings.filterwarnings("ignore", module="pyproj")
# init is deprecated by
ellipsoid = pyproj.Proj(init="EPSG:4326") # WGS84 datum ellipsoid height
# EGM96 geoid in Chile, we expect ~30 m difference
geoid = pyproj.Proj(init="EPSG:4326", geoidgrids="us_nga_egm96_15.tif")
transformer = pyproj.Transformer.from_proj(ellipsoid, geoid)
z_out = transformer.transform(lng, lat, z)[2]

# Check that the final elevation is finite, and higher than ellipsoid by less than 40 m (typical geoid in Chile)
assert np.logical_and.reduce((np.isfinite(z_out), np.greater(z_out, z), np.less(np.abs(z_out - z), 40)))

# With the EGM2008 (catch warnings as this use of init is depecrated)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", module="pyproj")
ellipsoid = pyproj.Proj(init="EPSG:4326") # WGS84 datum ellipsoid height
geoid = pyproj.Proj(init="EPSG:4326", geoidgrids="us_nga_egm08_25.tif")
transformer = pyproj.Transformer.from_proj(ellipsoid, geoid)
z_out = transformer.transform(lng, lat, z)[2]

# Check final elevation is finite, higher than ellipsoid with less than 40 m difference (typical geoid in Chile)
assert np.logical_and.reduce((np.isfinite(z_out), np.greater(z_out, z), np.less(np.abs(z_out - z), 40)))

# With GEOID2006 for Alaska
lat = 65
lng = -140
with warnings.catch_warnings():
warnings.filterwarnings("ignore", module="pyproj")
# init is deprecated by
ellipsoid = pyproj.Proj(init="EPSG:4326") # WGS84 datum ellipsoid height
geoid = pyproj.Proj(init="EPSG:4326", geoidgrids="us_noaa_geoid06_ak.tif")
transformer = pyproj.Transformer.from_proj(ellipsoid, geoid)
z_out = transformer.transform(lng, lat, z)[2]

# Check that the final elevation is finite, lower than ellipsoid by less than 20 m (typical geoid in Alaska)
assert np.logical_and.reduce((np.isfinite(z_out), np.less(z_out, z), np.less(np.abs(z_out - z), 20)))

# With ISN1993 for Iceland
lat = 65
lng = -18
# TODO: Figure out why CI cannot get the grids on Windows
with warnings.catch_warnings():
warnings.filterwarnings("ignore", module="pyproj")
# init is deprecated by
ellipsoid = pyproj.Proj(init="EPSG:4326") # WGS84 datum ellipsoid height
# Iceland, we expect a ~70m difference
geoid = pyproj.Proj(init="EPSG:4326", geoidgrids="is_lmi_Icegeoid_ISN93.tif")
transformer = pyproj.Transformer.from_proj(ellipsoid, geoid)
z_out = transformer.transform(lng, lat, z)[2]

# Check that the final elevation is finite, lower than ellipsoid by less than 100 m (typical geoid in Iceland)
assert np.logical_and.reduce((np.isfinite(z_out), np.less(z_out, z), np.less(np.abs(z_out - z), 100)))

# Check that the function does not run without a reference set
fn_img = xdem.examples.get_path("longyearbyen_ref_dem")
img = DEM(fn_img)
with pytest.raises(ValueError):
img.to_vref(vref_name="EGM96")

# Check that the function properly runs with a reference set
img.set_vref(vref_name="WGS84")
mean_ellips = np.nanmean(img.data)
img.to_vref(vref_name="EGM96")
mean_geoid_96 = np.nanmean(img.data)
assert img.vref == "EGM96"
assert img.vref_grid == "us_nga_egm96_15.tif"
# Check that the geoid is lower than ellipsoid, less than 35 m difference (Svalbard)
assert np.greater(mean_ellips, mean_geoid_96)
assert np.less(np.abs(mean_ellips - mean_geoid_96), 35.0)

# Check in the other direction
img = DEM(fn_img)
img.set_vref(vref_name="EGM96")
mean_geoid_96 = np.nanmean(img.data)
img.to_vref(vref_name="WGS84")
mean_ellips = np.nanmean(img.data)
assert img.vref == "WGS84"
assert img.vref_grid is None
# Check that the geoid is lower than ellipsoid, less than 35 m difference (Svalbard)
assert np.greater(mean_ellips, mean_geoid_96)
assert np.less(np.abs(mean_ellips - mean_geoid_96), 35.0)
# Compare the elevation difference
z_diff = 100 - dem.data[0, 0]

# Check the shift is the one expect within 10%
assert z_diff == pytest.approx(grid_shifts["shift"], rel=0.1)
Loading