GRSdriver 2.0.2__tar.gz → 2.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/__init__.py +8 -1
  2. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/driver_landsat_col2.py +32 -14
  3. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/run.py +0 -1
  4. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/utils.py +28 -0
  5. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver.egg-info/PKG-INFO +56 -22
  6. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver.egg-info/requires.txt +16 -4
  7. {grsdriver-2.0.2 → grsdriver-2.0.4}/PKG-INFO +56 -22
  8. {grsdriver-2.0.2 → grsdriver-2.0.4}/README.md +41 -17
  9. {grsdriver-2.0.2 → grsdriver-2.0.4}/pyproject.toml +6 -1
  10. {grsdriver-2.0.2 → grsdriver-2.0.4}/tests/test_landsat_driver.py +16 -0
  11. {grsdriver-2.0.2 → grsdriver-2.0.4}/tests/test_utils.py +29 -0
  12. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/driver_S2_SAFE.py +0 -0
  13. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/__init__.py +0 -0
  14. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/L4_TM_RSR.xlsx +0 -0
  15. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/L5_TM_RSR.xlsx +0 -0
  16. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/L7_RSR.xlsx +0 -0
  17. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/L8_Ball_BA_RSR.v1.2.xlsx +0 -0
  18. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/L8_TIRS_Relative_Spectral_Responses.BA_.v1.xlsx +0 -0
  19. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/L9_OLI2_Ball_FPM_RSR.v1.0.xlsx +0 -0
  20. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/L9_TIRS2_Relative_Spectral_Responses.BA.v1.0.xlsx +0 -0
  21. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/Sentinel-2_MSI_Spectral_Responses.xlsx +0 -0
  22. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/__init__.py +0 -0
  23. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/rsr_landsat_8_oli.nc +0 -0
  24. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/rsr_landsat_8_oli_tirs.nc +0 -0
  25. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/rsr_landsat_8_tirs.nc +0 -0
  26. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/rsr_landsat_9_oli.nc +0 -0
  27. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/rsr_landsat_9_oli_tirs.nc +0 -0
  28. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/data/rsr_landsat_9_tirs.nc +0 -0
  29. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/rsr/landsat_formatting.py +0 -0
  30. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver/visual.py +0 -0
  31. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver.egg-info/SOURCES.txt +0 -0
  32. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver.egg-info/dependency_links.txt +0 -0
  33. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver.egg-info/entry_points.txt +0 -0
  34. {grsdriver-2.0.2 → grsdriver-2.0.4}/GRSdriver.egg-info/top_level.txt +0 -0
  35. {grsdriver-2.0.2 → grsdriver-2.0.4}/LICENSE +0 -0
  36. {grsdriver-2.0.2 → grsdriver-2.0.4}/requirements.txt +0 -0
  37. {grsdriver-2.0.2 → grsdriver-2.0.4}/setup.cfg +0 -0
  38. {grsdriver-2.0.2 → grsdriver-2.0.4}/tests/test_package.py +0 -0
  39. {grsdriver-2.0.2 → grsdriver-2.0.4}/tests/test_s2_driver.py +0 -0
@@ -88,7 +88,14 @@ equations.
88
88
  # v2.0.2 (2026-10-16):
89
89
  # continuous integration (tests with Python 3.9 to 3.13, PyPI publishing on tags);
90
90
  # grs_driver --help without matplotlib/cartopy
91
- __version__='2.0.2'
91
+ #
92
+ # v2.0.3 (2026-10-16):
93
+ # README for 2.0, optional extras viz and tutorials, minimal environment.yml
94
+ #
95
+ # v2.0.4 (2026-10-17):
96
+ # direct reading of the bands on their own grid (~20-25 % faster on full scenes),
97
+ # Landsat angles in float32 (also interpolated and in dask mode), automatic GitHub release
98
+ __version__='2.0.4'
92
99
  __package__ ='GRSdriver'
93
100
 
94
101
  from .driver_landsat_col2 import LandsatDriver
@@ -18,8 +18,10 @@ Example::
18
18
  import datetime
19
19
  import glob
20
20
  import os
21
+ from concurrent.futures import ThreadPoolExecutor
21
22
 
22
23
  import numpy as np
24
+ import dask.array as da
23
25
  import pandas as pd
24
26
  import geopandas as gpd
25
27
  import xarray as xr
@@ -254,9 +256,11 @@ class LandsatDriver():
254
256
  # convert band from normalized radiance into reflectance
255
257
  # vis-NIR-SWIR bands only (upper limit 3000nm)
256
258
  # -----------------------------------------------------------
