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

Add temporal attribute to RasterBlock + base Clip logic on it #117

Merged
merged 18 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/test-conda.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, macos-12, windows-latest]
python: ["3.9", "3.10"]

steps:
Expand All @@ -29,7 +29,7 @@ jobs:
- name: Setup Environment
shell: bash
run: |
conda create --name test python=${{ matrix.python }} pytest numpy gdal=3.* scipy pytz dask-core toolz geopandas pyproj>=2
conda create --name test python=${{ matrix.python }} pytest numpy=1.* gdal=3.* scipy pytz dask-core toolz "pandas<2.2" geopandas=0.* "pyproj>=2"
source activate test
python -V
conda info
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ jobs:
pins: "pygdal==3.4.1.* scipy==1.9.* dask[delayed]==2021.7.* pandas==1.4.* geopandas==0.11.*"
- os: ubuntu-22.04
python: "3.11"
display_name: "latest"
pins: "pygdal==3.4.1.*"
numpy: "==1.*"
pins: "pygdal==3.4.1.* geopandas==0.* pandas==2.1.*"

steps:
- uses: actions/checkout@v2
Expand Down
7 changes: 5 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
Changelog of dask-geomodeling
===================================================

2.4.5 (unreleased)
2.5.0 (unreleased)
------------------

- Nothing changed yet.
- Added mandatory `temporal` attribute for RasterBlock.

- Added version constraints showing incompatibility with geopandas 1.*,
pandas 2.2, and numpy 2.


2.4.4 (2024-01-17)
Expand Down
9 changes: 7 additions & 2 deletions dask_geomodeling/raster/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ class RasterBlock(Block):
All RasterBlocks must be derived from this base class and must implement
the following attributes:

- ``period``: a tuple of datetimes
- ``timedelta``: a datetime.timedelta (or None if nonequidistant)
- ``period``: a tuple of datetimes (or None if empty)
- ``timedelta``: a datetime.timedelta (or None if nonequidistant / nontemporal)
- ``extent``: a tuple ``(x1, y1, x2, y2)``
- ``dtype``: a numpy dtype object
- ``fillvalue``: a number
- ``geometry``: OGR Geometry
- ``projection``: WKT string
- ``geo_transform``: a tuple of 6 numbers
- ``temporal``: a boolean indicating whether the raster is temporal

These attributes are ``None`` if the raster is empty.

Expand Down Expand Up @@ -178,6 +179,10 @@ def period(self):
def timedelta(self):
return self.store.timedelta

@property
def temporal(self):
return self.store.temporal

@property
def dtype(self):
return self.store.dtype
Expand Down
4 changes: 4 additions & 0 deletions dask_geomodeling/raster/combine.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def get_aligned_timedelta(sources):
def timedelta(self):
""" The period between timesteps in case of equidistant time. """
return self.get_aligned_timedelta(self.args)

@property
def temporal(self):
return any(x.temporal for x in self.args)

@property
def period(self):
Expand Down
37 changes: 23 additions & 14 deletions dask_geomodeling/raster/elemwise.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,21 @@ class BaseElementwise(RasterBlock):

def __init__(self, *args):
super(BaseElementwise, self).__init__(*args)
# check the timedelta so that an error is raised if incompatible
if len(self._sources) > 1:
self.timedelta # NOQA
# check the temporal and timedelta attributes of the sources
temporal, delta = self._sources[0].temporal, self._sources[0].timedelta
has_delta = temporal and (delta is not None)
for source in self._sources[1:]:
if source.temporal != temporal:
raise ValueError("Temporal properties of input rasters do not match.")
if not has_delta:
continue # non temporal or nonequidistant in time; skip the check on timedelta
other_delta = source.timedelta
if other_delta is None:
continue # nonequidistant in time; skip the check on timedelta
if delta != other_delta:
raise ValueError(
"Time resolutions of input rasters are not equal."
)

@property
def _sources(self):
Expand Down Expand Up @@ -75,20 +87,17 @@ def timedelta(self):
""" The period between timesteps in case of equidistant time. """
if len(self._sources) == 1:
return self._sources[0].timedelta

timedeltas = [s.timedelta for s in self._sources]
if any(timedelta is None for timedelta in timedeltas):
return None # None precedes

# multiple timedeltas: assert that they are equal
if not timedeltas[1:] == timedeltas[:-1]:
raise ValueError(
"Time resolutions of input rasters are not "
"equal ({}).".format(timedeltas)
)
if any(x is None for x in timedeltas):
return None
else:
return timedeltas[0]

