eo-processor 0.4.0__cp312-cp312-manylinux_2_34_x86_64.whl

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.

Potentially problematic release.


This version of eo-processor might be problematic. Click here for more details.

@@ -0,0 +1,379 @@
1
+ """
2
+ High-performance Earth Observation processing library.
3
+
4
+ This library provides Rust-accelerated functions for common EO/geospatial
5
+ computations that can be used within XArray/Dask workflows to bypass Python's GIL.
6
+
7
+ NOTE: All public spectral and temporal functions accept any numeric NumPy dtype
8
+ (int, uint, float32, float64, etc.). Inputs are automatically coerced to float64
9
+ in the Rust layer for consistent and stable computation.
10
+ """
11
+
12
+ from ._core import (
13
+ normalized_difference as _normalized_difference,
14
+ ndvi as _ndvi,
15
+ ndwi as _ndwi,
16
+ savi as _savi,
17
+ nbr as _nbr,
18
+ ndmi as _ndmi,
19
+ nbr2 as _nbr2,
20
+ gci as _gci,
21
+ enhanced_vegetation_index as _enhanced_vegetation_index,
22
+ median as _median,
23
+ temporal_mean as _temporal_mean,
24
+ temporal_std as _temporal_std,
25
+ euclidean_distance as _euclidean_distance,
26
+ manhattan_distance as _manhattan_distance,
27
+ chebyshev_distance as _chebyshev_distance,
28
+ minkowski_distance as _minkowski_distance,
29
+ delta_ndvi as _delta_ndvi,
30
+ delta_nbr as _delta_nbr,
31
+ )
32
+
33
+ __version__ = "0.3.0"
34
+
35
+ __all__ = [
36
+ "normalized_difference",
37
+ "ndvi",
38
+ "ndwi",
39
+ "savi",
40
+ "nbr",
41
+ "ndmi",
42
+ "nbr2",
43
+ "gci",
44
+ "enhanced_vegetation_index",
45
+ "evi",
46
+ "delta_ndvi",
47
+ "delta_nbr",
48
+ "median",
49
+ "composite",
50
+ "temporal_mean",
51
+ "temporal_std",
52
+ "euclidean_distance",
53
+ "manhattan_distance",
54
+ "chebyshev_distance",
55
+ "minkowski_distance",
56
+ ]
57
+
58
+
59
+ def normalized_difference(a, b):
60
+ """
61
+ Compute normalized difference (a - b) / (a + b) using the Rust core.
62
+ Supports 1D or 2D numpy float arrays; dimensional dispatch occurs in Rust.
63
+ """
64
+ return _normalized_difference(a, b)
65
+
66
+
67
+ def ndvi(nir, red):
68
+ """
69
+ Compute NDVI = (NIR - Red) / (NIR + Red) via Rust core (1D or 2D).
70
+ """
71
+ return _ndvi(nir, red)
72
+
73
+
74
+ def ndwi(green, nir):
75
+ """
76
+ Compute NDWI = (Green - NIR) / (Green + NIR) via Rust core (1D or 2D).
77
+ """
78
+ return _ndwi(green, nir)
79
+
80
+
81
+ def savi(nir, red, L=0.5, **kwargs):
82
+ """
83
+ Compute Soil Adjusted Vegetation Index (SAVI).
84
+
85
+ SAVI = (NIR - Red) / (NIR + Red + L) * (1 + L)
86
+
87
+ Parameters
88
+ ----------
89
+ nir : numpy.ndarray
90
+ Near-infrared band.
91
+ red : numpy.ndarray
92
+ Red band.
93
+ L : float, optional
94
+ Soil brightness correction factor (default 0.5). Typical range 0–1.
95
+ Larger L reduces soil background influence.
96
+ **kwargs :
97
+ May contain 'l' to specify the soil adjustment factor instead of 'L'.
98
+
99
+ Returns
100
+ -------
101
+ numpy.ndarray
102
+ SAVI values with same shape as inputs.
103
+
104
+ Notes
105
+ -----
106
+ You can call as:
107
+ savi(nir, red) # uses L=0.5
108
+ savi(nir, red, L=0.25) # custom L
109
+ savi(nir, red, l=0.25) # alternative keyword
110
+ If both L and l are provided, 'l' takes precedence.
111
+ """
112
+ l_val = kwargs.get("l", L)
113
+ return _savi(nir, red, l_val)
114
+
115
+
116
+ def ndmi(nir, swir1):
117
+ """
118
+ Normalized Difference Moisture Index (NDMI)
119
+
120
+ NDMI = (NIR - SWIR1) / (NIR + SWIR1)
121
+
122
+ Parameters
123
+ ----------
124
+ nir : numpy.ndarray
125
+ Near-infrared band.
126
+ swir1 : numpy.ndarray
127
+ Short-wave infrared 1 band.
128
+
129
+ Returns
130
+ -------
131
+ numpy.ndarray
132
+ NDMI values (-1 .. 1).
133
+ """
134
+ return _ndmi(nir, swir1)
135
+
136
+
137
+ def nbr2(swir1, swir2):
138
+ """
139
+ Normalized Burn Ratio 2 (NBR2)
140
+
141
+ NBR2 = (SWIR1 - SWIR2) / (SWIR1 + SWIR2)
142
+
143
+ Parameters
144
+ ----------
145
+ swir1 : numpy.ndarray
146
+ Short-wave infrared 1 band.
147
+ swir2 : numpy.ndarray
148
+ Short-wave infrared 2 band.
149
+
150
+ Returns
151
+ -------
152
+ numpy.ndarray
153
+ NBR2 values (-1 .. 1).
154
+ """
155
+ return _nbr2(swir1, swir2)
156
+
157
+
158
+ def gci(nir, green):
159
+ """
160
+ Green Chlorophyll Index (GCI)
161
+
162
+ GCI = (NIR / Green) - 1
163
+
164
+ Parameters
165
+ ----------
166
+ nir : numpy.ndarray
167
+ Near-infrared band (any numeric dtype; auto-coerced to float64).
168
+ green : numpy.ndarray
169
+ Green band (any numeric dtype).
170
+
171
+ Returns
172
+ -------
173
+ numpy.ndarray
174
+ GCI values (unbounded; typical vegetation > 0).
175
+
176
+ Notes
177
+ -----
178
+ Division-by-near-zero guarded; returns 0 where Green is ~0.
179
+ """
180
+ return _gci(nir, green)
181
+
182
+
183
+ def delta_ndvi(pre_nir, pre_red, post_nir, post_red):
184
+ """
185
+ Change in NDVI (pre - post).
186
+
187
+ Parameters
188
+ ----------
189
+ pre_nir, pre_red : numpy.ndarray
190
+ Pre-event near-infrared and red bands.
191
+ post_nir, post_red : numpy.ndarray
192
+ Post-event near-infrared and red bands.
193
+
194
+ Returns
195
+ -------
196
+ numpy.ndarray
197
+ ΔNDVI array (same shape as inputs), positive values often indicate vegetation loss.
198
+
199
+ Notes
200
+ -----
201
+ Inputs may be any numeric dtype; values are coerced to float64 internally.
202
+ """
203
+ return _delta_ndvi(pre_nir, pre_red, post_nir, post_red)
204
+
205
+
206
+ def delta_nbr(pre_nir, pre_swir2, post_nir, post_swir2):
207
+ """
208
+ Change in NBR (pre - post) for burn severity analysis.
209
+
210
+ Parameters
211
+ ----------
212
+ pre_nir, pre_swir2 : numpy.ndarray
213
+ Pre-event NIR and SWIR2 bands.
214
+ post_nir, post_swir2 : numpy.ndarray
215
+ Post-event NIR and SWIR2 bands.
216
+
217
+ Returns
218
+ -------
219
+ numpy.ndarray
220
+ ΔNBR array (same shape as inputs). Larger positive values generally indicate higher burn severity.
221
+
222
+ Notes
223
+ -----
224
+ Inputs may be any numeric dtype; internal coercion to float64.
225
+ """
226
+ return _delta_nbr(pre_nir, pre_swir2, post_nir, post_swir2)
227
+
228
+
229
+ def nbr(nir, swir2):
230
+ """
231
+ Compute Normalized Burn Ratio (NBR).
232
+
233
+ NBR = (NIR - SWIR2) / (NIR + SWIR2)
234
+
235
+ Parameters
236
+ ----------
237
+ nir : numpy.ndarray
238
+ Near-infrared band.
239
+ swir2 : numpy.ndarray
240
+ Short-wave infrared (SWIR2) band.
241
+
242
+ Returns
243
+ -------
244
+ numpy.ndarray
245
+ NBR values with same shape as inputs.
246
+ """
247
+ return _nbr(nir, swir2)
248
+
249
+
250
+ def enhanced_vegetation_index(nir, red, blue):
251
+ """
252
+ Compute EVI = 2.5 * (NIR - Red) / (NIR + 6*Red - 7.5*Blue + 1) via Rust core (1D or 2D).
253
+ """
254
+ return _enhanced_vegetation_index(nir, red, blue)
255
+
256
+
257
+ # Alias
258
+ evi = enhanced_vegetation_index
259
+
260
+
261
+ def median(arr, skip_na=True):
262
+ """
263
+ Compute median over the time axis of a 1D, 2D, 3D, or 4D array.
264
+
265
+ Parameters
266
+ ----------
267
+ arr : numpy.ndarray
268
+ Input array.
269
+ skip_na : bool, optional
270
+ Whether to skip NaN values, by default True. If False, the median
271
+ of any pixel containing a NaN will be NaN.
272
+ """
273
+ return _median(arr, skip_na=skip_na)
274
+
275
+
276
+ def composite(arr, method="median", **kwargs):
277
+ """
278
+ Compute a composite over the time axis of a 1D, 2D, 3D, or 4D array.
279
+
280
+ Parameters
281
+ ----------
282
+ arr : numpy.ndarray
283
+ Input array.
284
+ method : str, optional
285
+ The compositing method to use, by default "median".
286
+ **kwargs
287
+ Additional keyword arguments to pass to the compositing function.
288
+ """
289
+ if method == "median":
290
+ return median(arr, **kwargs)
291
+ else:
292
+ raise ValueError(f"Unknown composite method: {method}")
293
+
294
+ def temporal_mean(arr, skip_na=True):
295
+ """
296
+ Compute mean over the time axis of a 1D, 2D, 3D, or 4D array.
297
+ Parameters
298
+ ----------
299
+ arr : numpy.ndarray
300
+ Input array.
301
+ skip_na : bool, optional
302
+ Whether to skip NaN values, by default True. If False, the mean
303
+ of any pixel containing a NaN will be NaN.
304
+ """
305
+ return _temporal_mean(arr, skip_na=skip_na)
306
+
307
+
308
+ def temporal_std(arr, skip_na=True):
309
+ """
310
+ Compute standard deviation over the time axis of a 1D, 2D, 3D, or 4D array.
311
+ Parameters
312
+ ----------
313
+ arr : numpy.ndarray
314
+ Input array.
315
+ skip_na : bool, optional
316
+ Whether to skip NaN values, by default True. If False, the std
317
+ of any pixel containing a NaN will be NaN.
318
+ """
319
+ return _temporal_std(arr, skip_na=skip_na)
320
+
321
+
322
+ def euclidean_distance(points_a, points_b):
323
+ """
324
+ Compute pairwise Euclidean distances between two point sets.
325
+
326
+ Parameters
327
+ ----------
328
+ points_a : numpy.ndarray (N, D)
329
+ points_b : numpy.ndarray (M, D)
330
+
331
+ Returns
332
+ -------
333
+ numpy.ndarray (N, M)
334
+ Distance matrix where element (i, j) is distance between
335
+ points_a[i] and points_b[j].
336
+ """
337
+ return _euclidean_distance(points_a, points_b)
338
+
339
+
340
+ def manhattan_distance(points_a, points_b):
341
+ """
342
+ Compute pairwise Manhattan (L1) distances between two point sets.
343
+ See `euclidean_distance` for shape conventions.
344
+ """
345
+ return _manhattan_distance(points_a, points_b)
346
+
347
+
348
+ def chebyshev_distance(points_a, points_b):
349
+ """
350
+ Compute pairwise Chebyshev (L∞) distances between two point sets.
351
+ """
352
+ return _chebyshev_distance(points_a, points_b)
353
+
354
+
355
+ def minkowski_distance(points_a, points_b, p):
356
+ """
357
+ Compute pairwise Minkowski distances (order `p`) between two point sets.
358
+
359
+ Parameters
360
+ ----------
361
+ points_a : numpy.ndarray (N, D)
362
+ First point set.
363
+ points_b : numpy.ndarray (M, D)
364
+ Second point set.
365
+ p : float
366
+ Norm order (must be >= 1). p=1 → Manhattan, p=2 → Euclidean,
367
+ large p → approximates Chebyshev (L∞).
368
+
369
+ Returns
370
+ -------
371
+ numpy.ndarray (N, M)
372
+ Distance matrix.
373
+
374
+ Raises
375
+ ------
376
+ ValueError
377
+ If p < 1.0 (propagated from the Rust implementation).
378
+ """
379
+ return _minkowski_distance(points_a, points_b, p)
@@ -0,0 +1,60 @@
1
+ """Type stubs for eo_processor"""
2
+
3
+ from typing import Literal
4
+ import numpy as np
5
+ from numpy.typing import NDArray
6
+
7
+ # Inputs accept any numeric dtype; implementation coerces to float64 internally.
8
+ NumericArray = NDArray[np.generic]
9
+
10
+ __version__: Literal["0.3.0"]
11
+
12
+ def normalized_difference(
13
+ a: NumericArray, b: NumericArray
14
+ ) -> NDArray[np.float64]: ...
15
+ def ndvi(nir: NumericArray, red: NumericArray) -> NDArray[np.float64]: ...
16
+ def ndwi(
17
+ green: NumericArray, nir: NumericArray
18
+ ) -> NDArray[np.float64]: ...
19
+ def savi(
20
+ nir: NumericArray, red: NumericArray, L: float = ...
21
+ ) -> NDArray[np.float64]: ...
22
+ def nbr(
23
+ nir: NumericArray, swir2: NumericArray
24
+ ) -> NDArray[np.float64]: ...
25
+ def ndmi(
26
+ nir: NumericArray, swir1: NumericArray
27
+ ) -> NDArray[np.float64]: ...
28
+ def nbr2(
29
+ swir1: NumericArray, swir2: NumericArray
30
+ ) -> NDArray[np.float64]: ...
31
+ def gci(
32
+ nir: NumericArray, green: NumericArray
33
+ ) -> NDArray[np.float64]: ...
34
+ def delta_ndvi(
35
+ pre_nir: NumericArray,
36
+ pre_red: NumericArray,
37
+ post_nir: NumericArray,
38
+ post_red: NumericArray,
39
+ ) -> NDArray[np.float64]: ...
40
+ def delta_nbr(
41
+ pre_nir: NumericArray,
42
+ pre_swir2: NumericArray,
43
+ post_nir: NumericArray,
44
+ post_swir2: NumericArray,
45
+ ) -> NDArray[np.float64]: ...
46
+ def enhanced_vegetation_index(
47
+ nir: NumericArray, red: NumericArray, blue: NumericArray
48
+ ) -> NDArray[np.float64]: ...
49
+ evi = enhanced_vegetation_index
50
+
51
+ def median(arr: NumericArray, skip_na: bool = ...) -> NDArray[np.float64]: ...
52
+ def composite(arr: NumericArray, method: str = ..., **kwargs) -> NDArray[np.float64]: ...
53
+ def temporal_mean(arr: NumericArray, skip_na: bool = ...) -> NDArray[np.float64]: ...
54
+ def temporal_std(arr: NumericArray, skip_na: bool = ...) -> NDArray[np.float64]: ...
55
+
56
+ def euclidean_distance(points_a: NumericArray, points_b: NumericArray) -> NDArray[np.float64]: ...
57
+ def manhattan_distance(points_a: NumericArray, points_b: NumericArray) -> NDArray[np.float64]: ...
58
+ def chebyshev_distance(points_a: NumericArray, points_b: NumericArray) -> NDArray[np.float64]: ...
59
+ def minkowski_distance(points_a: NumericArray, points_b: NumericArray, p: float) -> NDArray[np.float64]: ...
60
+ # Raises ValueError if p < 1.0
@@ -0,0 +1,542 @@
1
+ Metadata-Version: 2.4
2
+ Name: eo-processor
3
+ Version: 0.4.0
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: 3.8
7
+ Classifier: Programming Language :: Python :: 3.9
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Scientific/Engineering :: GIS
13
+ Requires-Dist: maturin>=1.9.6
14
+ Requires-Dist: numpy>=1.20.0
15
+ Requires-Dist: tox>=4.25.0
16
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
17
+ Requires-Dist: maturin>=1.0,<2.0 ; extra == 'dev'
18
+ Requires-Dist: dask[array]>=2023.0.0 ; extra == 'dask'
19
+ Requires-Dist: xarray>=2023.0.0 ; extra == 'dask'
20
+ Requires-Dist: pyarrow>=17.0.0 ; extra == 'dask'
21
+ Provides-Extra: dev
22
+ Provides-Extra: dask
23
+ License-File: LICENSE
24
+ Summary: High-performance Rust UDFs for Earth Observation processing
25
+ Keywords: earth-observation,gis,remote-sensing,ndvi,dask,xarray,rust,pyo3,numpy
26
+ Author: Ben Smith
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
29
+
30
+ # eo-processor
31
+ [![PyPI Version](https://img.shields.io/pypi/v/eo-processor.svg?color=blue)](https://pypi.org/project/eo-processor/)
32
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/eo-processor?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/eo-processor)
33
+ [![Coverage](./coverage-badge.svg)](#test-coverage)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
35
+
36
+ High-performance Rust UDFs for Earth Observation (EO) processing with Python bindings.
37
+ Provides fast spectral indices, temporal statistics, and (internally) spatial distance utilities.
38
+
39
+ ---
40
+
41
+ ## Overview
42
+
43
+ `eo-processor` accelerates common Earth Observation and geospatial computations using Rust + PyO3, exposing a Python API compatible with NumPy, XArray, and Dask. Rust execution bypasses Python's Global Interpreter Lock (GIL), enabling true parallelism in multi-core environments and large-array workflows.
44
+
45
+ Primary focus areas:
46
+ - Spectral indices (NDVI, NDWI, EVI, generic normalized differences)
47
+ - Temporal compositing/statistics (median, mean, standard deviation)
48
+ - Spatial utilities (distance computations — currently available via internal module)
49
+
50
+ ---
51
+
52
+ ## Key Features
53
+
54
+ - Rust-accelerated numerical kernels (safe, no `unsafe` code)
55
+ - Automatic dimensional dispatch (1D vs 2D for spectral indices)
56
+ - Temporal statistics across a leading “time” axis for 1D–4D arrays
57
+ - Optional skipping of NaN values (`skip_na=True`)
58
+ - Ready for XArray / Dask parallelized workflows
59
+ - Type hints and stubs for IDE assistance
60
+ - Deterministic, GIL-efficient performance
61
+
62
+ ---
63
+
64
+ ## Installation
65
+
66
+ ### Using `pip` (PyPI)
67
+
68
+ ```bash
69
+ pip install eo-processor
70
+ ```
71
+
72
+ Optional extras (for distributed / parallel array workflows):
73
+
74
+ ```bash
75
+ pip install eo-processor[dask]
76
+ ```
77
+
78
+ ### Using `uv` (fast dependency manager)
79
+
80
+ ```bash
81
+ # Create and sync environment
82
+ uv venv
83
+ source .venv/bin/activate
84
+ uv pip install eo-processor
85
+ ```
86
+
87
+ ### From Source
88
+
89
+ Requirements:
90
+ - Python 3.8+
91
+ - Rust toolchain (install via https://rustup.rs/)
92
+ - `maturin` for building the extension
93
+
94
+ ```bash
95
+ git clone https://github.com/BnJam/eo-processor.git
96
+ cd eo-processor
97
+
98
+ # Build & install in editable (development) mode
99
+ pip install maturin
100
+ maturin develop --release
101
+
102
+ # Or build a wheel
103
+ maturin build --release
104
+ pip install target/wheels/*.whl
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Quick Start
110
+
111
+ ```python
112
+ import numpy as np
113
+ from eo_processor import ndvi, ndwi, evi, normalized_difference
114
+
115
+ nir = np.array([0.8, 0.7, 0.6])
116
+ red = np.array([0.2, 0.1, 0.3])
117
+ blue = np.array([0.1, 0.05, 0.08])
118
+ green = np.array([0.35, 0.42, 0.55])
119
+
120
+ ndvi_vals = ndvi(nir, red)
121
+ ndwi_vals = ndwi(green, nir)
122
+ evi_vals = evi(nir, red, blue)
123
+ nd_generic = normalized_difference(nir, red)
124
+
125
+ print(ndvi_vals, ndwi_vals, evi_vals, nd_generic)
126
+ ```
127
+
128
+ All spectral index functions return NumPy arrays directly (no tuple wrappers).
129
+
130
+ ---
131
+
132
+ ## API Summary
133
+
134
+ Top-level Python exports (via `eo_processor`):
135
+
136
+ | Function | Description |
137
+ |----------|-------------|
138
+ | `normalized_difference(a, b)` | Generic `(a - b) / (a + b)` with zero-denominator safeguard |
139
+ | `ndvi(nir, red)` | Normalized Difference Vegetation Index |
140
+ | `ndwi(green, nir)` | Normalized Difference Water Index |
141
+ | `enhanced_vegetation_index(nir, red, blue)` / `evi(...)` | Enhanced Vegetation Index (EVI: G*(NIR-Red)/(NIR + C1*Red - C2*Blue + L)) |
142
+ | `median(arr, skip_na=True)` | Temporal median across leading axis for 1D–4D arrays |
143
+ | `composite(arr, method="median", **kwargs)` | Convenience wrapper (currently only median) |
144
+ | `temporal_mean(arr, skip_na=True)` | Mean across time dimension |
145
+ | `temporal_std(arr, skip_na=True)` | Sample standard deviation (n-1 denominator) across time |
146
+ | `savi(nir, red, L=0.5)` | Soil Adjusted Vegetation Index: (NIR - Red)/(NIR + Red + L) * (1 + L); variable L (≥ 0) |
147
+ | `nbr(nir, swir2)` | Normalized Burn Ratio: (NIR - SWIR2)/(NIR + SWIR2) |
148
+ | `ndmi(nir, swir1)` | Normalized Difference Moisture Index: (NIR - SWIR1)/(NIR + SWIR1) |
149
+ | `nbr2(swir1, swir2)` | Normalized Burn Ratio 2: (SWIR1 - SWIR2)/(SWIR1 + SWIR2) |
150
+ | `gci(nir, green)` | Green Chlorophyll Index: (NIR / Green) - 1 (division guarded) |
151
+ | `delta_ndvi(pre_nir, pre_red, post_nir, post_red)` | Change in NDVI (pre - post); vegetation loss (positive values often indicate decrease in post-event NDVI) |
152
+ | `delta_nbr(pre_nir, pre_swir2, post_nir, post_swir2)` | Change in NBR (pre - post); burn severity (higher positive change suggests more severe burn) |
153
+
154
+ Spatial distance functions (pairwise distance matrices; now exported at the top level — note O(N*M) memory/time for large point sets). Formulas (a, b ∈ ℝ^D).
155
+ All spectral/temporal index functions accept any numeric NumPy dtype (int, uint, float32, float64, etc.); inputs are automatically coerced to float64 internally for consistency:
156
+ - Euclidean: √(∑ᵢ (aᵢ - bᵢ)²)
157
+ - Manhattan (L₁): ∑ᵢ |aᵢ - bᵢ|
158
+ - Chebyshev (L_∞): maxᵢ |aᵢ - bᵢ|
159
+ - Minkowski (L_p): (∑ᵢ |aᵢ - bᵢ|^p)^(1/p), with p ≥ 1.0 (this library enforces p ≥ 1)
160
+
161
+
162
+ | Function | Description |
163
+ |----------|-------------|
164
+ | `euclidean_distance(points_a, points_b)` | Pairwise Euclidean distances (shape (N,M)) |
165
+ | `manhattan_distance(points_a, points_b)` | Pairwise L1 distance |
166
+ | `chebyshev_distance(points_a, points_b)` | Pairwise max-abs (L∞) distance |
167
+ | `minkowski_distance(points_a, points_b, p)` | Pairwise L^p distance |
168
+ | (Median helpers for dimension dispatch) | Implementations backing `median` |
169
+
170
+ If you need spatial distance functions at the top level, add them to `python/eo_processor/__init__.py` and re-export.
171
+
172
+ ---
173
+
174
+ ## Spectral Indices
175
+
176
+ ### NDVI
177
+ Formula: `(NIR - Red) / (NIR + Red)`
178
+ Typical interpretation:
179
+ - Water / snow: < 0 (often strongly negative for clear water)
180
+ - Bare soil / built surfaces: ~ 0.0 – 0.2
181
+ - Sparse vegetation / stressed crops: 0.2 – 0.5
182
+ - Healthy dense vegetation: > 0.5 (tropical forest can exceed 0.7)
183
+
184
+ ### NDWI
185
+ Formula: `(Green - NIR) / (Green + NIR)`
186
+ Typical interpretation:
187
+ - Open water bodies: > 0.3 (often 0.4–0.6)
188
+ - Moist vegetation / wetlands: 0.0 – 0.3
189
+ - Dry vegetation / bare soil: < 0.0 (negative values)
190
+
191
+ ### EVI
192
+ Formula:
193
+ `EVI = G * (NIR - Red) / (NIR + C1 * Red - C2 * Blue + L)`
194
+ Constants (MODIS): `G=2.5, C1=6.0, C2=7.5, L=1.0`
195
+ Typical interpretation:
196
+ - EVI dampens soil & atmospheric effects relative to NDVI
197
+ - Moderate vegetation: ~0.2 – 0.4
198
+ - Dense / healthy canopy: >0.4 (can reach ~0.8 in lush tropical zones)
199
+ - Very low / senescent vegetation: <0.2
200
+
201
+ ### SAVI
202
+ Formula:
203
+ `SAVI = (NIR - Red) / (NIR + Red + L) * (1 + L)`
204
+ Typical soil factor `L=0.5`; recommended range 0–1. Higher L reduces soil background effects. Implementation supports variable `L` (must be ≥ 0).
205
+ Interpretation (similar to NDVI but more robust over bright soil):
206
+ - Bare / bright soil: ~0.0 – 0.2
207
+ - Moderate vegetation: 0.2 – 0.5
208
+ - Healthy dense vegetation: > 0.5
209
+ Use smaller L (e.g. 0.25) for dense vegetation, larger L (~1.0) for very sparse vegetation / bright soil conditions.
210
+
211
+ ### NBR
212
+ Formula:
213
+ `NBR = (NIR - SWIR2) / (NIR + SWIR2)`
214
+ Used for burn severity and post-fire change detection.
215
+ Typical interpretation (pre-fire vs post-fire):
216
+ - Healthy vegetation (pre-fire): high positive (≈0.4 – 0.7)
217
+ - Recently burned areas: strong drop; post-fire NBR often < 0.1 or negative
218
+ Change analysis often uses ΔNBR (pre - post). Common burn severity thresholds (example ranges, refine per study):
219
+ - ΔNBR > 0.66: High severity
220
+ - 0.44 – 0.66: Moderate-high
221
+ - 0.27 – 0.44: Moderate-low
222
+ - 0.1 – 0.27: Low severity
223
+ - < 0.1: Unburned / noise
224
+
225
+ ### NDMI
226
+ Formula:
227
+ `NDMI = (NIR - SWIR1) / (NIR + SWIR1)`
228
+ Moisture / canopy water content indicator.
229
+ Typical interpretation:
230
+ - High positive (>0.3): Moist / healthy canopy (leaf water content high)
231
+ - Near zero (0.0 – 0.3): Moderate moisture / possible stress onset
232
+ - Negative (<0.0): Dry vegetation / senescence / possible drought stress
233
+
234
+ ### NBR2
235
+ Formula:
236
+ `NBR2 = (SWIR1 - SWIR2) / (SWIR1 + SWIR2)`
237
+ Highlights burn severity and subtle thermal / moisture differences.
238
+ Typical interpretation:
239
+ - Lower values: Increased moisture / less burn impact
240
+ - Higher values: Greater dryness / potential higher burn severity
241
+ Use in tandem with NBR or NDMI for refined burn severity or moisture discrimination.
242
+
243
+ ### GCI
244
+ Formula:
245
+ `GCI = (NIR / Green) - 1`
246
+ Green Chlorophyll Index; division by near-zero Green values is guarded to return 0.
247
+ Typical interpretation:
248
+ - Values > 0 indicate chlorophyll presence
249
+ - 0 – 2: Sparse to moderate chlorophyll (grassland, early growth)
250
+ - 2 – 8: Higher chlorophyll density (crops peak growth, healthy canopy)
251
+ - > 8: Very dense chlorophyll (may indicate saturation; verify sensor & calibration)
252
+ Absolute ranges vary with sensor, atmospheric correction, and reflectance scaling—use relative comparisons or time-series trends.
253
+
254
+ All indices auto-dispatch between 1D and 2D input arrays; shapes must match.
255
+
256
+ ### Change Detection Indices
257
+
258
+ Change detection indices operate on “pre” and “post” event imagery (e.g., before vs after fire, storm, harvest):
259
+
260
+ Formulae:
261
+ `ΔNDVI = NDVI(pre) - NDVI(post)`
262
+ `ΔNBR = NBR(pre) - NBR(post)`
263
+
264
+ Typical interpretation:
265
+ - Positive ΔNDVI: vegetation loss / canopy degradation.
266
+ - Near-zero ΔNDVI: minimal change.
267
+ - Positive ΔNBR: higher burn severity (consult study-specific threshold tables).
268
+ - Use masks (cloud, snow, shadow) to set unreliable pre/post pixels to NaN before computing deltas.
269
+
270
+ These delta indices also accept any numeric dtype; values are coerced to float64.
271
+
272
+ ### CLI Usage
273
+
274
+ A command-line helper is available (`scripts/eo_cli.py`) to batch compute indices from .npy band files:
275
+
276
+ Single index:
277
+ ```
278
+ python scripts/eo_cli.py --index ndvi --nir data/nir.npy --red data/red.npy --out outputs/ndvi.npy
279
+ ```
280
+
281
+ Multiple indices:
282
+ ```
283
+ python scripts/eo_cli.py --index ndvi savi ndmi nbr --nir data/nir.npy --red data/red.npy --swir1 data/swir1.npy --swir2 data/swir2.npy --out-dir outputs/
284
+ ```
285
+
286
+ Change detection:
287
+ ```
288
+ python scripts/eo_cli.py --index delta_nbr \
289
+ --pre-nir pre/nir.npy --pre-swir2 pre/swir2.npy \
290
+ --post-nir post/nir.npy --post-swir2 post/swir2.npy \
291
+ --out outputs/delta_nbr.npy
292
+ ```
293
+
294
+ Cloud mask (0=cloud, 1=clear):
295
+ ```
296
+ python scripts/eo_cli.py --index ndvi --nir data/nir.npy --red data/red.npy --mask data/cloudmask.npy --out outputs/ndvi_masked.npy
297
+ ```
298
+
299
+ PNG preview:
300
+ ```
301
+ python scripts/eo_cli.py --index ndvi --nir data/nir.npy --red data/red.npy --out outputs/ndvi.npy --png-preview outputs/ndvi.png
302
+ ```
303
+
304
+ Use `--savi-l` to adjust soil factor for SAVI; use `--clamp MIN MAX` to restrict output range before saving; `--allow-missing` skips indices lacking required bands.
305
+
306
+ ---
307
+
308
+ ## Temporal Statistics & Compositing
309
+
310
+ Temporal functions assume the first axis is “time”:
311
+
312
+ - 1D: `(time,)`
313
+ - 2D: `(time, band)`
314
+ - 3D: `(time, y, x)`
315
+ - 4D: `(time, band, y, x)`
316
+
317
+ Example (temporal mean of a stack):
318
+
319
+ ```python
320
+ import numpy as np
321
+ from eo_processor import temporal_mean, temporal_std
322
+
323
+ # Simulate (time, y, x) stack: 10 timesteps of 256x256
324
+ cube = np.random.rand(10, 256, 256)
325
+ mean_image = temporal_mean(cube) # shape (256, 256)
326
+ std_image = temporal_std(cube) # shape (256, 256)
327
+ ```
328
+
329
+ Median compositing:
330
+
331
+ ```python
332
+ from eo_processor import median, composite
333
+ median_image = median(cube) # same as composite(cube, method="median")
334
+ ```
335
+
336
+ Skip NaNs:
337
+
338
+ ```python
339
+ cloudy_series = np.array([[0.2, np.nan, 0.5],
340
+ [0.25, 0.3, 0.45],
341
+ [0.22, np.nan, 0.47]]) # (time, band)
342
+ clean_mean = temporal_mean(cloudy_series, skip_na=True) # ignores NaNs
343
+ strict_mean = temporal_mean(cloudy_series, skip_na=False) # bands with NaN → NaN
344
+ ```
345
+
346
+ ---
347
+
348
+ ## Spatial Distances (Internal)
349
+
350
+ Currently available in the Rust core module:
351
+
352
+ ```python
353
+ from eo_processor import _core
354
+
355
+ import numpy as np
356
+ points_a = np.array([[0.0, 0.0],
357
+ [1.0, 1.0]])
358
+ points_b = np.array([[1.0, 0.0],
359
+ [0.0, 1.0]])
360
+
361
+ dist_euclid = _core.euclidean_distance(points_a, points_b)
362
+ dist_manhat = _core.manhattan_distance(points_a, points_b)
363
+ dist_cheby = _core.chebyshev_distance(points_a, points_b)
364
+ dist_mink = _core.minkowski_distance(points_a, points_b, 3.0)
365
+ ```
366
+
367
+ Each returns an `(N, M)` array of pairwise distances.
368
+ Note: These perform O(N*M) computations; for very large sets consider spatial indexing approaches (not yet implemented here).
369
+
370
+ ---
371
+
372
+ ## XArray / Dask Integration
373
+
374
+ ```python
375
+ import dask.array as da
376
+ import xarray as xr
377
+ from eo_processor import ndvi
378
+
379
+ nir_dask = da.random.random((5000, 5000), chunks=(500, 500))
380
+ red_dask = da.random.random((5000, 5000), chunks=(500, 500))
381
+
382
+ nir_xr = xr.DataArray(nir_dask, dims=["y", "x"])
383
+ red_xr = xr.DataArray(red_dask, dims=["y", "x"])
384
+
385
+ ndvi_da = xr.apply_ufunc(
386
+ ndvi,
387
+ nir_xr,
388
+ red_xr,
389
+ dask="parallelized",
390
+ output_dtypes=[float],
391
+ )
392
+
393
+ result = ndvi_da.compute()
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Performance
399
+
400
+ Rust implementations avoid Python-loop overhead and release the GIL. Example benchmark (single-thread baseline):
401
+
402
+ ```python
403
+ import numpy as np, time
404
+ from eo_processor import ndvi
405
+
406
+ nir = np.random.rand(5000, 5000)
407
+ red = np.random.rand(5000, 5000)
408
+
409
+ t0 = time.time()
410
+ rust_out = ndvi(nir, red)
411
+ t_rust = time.time() - t0
412
+
413
+ t0 = time.time()
414
+ numpy_out = (nir - red) / (nir + red)
415
+ t_numpy = time.time() - t0
416
+
417
+ print(f"Rust: {t_rust:.4f}s NumPy: {t_numpy:.4f}s Speedup: {t_numpy/t_rust:.2f}x")
418
+ ```
419
+
420
+ Observed speedups vary by platform and array size. Always benchmark in your environment.
421
+
422
+ ---
423
+
424
+ ## Test Coverage
425
+
426
+ The badge above is generated from `coverage.xml` via `scripts/generate_coverage_badge.py`.
427
+ To regenerate after test changes:
428
+
429
+ ```bash
430
+ tox -e coverage
431
+ python scripts/generate_coverage_badge.py coverage.xml coverage-badge.svg
432
+ ```
433
+
434
+ ---
435
+
436
+ ## Contributing
437
+
438
+ See `CONTRIBUTING.md` and `AGENTS.md` for guidelines (workflows, security posture, and pre-commit checklist).
439
+ Typical steps:
440
+
441
+ ```bash
442
+ cargo fmt
443
+ cargo clippy --all-targets -- -D warnings
444
+ pytest
445
+ tox -e coverage
446
+ ```
447
+
448
+ Add new Rust functions → export via `#[pyfunction]` → register in `src/lib.rs` → expose in `python/eo_processor/__init__.py` → add type stubs → add tests → update README.
449
+
450
+ ---
451
+
452
+ ## Roadmap (Indicative)
453
+
454
+ - Additional spectral indices (SAVI, NBR, GCI)
455
+ - Sliding window / neighborhood stats
456
+ - Direct distance exports at top-level
457
+ - Distributed temporal
458
+ composites (chunk-aware)
459
+ - Optional GPU acceleration feasibility study
460
+
461
+ ---
462
+
463
+ ## Scientific Citation
464
+
465
+ ```bibtex
466
+ @software{eo_processor,
467
+ title = {eo-processor: High-performance Rust UDFs for Earth Observation},
468
+ author = {Ben Smith},
469
+ year = {2025},
470
+ url = {https://github.com/BnJam/eo-processor}
471
+ }
472
+ ```
473
+
474
+ ---
475
+
476
+ ## License
477
+
478
+ MIT License. See `LICENSE`.
479
+
480
+ ---
481
+
482
+ ## Disclaimer
483
+
484
+ This library focuses on computational primitives; it does not handle:
485
+ - Cloud masking
486
+ - Sensor-specific calibration
487
+ - CRS reprojection
488
+ - I/O of remote datasets
489
+
490
+ Combine with domain tools (e.g., rasterio, xarray, dask-geopandas) for complete EO pipelines.
491
+
492
+ ---
493
+
494
+ ## Support
495
+
496
+ Open issues for bugs or enhancements. Feature proposals with benchmarks and EO relevance are welcome.
497
+
498
+ ---
499
+
500
+ ## Benchmark Harness
501
+
502
+ A minimal benchmarking harness is provided at `scripts/benchmark.py` to measure performance of spectral, temporal, and spatial distance functions. The spectral group currently includes: ndvi, ndwi, evi, savi, nbr, ndmi, nbr2, gci, and normalized_difference.
503
+
504
+ Basic usage (spectral functions on a 2048x2048 image):
505
+ ```bash
506
+ python scripts/benchmark.py --group spectral --height 2048 --width 2048
507
+ ```
508
+
509
+ Compare Rust vs pure NumPy baselines (supported for spectral & temporal functions):
510
+ ```bash
511
+ python scripts/benchmark.py --group temporal --time 24 --height 1024 --width 1024 --compare-numpy
512
+ ```
513
+
514
+ Distance benchmarks (pairwise matrix; O(N*M)):
515
+ ```bash
516
+ python scripts/benchmark.py --group distances --points-a 2000 --points-b 2000 --point-dim 8
517
+ ```
518
+
519
+ Write JSON and Markdown reports:
520
+ ```bash
521
+ python scripts/benchmark.py --group all --compare-numpy --json-out bench.json --md-out bench.md
522
+ ```
523
+
524
+ Key options:
525
+ - `--group {spectral|temporal|distances|all}` selects predefined function sets.
526
+ - `--functions <names...>` overrides group selection with explicit functions.
527
+ - `--compare-numpy` enables baseline timing (speedup > 1.0 indicates Rust faster).
528
+ - `--loops / --warmups` control timing repetitions.
529
+ - `--json-out` writes structured results (including baseline metrics when enabled).
530
+ - `--md-out` writes a Markdown table suitable for PRs / reports.
531
+ - `--quiet` suppresses console table output (still writes artifacts).
532
+ - `--minkowski-p` sets the norm order for Minkowski distance (must be ≥ 1.0).
533
+
534
+ Example Markdown table excerpt (columns):
535
+ | Function | Mean (ms) | StDev (ms) | Min (ms) | Max (ms) | Elements | Throughput (M elems/s) | Speedup vs NumPy | Shape |
536
+
537
+ > Speedup vs NumPy = (NumPy mean time / Rust mean time); values > 1 indicate Rust is faster.
538
+
539
+ ---
540
+
541
+ Happy processing!
542
+
@@ -0,0 +1,7 @@
1
+ eo_processor-0.4.0.dist-info/METADATA,sha256=cbTkHyIVfJru2XwnSBEsqGu62OmCgK1xyM4a8DENgyA,18048
2
+ eo_processor-0.4.0.dist-info/WHEEL,sha256=6SMSE7o6zBbAY_fbEHyYtLCyWUTWMuzw9qWPgdJzl40,109
3
+ eo_processor-0.4.0.dist-info/licenses/LICENSE,sha256=g2ZF7Vny_FKpyYrev_KBpkOoO_k0h9-CtEBiASi2Jug,1073
4
+ eo_processor/__init__.py,sha256=N7P9wgpP0V4EbUH2PA9MSauMM51UjJSJdafzL-0YyFM,9291
5
+ eo_processor/__init__.pyi,sha256=SLlar9GCPUNDgwV7G_yNdIPi67UN_lG9P3xIu2DURnw,2192
6
+ eo_processor/_core.cpython-312-x86_64-linux-gnu.so,sha256=naf_C0hPgDhxdIctSziyibgAWAULvvWs64QfHsX93RY,707072
7
+ eo_processor-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.10.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-manylinux_2_34_x86_64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Benjamin J Smith
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.