257
- mus = np.cos(np.radians(self.prod['sza'])).astype(self.prod.bands.dtype)
258
- self.prod['bands'] = xr.where(self.prod.wl <= 3000, self.prod.bands / mus, self.prod.bands,
259
- keep_attrs=True).transpose(*self.prod.bands.dims)
259
+ bands = self.prod.bands
260
+ mus = np.cos(np.radians(self.prod['sza'])).astype(bands.dtype)
261
+ # DataArray.where rather than xr.where, which computes float64 on dask arrays (float32 declared)
262
+ self.prod['bands'] = bands.where(self.prod.wl > 3000, bands / mus).astype(bands.dtype) \
263
+ .transpose(*bands.dims)
260
264
 
261
265
 
262
266
  self.prod.attrs = self.prod.attrs
@@ -301,7 +305,19 @@ class LandsatDriver():
301
305
  margin = 2 * max(abs(float(ds.x[1] - ds.x[0])), abs(float(ds.y[1] - ds.y[0])))
302
306
  ds = ds.sel(x=slice(x.min() - margin, x.max() + margin),
303
307
  y=slice(y.max() + margin, y.min() - margin))
308
+ dtypes = {var: ds[var].dtype for var in ds.data_vars}
304
309
  ds = ds.interp(x=self.prod.x, y=self.prod.y, method=method)
310
+ # interp computes in float64 (on dask arrays while declaring the input dtype):
311
+ # float variables back to their dtype (e.g. float32 angles)
312
+ for var, dtype in dtypes.items():
313
+ if dtype.kind == 'f':
314
+ data = ds[var].data
315
+ if isinstance(data, da.Array):
316
+ data = data.map_blocks(lambda block, dtype=dtype: block.astype(dtype, copy=False),
317
+ dtype=dtype)
318
+ else:
319
+ data = data.astype(dtype, copy=False)
320
+ ds[var] = ds[var].copy(data=data)
305
321
  if self.prod.bands.chunks is not None:
306
322
  ds = ds.chunk({dim: self.prod.bands.chunksizes[dim] for dim in ('y', 'x')})
307
323
  return ds
@@ -463,18 +479,20 @@ class LandsatDriver():
463
479
 
464
480
  # lazy reading in dask mode
465
481
  chunks = DASK_CHUNKS if use_dask() else None
466
- geom = []
467
- for geom_ in geoms:
468
- ang_file = glob.glob(opj(self.abspath, '*' + geom_[1] + '.TIF'))[0]
469
- raster = xrio.open_rasterio(ang_file, chunks=chunks).squeeze()
470
- raster.rio.write_nodata(nodata, inplace=True)
471
- raster = raster.where(raster != raster.rio.nodata).astype(np.float32) * np.float32(scale_factor)
472
-
473
- raster.name = geom_[0]
474
- geom.append(raster)
475
- geom = xr.merge(geom,compat='override')
482
+
483
+ def read_angle(suffix):
484
+ # native grid of the angle file, read as float32 (exact for the stored integers)
485
+ ang_file = glob.glob(opj(self.abspath, '*' + suffix + '.TIF'))[0]
486
+ with rasterio.open(ang_file) as src:
487
+ grid = (src.transform, src.width, src.height)
488
+ raster = read_on_grid(ang_file, *grid, nodata=nodata, dtype='float32', chunks=chunks)
489
+ return raster.where(raster != nodata) * np.float32(scale_factor)
490
+
491
+ with ThreadPoolExecutor(max_workers=max(1, self.n_workers)) as executor:
492
+ rasters = list(executor.map(read_angle, [suffix for _, suffix in geoms]))
493
+ geom = xr.Dataset({name: raster for (name, _), raster in zip(geoms, rasters)})
476
494
  geom['raa'] = (geom['saa'] - geom['vaa']) # %360
477
- self.geom = geom.drop_vars(['vaa', 'saa','band'])
495
+ self.geom = geom.drop_vars(['vaa', 'saa'])
478
496
 
479
497
  @staticmethod
480
498
  def scat_angle(sza, vza, azi):
@@ -37,7 +37,6 @@ os.environ[USE_DASK] = "1"
37
37
  def main():
38
38
 
39
39
  args = docopt(__doc__, version=__package__ + '_' + __version__)
40
- print(args)
41
40
 
42
41
  # optional dependencies (extra 'cli'), imported after the parsing so that --help works without them
43
42
  try:
@@ -49,11 +49,39 @@ def use_dask():
49
49
  return value.lower() in ('1', 'true')
50
50
 
51
51
 