@property
def temporal(self):
"""If any of the sources is temporal, the result is temporal."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is enforced by the init(), but maybe not the most adequate description for this property.

return self._sources[0].temporal

@property
def period(self):
""" Return period datetime tuple. """
Expand All @@ -97,7 +106,7 @@ def period(self):

periods = [s.period for s in self._sources]
if any(period is None for period in periods):
return None # None precedes
return None # None (empty raster) precedes

# multiple periods: return the overlapping period
start = max([p[0] for p in periods])
Expand Down
21 changes: 19 additions & 2 deletions dask_geomodeling/raster/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,17 @@ class Clip(BaseSingle):
def __init__(self, store, source):
if not isinstance(source, RasterBlock):
raise TypeError("'{}' object is not allowed".format(type(store)))
# timedeltas are required to be equal
if store.timedelta != source.timedelta:
if store.temporal and not source.temporal:
raise ValueError(
"The values raster is temporal while the clipping mask is not. "
"Consider using Snap."
)
if not store.temporal and source.temporal:
raise ValueError(
"The clipping mask is temporal while the values raster is not. "
"Consider using Snap."
)
if store.temporal and (store.timedelta != source.timedelta):
raise ValueError(
"Time resolution of the clipping mask does not match that of "
"the values raster. Consider using Snap."
Expand Down Expand Up @@ -572,6 +581,10 @@ def extent(self):
@property
def timedelta(self):
return None

@property
def temporal(self):
return False

@property
def geometry(self):
Expand Down Expand Up @@ -743,6 +756,10 @@ def extent(self):
@property
def timedelta(self):
return None

@property
def temporal(self):
return False

@property
def geometry(self):
Expand Down
13 changes: 11 additions & 2 deletions dask_geomodeling/raster/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,14 @@ def period(self):

@property
def timedelta(self):
if len(self) <= 1:
if self.time_delta is None:
return None
return timedelta(milliseconds=self.time_delta)
else:
return timedelta(milliseconds=self.time_delta)

@property
def temporal(self):
return self.time_delta is not None

def get_sources_and_requests(self, **request):
mode = request["mode"]
Expand Down Expand Up @@ -404,6 +409,10 @@ def timedelta(self):
if len(self) <= 1:
return None
return timedelta(milliseconds=self.time_delta)

@property
def temporal(self):
return len(self) > 1

def get_sources_and_requests(self, **request):
mode = request["mode"]
Expand Down
8 changes: 8 additions & 0 deletions dask_geomodeling/raster/temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def period(self):
@property
def timedelta(self):
return self.index.timedelta

@property
def temporal(self):
return self.index.temporal

@property
def extent(self):
Expand Down Expand Up @@ -553,6 +557,10 @@ def timedelta(self):
except AttributeError:
return # e.g. Month is non-equidistant

@property
def temporal(self):
return self.frequency is not None

@property
def dtype(self):
return dtype_for_statistic(self.source.dtype, self.statistic)
Expand Down
16 changes: 16 additions & 0 deletions dask_geomodeling/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ def empty_source():
)



@pytest.fixture(scope="session")
def empty_temporal_source():
time_first = datetime(2000, 1, 1)
time_delta = timedelta(hours=1)
yield MemorySource(
data=np.empty((0, 0, 0), dtype=np.uint8),
no_data_value=255,
projection="EPSG:28992",
pixel_size=0.5,
pixel_origin=(135000, 456000),
time_first=time_first,
time_delta=time_delta,

)

@pytest.fixture(scope="session")
def nodata_source():
time_first = datetime(2000, 1, 1)
Expand Down
16 changes: 13 additions & 3 deletions dask_geomodeling/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ class MockRaster(RasterBlock):
"""

def __init__(
self, origin=None, timedelta=None, bands=None, value=1, projection="EPSG:3857"
self, origin=None, timedelta=None, bands=None, value=1, projection="EPSG:3857", temporal=None
):
self.origin = origin
self._timedelta = timedelta
self.bands = bands
self.value = value
super(MockRaster, self).__init__(origin, timedelta, bands, value, projection)
if temporal is None:
temporal = timedelta is not None
super(MockRaster, self).__init__(origin, timedelta, bands, value, projection, temporal)

@property
def dtype(self):
Expand All @@ -53,13 +55,17 @@ def dtype(self):
@property
def fillvalue(self):
return get_dtype_max(self.dtype)

@property
def temporal(self):
return self.args[5]

def get_sources_and_requests(self, **request):
return [(self.args, None), (request, None)]

@staticmethod
def process(args, request):
origin, timedelta, bands, value, src_projection = args
origin, timedelta, bands, value, src_projection, temporal = args
if origin is None or timedelta is None or bands is None:
return
td_seconds = timedelta.total_seconds()
Expand Down Expand Up @@ -155,6 +161,10 @@ def period(self):
@property
def timedelta(self):
return self._timedelta

@property
def temporal(self):
return self.args[5]

@property
def extent(self):
Expand Down
Loading
Loading