pysentinel2 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Borevitz Lab, Australian National University, and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.4
2
+ Name: pysentinel2
3
+ Version: 0.1.0
4
+ Summary: Cached Sentinel-2 ARD cube downloads (STAC to Zarr) with cloud masking
5
+ Author: Borevitz Lab, Australian National University
6
+ Author-email: Yasar Adeel Ansari <u6737670@anu.edu.au>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/thestochasticman/pysentinel2
9
+ Project-URL: Repository, https://github.com/thestochasticman/pysentinel2
10
+ Project-URL: Issues, https://github.com/thestochasticman/pysentinel2/issues
11
+ Keywords: sentinel-2,remote-sensing,stac,zarr,geospatial,earth-observation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: GIS
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: attrs
24
+ Requires-Dist: typing_extensions
25
+ Requires-Dist: numpy
26
+ Requires-Dist: xarray
27
+ Requires-Dist: zarr
28
+ Requires-Dist: dask
29
+ Requires-Dist: pyproj
30
+ Requires-Dist: affine
31
+ Requires-Dist: odc-stac
32
+ Requires-Dist: odc-geo
33
+ Requires-Dist: pystac
34
+ Requires-Dist: pystac-client
35
+ Requires-Dist: urllib3
36
+ Requires-Dist: opencv-python
37
+ Requires-Dist: rioxarray
38
+ Dynamic: license-file
39
+
40
+ # pysentinel2
41
+
42
+ A **local Sentinel-2 datacube that fills itself on demand**. Every pixel
43
+ this machine ever downloads lands in one sparse, pixel-indexed store —
44
+ so nothing is ever downloaded twice: overlapping areas, extended date
45
+ ranges and repeat runs all reuse the same chunks. Part of the
46
+ [Borevitz Lab](https://borevitzlab.anu.edu.au/) ecosystem; the default
47
+ source is [Digital Earth Australia](https://explorer.dea.ga.gov.au/)'s
48
+ ARD collections (`ga_s2am_ard_3` / `ga_s2bm_ard_3`) via STAC.
49
+
50
+ Full documentation — architecture, grid geometry, storage, cleaning,
51
+ indices, robustness — is in [`docs/`](docs/README.md), with flowcharts
52
+ and figures generated from a real store.
53
+
54
+ ![Every stored solar day for the example window](docs/images/cube_frames_rgb.png)
55
+ *Contents of the store for a 2 × 2 km example window. Clear, cloudy and
56
+ off-swath days are all stored raw and classified at read time.*
57
+
58
+ ## How it works
59
+
60
+ ```
61
+ {data_root}/sentinel2_cube/
62
+ ├── index.db # SQLite: coverage rects · seen scenes · past searches
63
+ └── cube.zarr/
64
+ ├── 2024-01-03/ # one group per solar day
65
+ │ ├── nbart_red # arrays on a fixed EPSG:6933 10 m global grid
66
+ │ └── ... # sparse: only written 256×256-px chunks exist on disk
67
+ └── 2024-01-08/ ...
68
+ ```
69
+
70
+ - Any bbox maps deterministically to a pixel window on the fixed grid.
71
+ `Cube.get_ds(bbox, start, end)` subtracts each day's recorded coverage
72
+ rectangles from that window and downloads **only the missing pixels** —
73
+ coverage accounting is pixel-exact, so small farms pay no chunk padding
74
+ (256×256-px chunks remain the *storage* unit inside the Zarr arrays).
75
+ - STAC results are cached (full item JSON) in the index, so re-reads and
76
+ re-fills of known regions work without re-searching. Cloud-cover
77
+ filtering happens at read time from the index — relaxing the threshold
78
+ later needs no re-search.
79
+ - Only raw bands (incl. fmask) are stored. `get_ds(..., clean=True)` applies
80
+ cloud masking **on read** — there is no second "clean" copy on disk,
81
+ roughly halving storage versus a raw+clean layout. See
82
+ [Cleaning & masking](#cleaning--masking) for exactly what the mask does.
83
+ - Spectral indices — NDVI, CFI, NIRv, NDTI, CAI — are on-read
84
+ derivatives too: `get_ds(..., indices=('NDVI', 'NIRv'))` computes them
85
+ from cloud-masked reflectance and stores nothing.
86
+ - Writes are whole-chunk and the index is transactional (SQLite/WAL): a
87
+ crash mid-fill just leaves cells unmarked, and the next run resumes.
88
+
89
+ ## Usage
90
+
91
+ The core API is **troi-agnostic** — just a bbox and dates, no setup:
92
+
93
+ ```python
94
+ from datetime import date
95
+ from pysentinel2.cube import Cube
96
+
97
+ cube = Cube()
98
+ bbox = [148.36265, -33.52606, 148.38265, -33.50606] # [W, S, E, N]
99
+
100
+ ds_raw = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31))
101
+ ds = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31), clean=True)
102
+ ds = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31),
103
+ indices=('NDVI', 'CFI', 'NIRv', 'NDTI', 'CAI'))
104
+
105
+ cube.fill(bbox, date(2024, 1, 1), date(2024, 12, 31)) # → 0: already local
106
+ ```
107
+
108
+ Pipelines that speak the shared `troi.troi.Troi` (the
109
+ reproducibility layer — stubs, registry) use the adapters:
110
+
111
+ ```python
112
+ ds = cube.get_ds_troi(troi) # = cube.get_ds(troi.bbox, troi.start, troi.end)
113
+ ```
114
+
115
+ `download_sentinel2(troi)` and `clean_sentinel2(troi)` remain as thin
116
+ wrappers over `Cube.get_ds_troi` for pipeline compatibility.
117
+
118
+ Package design (shared across the lab's packages — no inheritance,
119
+ composition only):
120
+
121
+ - **`Troi`** (from `troi`) — identity: what region, what dates.
122
+ - **`Sentinel2`** (`pysentinel2.sentinel2`) — config: STAC URL,
123
+ collections, bands, CRS, cloud threshold, fmask codes.
124
+ - **`Paths`** (`pysentinel2.paths`) — derived locations of the store for
125
+ a given `Config`.
126
+ - **`grid`** — the fixed global grid (pure, offline-testable math).
127
+ - **`Index`** (`pysentinel2.index`) — the SQLite ledger.
128
+ - **`Cube`** (`pysentinel2.cube`) — ties them together.
129
+
130
+ ## Cleaning & masking
131
+
132
+ `clean=True` (and any `indices=` request, which implies it) runs the
133
+ window through `pysentinel2.cube.clean_dataset`. The design principle:
134
+ **invalid and contaminated are different things.**
135
+
136
+ | Pixel state | fmask | Meaning | Treatment |
137
+ |---|---|---|---|
138
+ | Invalid | 0 (nodata) | Outside the scene footprint / never sensed | → NaN; counts *against coverage*, not against cloudiness |
139
+ | Clear | 1 | Usable land observation | kept |
140
+ | Cloud | 2 | Contaminated | → NaN (dilated) |
141
+ | Shadow | 3 | Contaminated | → NaN (dilated) |
142
+ | Snow | 4 | Surface state; corrupts vegetation statistics | → NaN by default (`mask_snow=False` to keep); never counts toward the frame gate |
143
+ | Water | 5 | Legitimate signal (NDWI, dams, rivers) | kept by default (`mask_water=True` to drop) |
144
+
145
+ Contaminated pixels are dilated before masking, frames are gated on the
146
+ two fractions independently, and every read is annotated with the
147
+ statistics and thresholds that produced it. Nothing is persisted —
148
+ different thresholds on the same window are just different reads of the
149
+ same raw store. Full pipeline, tunables and figures:
150
+ [docs/cleaning.md](docs/cleaning.md).
151
+
152
+ ## Performance
153
+
154
+ Live measurements against DEA — a ~2 × 2 km AOI, 11-band ARD at 10 m
155
+ (one *cell* = one 256 × 256-px chunk on one solar day):
156
+
157
+ | Scenario | Downloaded | Time |
158
+ |---|---|---|
159
+ | Cold fill — 3 weeks (3 clear scenes) | 12 cells | 5.7 s |
160
+ | Same request again | nothing | **0.0 s** |
161
+ | AOI shifted 1 km (inside cached chunks) | nothing | **0.0 s** |
162
+ | Date range extended +1 month | 32 cells — *new days only* | 17.2 s |
163
+ | Read cached window (512² px × 3 days × 11 bands) | — | 0.13 s |
164
+ | Read cached window, cloud-masked (`clean=True`) | — | 0.23 s |
165
+
166
+ Store footprint: **13.6 MB for 11 solar days** — raw + fmask only, since
167
+ the clean cube is a 0.1 s on-read transform rather than a second copy.
168
+
169
+ Absolute times vary with network and DEA load. The zero rows are the
170
+ significant ones: those requests are resolved by index lookups alone,
171
+ with no network access.
172
+
173
+ Multi-year fills are batched, not per-day: all 11 bands for up to 64
174
+ missing days come down in one bulk load per batch (see
175
+ [the fill algorithm](docs/architecture.md#the-fill-algorithm)), keeping
176
+ the I/O threads saturated across day boundaries — a two-month cold fill
177
+ measured 20-26 s where a per-day loop measured 33 s on a healthy DEA
178
+ and 270 s on a degraded one, and the gap widens with the length of the
179
+ range. An earlier fmask-first screening pass was removed after
180
+ measurement: it skipped 8.8% of days' reflectance while paying an extra
181
+ request round on every day.
182
+
183
+ ## Install
184
+
185
+ ### pip
186
+
187
+ ```bash
188
+ pip install git+https://github.com/thestochasticman/pysentinel2.git
189
+ ```
190
+
191
+ Dependencies (the `troi` core included, pulled from GitHub) are
192
+ declared in `pyproject.toml` and installed automatically.
193
+
194
+ ### From source
195
+
196
+ ```bash
197
+ git clone https://github.com/thestochasticman/pysentinel2.git
198
+ cd pysentinel2
199
+ pip install -e .
200
+ ```
201
+
202
+ The wheels for `rasterio`/`rioxarray`/`opencv` bundle their native
203
+ libraries on common platforms; in a conda environment the conda-forge
204
+ equivalents are used instead if already installed.
205
+
206
+ ## Robustness notes
207
+
208
+ Hardening for DEA's public S3 + STAC quirks (cold-start 504s, stalled
209
+ reads, corrupt tiles) is built in — see
210
+ [docs/robustness.md](docs/robustness.md) and, for the underlying
211
+ investigations, [`diagnostics.md`](diagnostics.md).
212
+
213
+ ## Test
214
+
215
+ ```bash
216
+ # offline (pure math + synthetic store):
217
+ python pysentinel2/grid.py # True
218
+ python pysentinel2/index.py # True
219
+ python pysentinel2/paths.py # True
220
+ python pysentinel2/cube.py # True
221
+
222
+ # live (small real downloads from DEA, incl. dedup assertions):
223
+ python pysentinel2/download_sentinel2.py # True
224
+ python pysentinel2/clean_sentinel2.py # True
225
+ ```
@@ -0,0 +1,186 @@
1
+ # pysentinel2
2
+
3
+ A **local Sentinel-2 datacube that fills itself on demand**. Every pixel
4
+ this machine ever downloads lands in one sparse, pixel-indexed store —
5
+ so nothing is ever downloaded twice: overlapping areas, extended date
6
+ ranges and repeat runs all reuse the same chunks. Part of the
7
+ [Borevitz Lab](https://borevitzlab.anu.edu.au/) ecosystem; the default
8
+ source is [Digital Earth Australia](https://explorer.dea.ga.gov.au/)'s
9
+ ARD collections (`ga_s2am_ard_3` / `ga_s2bm_ard_3`) via STAC.
10
+
11
+ Full documentation — architecture, grid geometry, storage, cleaning,
12
+ indices, robustness — is in [`docs/`](docs/README.md), with flowcharts
13
+ and figures generated from a real store.
14
+
15
+ ![Every stored solar day for the example window](docs/images/cube_frames_rgb.png)
16
+ *Contents of the store for a 2 × 2 km example window. Clear, cloudy and
17
+ off-swath days are all stored raw and classified at read time.*
18
+
19
+ ## How it works
20
+
21
+ ```
22
+ {data_root}/sentinel2_cube/
23
+ ├── index.db # SQLite: coverage rects · seen scenes · past searches
24
+ └── cube.zarr/
25
+ ├── 2024-01-03/ # one group per solar day
26
+ │ ├── nbart_red # arrays on a fixed EPSG:6933 10 m global grid
27
+ │ └── ... # sparse: only written 256×256-px chunks exist on disk
28
+ └── 2024-01-08/ ...
29
+ ```
30
+
31
+ - Any bbox maps deterministically to a pixel window on the fixed grid.
32
+ `Cube.get_ds(bbox, start, end)` subtracts each day's recorded coverage
33
+ rectangles from that window and downloads **only the missing pixels** —
34
+ coverage accounting is pixel-exact, so small farms pay no chunk padding
35
+ (256×256-px chunks remain the *storage* unit inside the Zarr arrays).
36
+ - STAC results are cached (full item JSON) in the index, so re-reads and
37
+ re-fills of known regions work without re-searching. Cloud-cover
38
+ filtering happens at read time from the index — relaxing the threshold
39
+ later needs no re-search.
40
+ - Only raw bands (incl. fmask) are stored. `get_ds(..., clean=True)` applies
41
+ cloud masking **on read** — there is no second "clean" copy on disk,
42
+ roughly halving storage versus a raw+clean layout. See
43
+ [Cleaning & masking](#cleaning--masking) for exactly what the mask does.
44
+ - Spectral indices — NDVI, CFI, NIRv, NDTI, CAI — are on-read
45
+ derivatives too: `get_ds(..., indices=('NDVI', 'NIRv'))` computes them
46
+ from cloud-masked reflectance and stores nothing.
47
+ - Writes are whole-chunk and the index is transactional (SQLite/WAL): a
48
+ crash mid-fill just leaves cells unmarked, and the next run resumes.
49
+
50
+ ## Usage
51
+
52
+ The core API is **troi-agnostic** — just a bbox and dates, no setup:
53
+
54
+ ```python
55
+ from datetime import date
56
+ from pysentinel2.cube import Cube
57
+
58
+ cube = Cube()
59
+ bbox = [148.36265, -33.52606, 148.38265, -33.50606] # [W, S, E, N]
60
+
61
+ ds_raw = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31))
62
+ ds = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31), clean=True)
63
+ ds = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31),
64
+ indices=('NDVI', 'CFI', 'NIRv', 'NDTI', 'CAI'))
65
+
66
+ cube.fill(bbox, date(2024, 1, 1), date(2024, 12, 31)) # → 0: already local
67
+ ```
68
+
69
+ Pipelines that speak the shared `troi.troi.Troi` (the
70
+ reproducibility layer — stubs, registry) use the adapters:
71
+
72
+ ```python
73
+ ds = cube.get_ds_troi(troi) # = cube.get_ds(troi.bbox, troi.start, troi.end)
74
+ ```
75
+
76
+ `download_sentinel2(troi)` and `clean_sentinel2(troi)` remain as thin
77
+ wrappers over `Cube.get_ds_troi` for pipeline compatibility.
78
+
79
+ Package design (shared across the lab's packages — no inheritance,
80
+ composition only):
81
+
82
+ - **`Troi`** (from `troi`) — identity: what region, what dates.
83
+ - **`Sentinel2`** (`pysentinel2.sentinel2`) — config: STAC URL,
84
+ collections, bands, CRS, cloud threshold, fmask codes.
85
+ - **`Paths`** (`pysentinel2.paths`) — derived locations of the store for
86
+ a given `Config`.
87
+ - **`grid`** — the fixed global grid (pure, offline-testable math).
88
+ - **`Index`** (`pysentinel2.index`) — the SQLite ledger.
89
+ - **`Cube`** (`pysentinel2.cube`) — ties them together.
90
+
91
+ ## Cleaning & masking
92
+
93
+ `clean=True` (and any `indices=` request, which implies it) runs the
94
+ window through `pysentinel2.cube.clean_dataset`. The design principle:
95
+ **invalid and contaminated are different things.**
96
+
97
+ | Pixel state | fmask | Meaning | Treatment |
98
+ |---|---|---|---|
99
+ | Invalid | 0 (nodata) | Outside the scene footprint / never sensed | → NaN; counts *against coverage*, not against cloudiness |
100
+ | Clear | 1 | Usable land observation | kept |
101
+ | Cloud | 2 | Contaminated | → NaN (dilated) |
102
+ | Shadow | 3 | Contaminated | → NaN (dilated) |
103
+ | Snow | 4 | Surface state; corrupts vegetation statistics | → NaN by default (`mask_snow=False` to keep); never counts toward the frame gate |
104
+ | Water | 5 | Legitimate signal (NDWI, dams, rivers) | kept by default (`mask_water=True` to drop) |
105
+
106
+ Contaminated pixels are dilated before masking, frames are gated on the
107
+ two fractions independently, and every read is annotated with the
108
+ statistics and thresholds that produced it. Nothing is persisted —
109
+ different thresholds on the same window are just different reads of the
110
+ same raw store. Full pipeline, tunables and figures:
111
+ [docs/cleaning.md](docs/cleaning.md).
112
+
113
+ ## Performance
114
+
115
+ Live measurements against DEA — a ~2 × 2 km AOI, 11-band ARD at 10 m
116
+ (one *cell* = one 256 × 256-px chunk on one solar day):
117
+
118
+ | Scenario | Downloaded | Time |
119
+ |---|---|---|
120
+ | Cold fill — 3 weeks (3 clear scenes) | 12 cells | 5.7 s |
121
+ | Same request again | nothing | **0.0 s** |
122
+ | AOI shifted 1 km (inside cached chunks) | nothing | **0.0 s** |
123
+ | Date range extended +1 month | 32 cells — *new days only* | 17.2 s |
124
+ | Read cached window (512² px × 3 days × 11 bands) | — | 0.13 s |
125
+ | Read cached window, cloud-masked (`clean=True`) | — | 0.23 s |
126
+
127
+ Store footprint: **13.6 MB for 11 solar days** — raw + fmask only, since
128
+ the clean cube is a 0.1 s on-read transform rather than a second copy.
129
+
130
+ Absolute times vary with network and DEA load. The zero rows are the
131
+ significant ones: those requests are resolved by index lookups alone,
132
+ with no network access.
133
+
134
+ Multi-year fills are batched, not per-day: all 11 bands for up to 64
135
+ missing days come down in one bulk load per batch (see
136
+ [the fill algorithm](docs/architecture.md#the-fill-algorithm)), keeping
137
+ the I/O threads saturated across day boundaries — a two-month cold fill
138
+ measured 20-26 s where a per-day loop measured 33 s on a healthy DEA
139
+ and 270 s on a degraded one, and the gap widens with the length of the
140
+ range. An earlier fmask-first screening pass was removed after
141
+ measurement: it skipped 8.8% of days' reflectance while paying an extra
142
+ request round on every day.
143
+
144
+ ## Install
145
+
146
+ ### pip
147
+
148
+ ```bash
149
+ pip install git+https://github.com/thestochasticman/pysentinel2.git
150
+ ```
151
+
152
+ Dependencies (the `troi` core included, pulled from GitHub) are
153
+ declared in `pyproject.toml` and installed automatically.
154
+
155
+ ### From source
156
+
157
+ ```bash
158
+ git clone https://github.com/thestochasticman/pysentinel2.git
159
+ cd pysentinel2
160
+ pip install -e .
161
+ ```
162
+
163
+ The wheels for `rasterio`/`rioxarray`/`opencv` bundle their native
164
+ libraries on common platforms; in a conda environment the conda-forge
165
+ equivalents are used instead if already installed.
166
+
167
+ ## Robustness notes
168
+
169
+ Hardening for DEA's public S3 + STAC quirks (cold-start 504s, stalled
170
+ reads, corrupt tiles) is built in — see
171
+ [docs/robustness.md](docs/robustness.md) and, for the underlying
172
+ investigations, [`diagnostics.md`](diagnostics.md).
173
+
174
+ ## Test
175
+
176
+ ```bash
177
+ # offline (pure math + synthetic store):
178
+ python pysentinel2/grid.py # True
179
+ python pysentinel2/index.py # True
180
+ python pysentinel2/paths.py # True
181
+ python pysentinel2/cube.py # True
182
+
183
+ # live (small real downloads from DEA, incl. dedup assertions):
184
+ python pysentinel2/download_sentinel2.py # True
185
+ python pysentinel2/clean_sentinel2.py # True
186
+ ```
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pysentinel2"
7
+ version = "0.1.0"
8
+ description = "Cached Sentinel-2 ARD cube downloads (STAC to Zarr) with cloud masking"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Borevitz Lab, Australian National University" },
12
+ { name = "Yasar Adeel Ansari", email = "u6737670@anu.edu.au" },
13
+ ]
14
+ license = { text = "MIT" }
15
+ requires-python = ">=3.11"
16
+ keywords = [
17
+ "sentinel-2",
18
+ "remote-sensing",
19
+ "stac",
20
+ "zarr",
21
+ "geospatial",
22
+ "earth-observation",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Science/Research",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Operating System :: OS Independent",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ "Topic :: Scientific/Engineering :: GIS",
33
+ ]
34
+ # The geospatial stack (GDAL/rasterio, odc-stac, dask, zarr, …) is
35
+ # provided by conda via environment.yml; only the lab core is declared
36
+ # here so `pip install -e .` is fast and doesn't re-resolve it.
37
+ dependencies = [
38
+ "attrs",
39
+ "typing_extensions",
40
+ "numpy",
41
+ "xarray",
42
+ "zarr",
43
+ "dask",
44
+ "pyproj",
45
+ "affine",
46
+ "odc-stac",
47
+ "odc-geo",
48
+ "pystac",
49
+ "pystac-client",
50
+ "urllib3",
51
+ "opencv-python",
52
+ "rioxarray",
53
+ ]
54
+
55
+ [project.urls]
56
+ Homepage = "https://github.com/thestochasticman/pysentinel2"
57
+ Repository = "https://github.com/thestochasticman/pysentinel2"
58
+ Issues = "https://github.com/thestochasticman/pysentinel2/issues"
59
+
60
+ [tool.setuptools.packages.find]
61
+ include = ["pysentinel2", "pysentinel2.*"]
@@ -0,0 +1,11 @@
1
+ # Light-weight exports only: pysentinel2.cube (and the download/clean
2
+ # wrappers) pull in the heavy geospatial stack (odc.stac, rioxarray, zarr),
3
+ # so those stay behind explicit submodule imports.
4
+ from pysentinel2.paths import Paths
5
+ from pysentinel2.sentinel2 import Sentinel2, defaultsentinel2
6
+
7
+ __all__ = [
8
+ 'Paths',
9
+ 'Sentinel2',
10
+ 'defaultsentinel2',
11
+ ]
@@ -0,0 +1,73 @@
1
+ """Cloud-masked Sentinel-2 window for a troi — computed on read.
2
+
3
+ Thin compatibility wrapper over :func:`pysentinel2.cube.Cube.get_ds` with
4
+ ``clean=True``. No "clean" copy is ever persisted: masking a window is
5
+ cheap, so the clean cube is a view of the raw store rather than a
6
+ second store.
7
+ """
8
+
9
+ from xarray import Dataset
10
+ from troi import Troi
11
+ from pysentinel2.sentinel2 import Sentinel2, defaultsentinel2
12
+
13
+
14
+ def clean_sentinel2(
15
+ troi: Troi,
16
+ ds_sentinel2: Dataset | None = None,
17
+ sentinel2: Sentinel2 = defaultsentinel2,
18
+ **clean_kwargs,
19
+ ) -> Dataset:
20
+ """Produce a cloud-masked, frame-filtered Sentinel-2 window.
21
+
22
+ Args:
23
+ troi: The :class:`troi.Troi`. Pixels come from the
24
+ shared cube (downloading only what's missing).
25
+ ds_sentinel2: Optional in-memory raw dataset (must still include
26
+ the fmask band); if given, it is masked directly and the cube
27
+ is not touched.
28
+ sentinel2: Config supplying the fmask band and class codes.
29
+ Defaults to the bundled DEA config.
30
+ **clean_kwargs: Forwarded to
31
+ :func:`pysentinel2.cube.clean_dataset` —
32
+ ``max_cloud_fraction``, ``min_valid_fraction``, ``mask_snow``,
33
+ ``mask_water``, ``buffer_px``.
34
+
35
+ Returns:
36
+ xarray.Dataset: The cleaned window, fmask band removed, only the
37
+ retained timesteps, with per-frame ``cloud_fraction`` /
38
+ ``valid_fraction`` coordinates.
39
+ """
40
+ from pysentinel2.cube import Cube, clean_dataset
41
+ if ds_sentinel2 is not None:
42
+ return clean_dataset(ds_sentinel2, sentinel2, **clean_kwargs)
43
+ cube = Cube(config=troi.config, sentinel2=sentinel2)
44
+ return cube.get_ds_troi(troi, clean=True, **clean_kwargs)
45
+
46
+
47
+ def test_clean_drops_fmask_band():
48
+ """The fmask band must not appear in the cleaned dataset (live)."""
49
+ import tempfile
50
+ from datetime import date
51
+ from troi import Config
52
+
53
+ tmpdir = tempfile.mkdtemp(prefix='pysentinel2_clean_test_')
54
+ cfg = Config(out_dir=tmpdir, tmp_dir=tmpdir)
55
+ q = Troi(
56
+ bbox=[148.36265, -33.52606, 148.38265, -33.50606],
57
+ start=date(2024, 1, 1), end=date(2024, 1, 21),
58
+ stub='clean_no_fmask', config=cfg,
59
+ )
60
+ ds = clean_sentinel2(q, max_cloud_fraction=0.7)
61
+ return defaultsentinel2.cloud_mask_band not in ds.data_vars and ds.time.size > 0
62
+
63
+
64
+ def test():
65
+ from pysentinel2.download_sentinel2 import test_internet
66
+ return all([
67
+ test_internet(None),
68
+ test_clean_drops_fmask_band(),
69
+ ])
70
+
71
+
72
+ if __name__ == '__main__':
73
+ print(test())