52
+ def _pixel_offset(src_transform, transform):
53
+ '''
54
+ (row, col) offset of the target grid in the source grid when both grids coincide (same
55
+ pixel size, north up, target origin on a source pixel corner), else None.
56
+ '''
57
+ if not (np.isclose(transform.a, src_transform.a) and np.isclose(transform.e, src_transform.e)
58
+ and transform.b == transform.d == src_transform.b == src_transform.d == 0):
59
+ return None
60
+ col = (transform.c - src_transform.c) / src_transform.a
61
+ row = (transform.f - src_transform.f) / src_transform.e
62
+ if abs(col - round(col)) > 1e-6 or abs(row - round(row)) > 1e-6:
63
+ return None
64
+ return round(row), round(col)
65
+
66
+
52
67
  def _read_window(path, transform, width, height, resampling, nodata, dtype):
53
68
  '''
54
69
  Read the first band of ``path`` on a target grid, as a numpy array (see :func:`read_on_grid`).
55
70
  '''
56
71
  with rasterio.open(path) as src:
72
+ offset = _pixel_offset(src.transform, transform)
73
+ if offset is not None:
74
+ # target grid = part of the source grid: direct copy, no resampling needed
75
+ row, col = offset
76
+ src_nodata = src.nodata if nodata is None else nodata
77
+ data = np.full((height, width), 0 if src_nodata is None else src_nodata,
78
+ dtype=dtype or src.dtypes[0])
79
+ r0, c0 = max(row, 0), max(col, 0)
80
+ r1, c1 = min(row + height, src.height), min(col + width, src.width)
81
+ if r1 > r0 and c1 > c0:
82
+ window = rasterio.windows.Window(c0, r0, c1 - c0, r1 - r0)
83
+ data[r0 - row:r1 - row, c0 - col:c1 - col] = src.read(1, window=window)
84
+ return data
57
85
  # source window covering the target grid (north up), with a margin for the
58
86
  # resampling kernel
59
87
  minx, maxx = transform.c, transform.c + transform.a * width
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GRSdriver
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Driver for L1C satellite images dedicated to GRS processor
5
5
  Author-email: Tristan Harmel <tristan.harmel@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -33,14 +33,24 @@ Requires-Dist: xarray
33
33
  Requires-Dist: shapely
34
34
  Requires-Dist: geopandas>=1.0
35
35
  Provides-Extra: dev
36
- Requires-Dist: black; extra == "dev"
37
- Requires-Dist: bumpver; extra == "dev"
38
- Requires-Dist: isort; extra == "dev"
39
- Requires-Dist: pip-tools; extra == "dev"
40
36
  Requires-Dist: pytest; extra == "dev"
37
+ Requires-Dist: build; extra == "dev"
38
+ Requires-Dist: twine; extra == "dev"
41
39
  Provides-Extra: cli
42
40
  Requires-Dist: matplotlib; extra == "cli"
43
41
  Requires-Dist: cartopy; extra == "cli"
42
+ Provides-Extra: viz
43
+ Requires-Dist: matplotlib; extra == "viz"
44
+ Requires-Dist: holoviews; extra == "viz"
45
+ Requires-Dist: datashader; extra == "viz"
46
+ Requires-Dist: bokeh; extra == "viz"
47
+ Requires-Dist: colorcet; extra == "viz"
48
+ Requires-Dist: panel; extra == "viz"
49
+ Provides-Extra: tutorials
50
+ Requires-Dist: jupyterlab; extra == "tutorials"
51
+ Requires-Dist: matplotlib; extra == "tutorials"
52
+ Requires-Dist: cartopy; extra == "tutorials"
53
+ Requires-Dist: requests; extra == "tutorials"
44
54
  Dynamic: license-file
45
55
 
46
56
  <img src="https://raw.githubusercontent.com/Tristanovsk/GRSdriver/master/docs/source/_static/GRS_driver.png" alt="GRSdriver_icon" width="100"/>
@@ -49,7 +59,10 @@ Dynamic: license-file
49
59
  ## Driver dedicated to load and prepare data for the GRS processor
50
60
 
51
61
  [![PyPI](https://img.shields.io/pypi/v/GRSdriver)](https://pypi.org/project/GRSdriver/)
62
+ [![Python](https://img.shields.io/pypi/pyversions/GRSdriver)](https://pypi.org/project/GRSdriver/)
63
+ [![tests](https://github.com/Tristanovsk/GRSdriver/actions/workflows/tests.yml/badge.svg)](https://github.com/Tristanovsk/GRSdriver/actions/workflows/tests.yml)
52
64
  [![Documentation](https://readthedocs.org/projects/grsdriver/badge/?version=latest)](https://grsdriver.readthedocs.io)
65
+ [![License](https://img.shields.io/pypi/l/GRSdriver)](https://github.com/Tristanovsk/GRSdriver/blob/master/LICENSE)
53
66
 
54
67
  GRSdriver loads a Level-1 satellite image into a single `xarray.Dataset` ready for the
55
68
  [GRS](https://github.com/CNES/GRSprocessor) atmospheric and sunglint correction:
@@ -59,37 +72,49 @@ GRSdriver loads a Level-1 satellite image into a single `xarray.Dataset` ready f
59
72
  - Landsat 8 and 9 Collection-2 L1: reflectance, brightness temperature of the thermal bands,
60
73
  sun/viewing angles and L1 quality flags
61
74
  - spectral response functions and central wavelengths of each band
62
- - spatial subsetting, lazy (dask) loading and low memory options for full tiles
75
+ - spatial subsetting, parallel reading, lazy (dask) loading and low memory options for full tiles
63
76
  - visualization tools
64
77
 
65
78
  Full documentation: https://grsdriver.readthedocs.io
66
79
 
80
+ ## What's new in 2.0
81
+
82
+ - **pip-only install**: EOReader and the GDAL Python bindings are no longer needed.
83
+ - **~2x faster**: full Sentinel-2 tile at 20 m in ~30 s (bands read and angles computed in
84
+ parallel), fully lazy dask mode in ~20 s.
85
+ - **Fixes**: Sentinel-2 angles of processing baselines < 4.0, alignment of the Landsat
86
+ panchromatic band, size of the `SpatioTemp` boxes.
87
+ - **Upgrading from 1.x**: the drivers no longer take `**kwargs` (they were passed to EOReader) and
88
+ have no `.reader` attribute; a `subset` given as a sequence is a bounding box
89
+ `(minx, miny, maxx, maxy)` in the product CRS. See the
90
+ [CHANGELOG](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md).
91
+
67
92
  ## Installation
68
93
 
94
+ Python 3.9 to 3.13:
95
+
69
96
  ```commandline
70
97
  pip install GRSdriver
71
98
  ```
72
99
 
73
100
  No separate GDAL installation is needed: rasterio and geopandas ship with their own GDAL
74
- (including the JPEG2000 driver for Sentinel-2). The `grs_driver` command line (RGB quicklooks)
75
- needs matplotlib and cartopy:
76
- ```commandline
77
- pip install "GRSdriver[cli]"
78
- ```
101
+ (including the JPEG2000 driver for Sentinel-2). Optional extras:
102
+
103
+ | Extra | For | Install |
104
+ |---|---|---|
105
+ | `cli` | `grs_driver` command line (RGB quicklooks) | `pip install "GRSdriver[cli]"` |
106
+ | `viz` | interactive visualization tools (`GRSdriver.visual`) | `pip install "GRSdriver[viz]"` |
107
+ | `tutorials` | running the tutorial notebooks | `pip install "GRSdriver[tutorials]"` |
79
108
 
80
- ### Installation from the repository (to use the notebooks and visualization tools)
109
+ ### From the repository
110
+
111
+ To run the tutorials or work on the code:
81
112
  ```commandline
82
113
  git clone https://github.com/Tristanovsk/GRSdriver.git
83
114
  cd GRSdriver
84
- conda env create -f environment.yml
85
- conda activate grs
86
- pip install -e .
87
- ```
88
-
89
- For the interactive visualization tools and the notebooks:
90
- ```commandline
91
- conda install -c conda-forge jupyterlab holoviews panel datashader cartopy
115
+ pip install -e ".[cli,viz,tutorials,dev]"
92
116
  ```
117
+ or with conda: `conda env create -f environment.yml && conda activate grsdriver`.
93
118
 
94
119
  ## Quick start
95
120
 
@@ -104,6 +129,12 @@ prod = l1c.prod # bands, vza, raa, sza, SRF, wl_true + metadata
104
129
  # Landsat 8/9
105
130
  l1 = LandsatDriver('LC09_L1TP_..._02_T1', resolution=30)
106
131
  l1.load_product()
132
+
133
+ # region of interest: any geometry (GeoSeries, any CRS), here a lon/lat box
134
+ import geopandas as gpd
135
+ from shapely.geometry import box
136
+ roi = gpd.GeoSeries([box(5.0, 43.38, 5.25, 43.58)], crs=4326)
137
+ l1c = Sentinel2Driver('S2B_MSIL1C_..._.SAFE', resolution=20, subset=roi)
107
138
  ```
108
139
 
109
140
  The bands are read by 4 threads in parallel: set `n_workers` (e.g. `n_workers=1`) to trade speed
@@ -174,6 +205,9 @@ Viewing azimuth of each band computed by GRSdriver:
174
205
 
175
206
  ![example files](https://raw.githubusercontent.com/Tristanovsk/GRSdriver/master/docs/source/_static/example_ndwi_mask.png)
176
207
 
177
- ## Changes
208
+ ## Changes and contributing
178
209
 
179
- See [CHANGELOG.md](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md).
210
+ See the [CHANGELOG](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md). Tests run
211
+ with `pytest tests` (the tests that need satellite products are skipped when they are not
212
+ available); the [contributing guide](https://grsdriver.readthedocs.io/en/latest/contributing.html)
213
+ explains how to update the tutorials and make a release.
@@ -19,8 +19,20 @@ matplotlib
19
19
  cartopy
20
20
 
21
21
  [dev]
22
- black
23
- bumpver
24
- isort
25
- pip-tools
26
22
  pytest
23
+ build
24
+ twine
25
+
26
+ [tutorials]
27
+ jupyterlab
28
+ matplotlib
29
+ cartopy
30
+ requests
31
+
32
+ [viz]
33
+ matplotlib
34
+ holoviews
35
+ datashader
36
+ bokeh
37
+ colorcet
38
+ panel
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GRSdriver
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Driver for L1C satellite images dedicated to GRS processor
5
5
  Author-email: Tristan Harmel <tristan.harmel@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -33,14 +33,24 @@ Requires-Dist: xarray
33
33
  Requires-Dist: shapely
34
34
  Requires-Dist: geopandas>=1.0
35
35
  Provides-Extra: dev
36
- Requires-Dist: black; extra == "dev"
37
- Requires-Dist: bumpver; extra == "dev"
38
- Requires-Dist: isort; extra == "dev"
39
- Requires-Dist: pip-tools; extra == "dev"
40
36
  Requires-Dist: pytest; extra == "dev"
37
+ Requires-Dist: build; extra == "dev"
38
+ Requires-Dist: twine; extra == "dev"
41
39
  Provides-Extra: cli
42
40
  Requires-Dist: matplotlib; extra == "cli"
43
41
  Requires-Dist: cartopy; extra == "cli"
42
+ Provides-Extra: viz
43
+ Requires-Dist: matplotlib; extra == "viz"
44
+ Requires-Dist: holoviews; extra == "viz"
45
+ Requires-Dist: datashader; extra == "viz"
46
+ Requires-Dist: bokeh; extra == "viz"
47
+ Requires-Dist: colorcet; extra == "viz"
48
+ Requires-Dist: panel; extra == "viz"
49
+ Provides-Extra: tutorials
50
+ Requires-Dist: jupyterlab; extra == "tutorials"
51
+ Requires-Dist: matplotlib; extra == "tutorials"
52
+ Requires-Dist: cartopy; extra == "tutorials"
53
+ Requires-Dist: requests; extra == "tutorials"
44
54
  Dynamic: license-file
45
55
 
46
56
  <img src="https://raw.githubusercontent.com/Tristanovsk/GRSdriver/master/docs/source/_static/GRS_driver.png" alt="GRSdriver_icon" width="100"/>
@@ -49,7 +59,10 @@ Dynamic: license-file
49
59
  ## Driver dedicated to load and prepare data for the GRS processor
50
60
 
51
61
  [![PyPI](https://img.shields.io/pypi/v/GRSdriver)](https://pypi.org/project/GRSdriver/)
62
+ [![Python](https://img.shields.io/pypi/pyversions/GRSdriver)](https://pypi.org/project/GRSdriver/)
63
+ [![tests](https://github.com/Tristanovsk/GRSdriver/actions/workflows/tests.yml/badge.svg)](https://github.com/Tristanovsk/GRSdriver/actions/workflows/tests.yml)
52
64
  [![Documentation](https://readthedocs.org/projects/grsdriver/badge/?version=latest)](https://grsdriver.readthedocs.io)
65
+ [![License](https://img.shields.io/pypi/l/GRSdriver)](https://github.com/Tristanovsk/GRSdriver/blob/master/LICENSE)
53
66
 
54
67
  GRSdriver loads a Level-1 satellite image into a single `xarray.Dataset` ready for the
55
68
  [GRS](https://github.com/CNES/GRSprocessor) atmospheric and sunglint correction:
@@ -59,37 +72,49 @@ GRSdriver loads a Level-1 satellite image into a single `xarray.Dataset` ready f
59
72
  - Landsat 8 and 9 Collection-2 L1: reflectance, brightness temperature of the thermal bands,
60
73
  sun/viewing angles and L1 quality flags
61
74
  - spectral response functions and central wavelengths of each band
62
- - spatial subsetting, lazy (dask) loading and low memory options for full tiles
75
+ - spatial subsetting, parallel reading, lazy (dask) loading and low memory options for full tiles
63
76
  - visualization tools
64
77
 
65
78
  Full documentation: https://grsdriver.readthedocs.io
66
79
 
80
+ ## What's new in 2.0
81
+
82
+ - **pip-only install**: EOReader and the GDAL Python bindings are no longer needed.
83
+ - **~2x faster**: full Sentinel-2 tile at 20 m in ~30 s (bands read and angles computed in
84
+ parallel), fully lazy dask mode in ~20 s.
85
+ - **Fixes**: Sentinel-2 angles of processing baselines < 4.0, alignment of the Landsat
86
+ panchromatic band, size of the `SpatioTemp` boxes.
87
+ - **Upgrading from 1.x**: the drivers no longer take `**kwargs` (they were passed to EOReader) and
88
+ have no `.reader` attribute; a `subset` given as a sequence is a bounding box
89
+ `(minx, miny, maxx, maxy)` in the product CRS. See the
90
+ [CHANGELOG](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md).
91
+
67
92
  ## Installation
68
93
 
94
+ Python 3.9 to 3.13:
95
+
69
96
  ```commandline
70
97
  pip install GRSdriver
71
98
  ```
72
99
 
73
100
  No separate GDAL installation is needed: rasterio and geopandas ship with their own GDAL
74
- (including the JPEG2000 driver for Sentinel-2). The `grs_driver` command line (RGB quicklooks)
75
- needs matplotlib and cartopy:
76
- ```commandline
77
- pip install "GRSdriver[cli]"
78
- ```
101
+ (including the JPEG2000 driver for Sentinel-2). Optional extras:
102
+
103
+ | Extra | For | Install |
104
+ |---|---|---|
105
+ | `cli` | `grs_driver` command line (RGB quicklooks) | `pip install "GRSdriver[cli]"` |
106
+ | `viz` | interactive visualization tools (`GRSdriver.visual`) | `pip install "GRSdriver[viz]"` |
107
+ | `tutorials` | running the tutorial notebooks | `pip install "GRSdriver[tutorials]"` |
79
108
 
80
- ### Installation from the repository (to use the notebooks and visualization tools)
109
+ ### From the repository
110
+
111
+ To run the tutorials or work on the code:
81
112
  ```commandline
82
113
  git clone https://github.com/Tristanovsk/GRSdriver.git
83
114
  cd GRSdriver
84
- conda env create -f environment.yml
85
- conda activate grs
86
- pip install -e .
87
- ```
88
-
89
- For the interactive visualization tools and the notebooks:
90
- ```commandline
91
- conda install -c conda-forge jupyterlab holoviews panel datashader cartopy
115
+ pip install -e ".[cli,viz,tutorials,dev]"
92
116
  ```
117
+ or with conda: `conda env create -f environment.yml && conda activate grsdriver`.
93
118
 
94
119
  ## Quick start
95
120
 
@@ -104,6 +129,12 @@ prod = l1c.prod # bands, vza, raa, sza, SRF, wl_true + metadata
104
129
  # Landsat 8/9
105
130
  l1 = LandsatDriver('LC09_L1TP_..._02_T1', resolution=30)
106
131
  l1.load_product()
132
+
133
+ # region of interest: any geometry (GeoSeries, any CRS), here a lon/lat box
134
+ import geopandas as gpd
135
+ from shapely.geometry import box
136
+ roi = gpd.GeoSeries([box(5.0, 43.38, 5.25, 43.58)], crs=4326)
137
+ l1c = Sentinel2Driver('S2B_MSIL1C_..._.SAFE', resolution=20, subset=roi)
107
138
  ```
108
139
 
109
140
  The bands are read by 4 threads in parallel: set `n_workers` (e.g. `n_workers=1`) to trade speed
@@ -174,6 +205,9 @@ Viewing azimuth of each band computed by GRSdriver:
174
205
 
175
206
  ![example files](https://raw.githubusercontent.com/Tristanovsk/GRSdriver/master/docs/source/_static/example_ndwi_mask.png)
176
207
 
177
- ## Changes
208
+ ## Changes and contributing
178
209
 
179
- See [CHANGELOG.md](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md).
210
+ See the [CHANGELOG](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md). Tests run
211
+ with `pytest tests` (the tests that need satellite products are skipped when they are not
212
+ available); the [contributing guide](https://grsdriver.readthedocs.io/en/latest/contributing.html)
213
+ explains how to update the tutorials and make a release.
@@ -4,7 +4,10 @@
4
4
  ## Driver dedicated to load and prepare data for the GRS processor
5
5
 
6
6
  [![PyPI](https://img.shields.io/pypi/v/GRSdriver)](https://pypi.org/project/GRSdriver/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/GRSdriver)](https://pypi.org/project/GRSdriver/)
8
+ [![tests](https://github.com/Tristanovsk/GRSdriver/actions/workflows/tests.yml/badge.svg)](https://github.com/Tristanovsk/GRSdriver/actions/workflows/tests.yml)
7
9
  [![Documentation](https://readthedocs.org/projects/grsdriver/badge/?version=latest)](https://grsdriver.readthedocs.io)
10
+ [![License](https://img.shields.io/pypi/l/GRSdriver)](https://github.com/Tristanovsk/GRSdriver/blob/master/LICENSE)
8
11
 
9
12
  GRSdriver loads a Level-1 satellite image into a single `xarray.Dataset` ready for the
10
13
  [GRS](https://github.com/CNES/GRSprocessor) atmospheric and sunglint correction:
@@ -14,37 +17,49 @@ GRSdriver loads a Level-1 satellite image into a single `xarray.Dataset` ready f
14
17
  - Landsat 8 and 9 Collection-2 L1: reflectance, brightness temperature of the thermal bands,
15
18
  sun/viewing angles and L1 quality flags
16
19
  - spectral response functions and central wavelengths of each band
17
- - spatial subsetting, lazy (dask) loading and low memory options for full tiles
20
+ - spatial subsetting, parallel reading, lazy (dask) loading and low memory options for full tiles
18
21
  - visualization tools
19
22
 
20
23
  Full documentation: https://grsdriver.readthedocs.io
21
24
 
25
+ ## What's new in 2.0
26
+
27
+ - **pip-only install**: EOReader and the GDAL Python bindings are no longer needed.
28
+ - **~2x faster**: full Sentinel-2 tile at 20 m in ~30 s (bands read and angles computed in
29
+ parallel), fully lazy dask mode in ~20 s.
30
+ - **Fixes**: Sentinel-2 angles of processing baselines < 4.0, alignment of the Landsat
31
+ panchromatic band, size of the `SpatioTemp` boxes.
32
+ - **Upgrading from 1.x**: the drivers no longer take `**kwargs` (they were passed to EOReader) and
33
+ have no `.reader` attribute; a `subset` given as a sequence is a bounding box
34
+ `(minx, miny, maxx, maxy)` in the product CRS. See the
35
+ [CHANGELOG](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md).
36
+
22
37
  ## Installation
23
38
 
39
+ Python 3.9 to 3.13:
40
+
24
41
  ```commandline
25
42
  pip install GRSdriver
26
43
  ```
27
44
 
28
45
  No separate GDAL installation is needed: rasterio and geopandas ship with their own GDAL
29
- (including the JPEG2000 driver for Sentinel-2). The `grs_driver` command line (RGB quicklooks)
30
- needs matplotlib and cartopy:
31
- ```commandline
32
- pip install "GRSdriver[cli]"
33
- ```
46
+ (including the JPEG2000 driver for Sentinel-2). Optional extras:
47
+
48
+ | Extra | For | Install |
49
+ |---|---|---|
50
+ | `cli` | `grs_driver` command line (RGB quicklooks) | `pip install "GRSdriver[cli]"` |
51
+ | `viz` | interactive visualization tools (`GRSdriver.visual`) | `pip install "GRSdriver[viz]"` |
52
+ | `tutorials` | running the tutorial notebooks | `pip install "GRSdriver[tutorials]"` |
34
53
 
35
- ### Installation from the repository (to use the notebooks and visualization tools)
54
+ ### From the repository
55
+
56
+ To run the tutorials or work on the code:
36
57
  ```commandline
37
58
  git clone https://github.com/Tristanovsk/GRSdriver.git
38
59
  cd GRSdriver
39
- conda env create -f environment.yml
40
- conda activate grs
41
- pip install -e .
42
- ```
43
-
44
- For the interactive visualization tools and the notebooks:
45
- ```commandline
46
- conda install -c conda-forge jupyterlab holoviews panel datashader cartopy
60
+ pip install -e ".[cli,viz,tutorials,dev]"
47
61
  ```
62
+ or with conda: `conda env create -f environment.yml && conda activate grsdriver`.
48
63
 
49
64
  ## Quick start
50
65
 
@@ -59,6 +74,12 @@ prod = l1c.prod # bands, vza, raa, sza, SRF, wl_true + metadata
59
74
  # Landsat 8/9
60
75
  l1 = LandsatDriver('LC09_L1TP_..._02_T1', resolution=30)
61
76
  l1.load_product()
77
+
78
+ # region of interest: any geometry (GeoSeries, any CRS), here a lon/lat box
79
+ import geopandas as gpd
80
+ from shapely.geometry import box
81
+ roi = gpd.GeoSeries([box(5.0, 43.38, 5.25, 43.58)], crs=4326)
82
+ l1c = Sentinel2Driver('S2B_MSIL1C_..._.SAFE', resolution=20, subset=roi)
62
83
  ```
63
84
 
64
85
  The bands are read by 4 threads in parallel: set `n_workers` (e.g. `n_workers=1`) to trade speed
@@ -129,6 +150,9 @@ Viewing azimuth of each band computed by GRSdriver:
129
150
 
130
151
  ![example files](https://raw.githubusercontent.com/Tristanovsk/GRSdriver/master/docs/source/_static/example_ndwi_mask.png)
131
152
 
132
- ## Changes
153
+ ## Changes and contributing
133
154
 
134
- See [CHANGELOG.md](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md).
155
+ See the [CHANGELOG](https://github.com/Tristanovsk/GRSdriver/blob/master/CHANGELOG.md). Tests run
156
+ with `pytest tests` (the tests that need satellite products are skipped when they are not
157
+ available); the [contributing guide](https://grsdriver.readthedocs.io/en/latest/contributing.html)
158
+ explains how to update the tutorials and make a release.
@@ -46,9 +46,14 @@ version = { attr = "GRSdriver.__version__" }
46
46
  dependencies = { file = ["requirements.txt"] }
47
47
 
48
48
  [project.optional-dependencies]
49
- dev = ["black", "bumpver", "isort", "pip-tools", "pytest"]
49
+ # tests and release (see the contributing guide)
50
+ dev = ["pytest", "build", "twine"]
50
51
  # grs_driver command line (RGB quicklook)
51
52
  cli = ["matplotlib", "cartopy"]
53
+ # interactive visualization tools (GRSdriver.visual)
54
+ viz = ["matplotlib", "holoviews", "datashader", "bokeh", "colorcet", "panel"]
55
+ # to run the tutorial notebooks
56
+ tutorials = ["jupyterlab", "matplotlib", "cartopy", "requests"]
52
57
 
53
58
  [project.urls]
54
59
  Documentation = "https://grsdriver.readthedocs.io"
@@ -94,3 +94,19 @@ def test_n_workers_identical():
94
94
  l1.load_product()
95
95
  prods.append(l1.prod)
96
96
  np.testing.assert_array_equal(prods[0].bands.values, prods[1].bands.values)
97
+
98
+
99
+ @pytest.mark.parametrize('resolution', [30, 60])
100
+ def test_dask_identical(monkeypatch, resolution):
101
+ '''Lazy (dask) product identical to the eager one, float32 angles in both modes.'''
102
+ prods = []
103
+ for lazy in ('0', '1'):
104
+ monkeypatch.setenv('GRSDRIVER_USE_DASK', lazy)
105
+ l1 = LandsatDriver(IMAGE, band_idx=[1, 4, 9], subset=SUBSET, resolution=resolution)
106
+ l1.load_product()
107
+ prods.append(l1.prod.compute())
108
+ eager, lazy = prods
109
+ for var in ('bands', 'sza', 'vza', 'raa'):
110
+ assert eager[var].dtype == lazy[var].dtype == np.float32
111
+ np.testing.assert_array_equal(eager[var].values, lazy[var].values)
112
+ np.testing.assert_array_equal(eager.flags_l1.values, lazy.flags_l1.values)
@@ -34,3 +34,32 @@ def test_clip_raster_size():
34
34
  clipped = SpatioTemp.clip_raster(raster, 43.47, 5.10, 10000)
35
35
  for coord in (clipped.x, clipped.y):
36
36
  assert abs(float(coord.max() - coord.min()) + 100 - 10000) <= 200
37
+
38
+
39
+ @pytest.mark.parametrize('offset', [(0, 0), (3, 5), (-4, -2), (95, 90)])
40
+ def test_read_on_grid_direct(tmp_path, offset):
41
+ '''Target grid aligned on the source grid: direct copy identical to the GDAL resampling,
42
+ with the fill value outside the source.'''
43
+ import rasterio
44
+ from affine import Affine
45
+ from rasterio.enums import Resampling
46
+ from GRSdriver import utils
47
+ src_transform = Affine(30, 0, 600000, 0, -30, 4800000)
48
+ data = np.arange(100 * 100, dtype=np.uint16).reshape(100, 100) + 1
49
+ data[10:20, 30:40] = 0
50
+ path = tmp_path / 'src.tif'
51
+ with rasterio.open(path, 'w', driver='GTiff', width=100, height=100, count=1, dtype='uint16',
52
+ crs='EPSG:32631', transform=src_transform, nodata=0) as dst:
53
+ dst.write(data, 1)
54
+ row, col = offset
55
+ transform = src_transform * Affine.translation(col, row)
56
+ args = (str(path), transform, 20, 30, Resampling.nearest, 0, 'float32')
57
+ direct = utils._read_window(*args)
58
+ try:
59
+ utils._pixel_offset, pixel_offset = (lambda *a: None), utils._pixel_offset
60
+ resampled = utils._read_window(*args)
61
+ finally:
62
+ utils._pixel_offset = pixel_offset
63
+ assert pixel_offset(src_transform, transform) == (row, col)
64
+ assert direct.dtype == resampled.dtype == np.float32
65
+ np.testing.assert_array_equal(direct, resampled)
File without changes
File without changes
File without changes
File without changes