adjeff 1.2.2__tar.gz → 1.2.5__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.
- {adjeff-1.2.2 → adjeff-1.2.5}/CHANGELOG.md +34 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/PKG-INFO +1 -1
- {adjeff-1.2.2 → adjeff-1.2.5}/docs/guide.md +61 -16
- {adjeff-1.2.2 → adjeff-1.2.5}/pyproject.toml +1 -1
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/loaders/maja_loader.py +1 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/loaders/product_loader.py +9 -3
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/_brdf_sampler.py +1 -5
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/rho_toa.py +1 -1
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/rho_toa_sym.py +1 -1
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/scene_module.py +4 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/sweep_sampler.py +32 -10
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/reference/wu2024.py +0 -1
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_atmo_samplers.py +60 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_loaders.py +48 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/.gitattributes +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/.github/workflows/ci.yml +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/.github/workflows/publish.yml +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/.gitignore +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/.pre-commit-config.yaml +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/LICENSE +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/README.md +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/docs/plan-observabilite-0.13.0.md +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/docs/plan-smartg-2.0-0.15.0.md +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/docs/revue-simplification-2026-08.md +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/docs/roadmap.md +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/notebooks/01-create-and-display-image.ipynb +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/notebooks/02-atmospheric-configuration.ipynb +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/notebooks/03-compute-radiative-quantities.ipynb +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/notebooks/04-simulate-rho-toa.ipynb +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/notebooks/05-learn-psf.ipynb +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/notebooks/06-evaluate-a-psf.ipynb +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/pixi.lock +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/_logging.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/accessor.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/analysis/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/analysis/_energy.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/analysis/_error.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/analysis/_plane.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/analysis/_radial.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/analysis/_vectorise.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/api/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/api/_config.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/api/_load.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/api/_pipeline.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/api/_psf.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/atmosphere/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/atmosphere/atmo_config.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/atmosphere/atmo_factory.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/atmosphere/geo_config.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/atmosphere/spectral_config.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/atmosphere/surface.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/core/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/core/_psf.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/core/analytical_psf.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/core/bands.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/core/image_dict.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/core/image_generator.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/core/non_analytical_psf.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/core/psf_tree.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/exceptions.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/classic/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/classic/toa_to_unif.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/classic/unif_to_toa.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/loaders/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/models/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/models/psf_conv_module.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/models/unif2surface.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/pipeline.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/_atmo_sampler.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/_ensure.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/_smartg.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/radiatives.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/rho_atm.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/sph_alb.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/sph_alb_brdf.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/tdif_down.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/tdif_up.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/tdif_up_brdf.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/tdir_down.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/samplers/tdir_up.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/modules/scene_source.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/_combo_stage.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/_config.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/adam_optimizer.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/fit.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/landscape.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/lbfgs_optimizer.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/loss.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/metrics.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/optim/training_set.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/py.typed +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/reference/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/utils/__init__.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/utils/_config.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/utils/cache_store.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/utils/convolve.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/utils/radial.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/utils/smartgutils.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/utils/torchutils.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/src/adjeff/utils/xrutils.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/_test_module.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/conftest.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_accessor.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_analysis.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_api_docs.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_api_psf.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_atmosphere_config.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_brdf_samplers.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_classic_modules.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_config.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_convolve.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_image_dict.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_image_generator.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_import.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_integration.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_landscape.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_logging.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_optim.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_psf.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_psf_tree.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_radial.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_scene_module.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_smartg_api.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_smartg_defaults.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_smartgutils.py +0 -0
- {adjeff-1.2.2 → adjeff-1.2.5}/tests/test_xrutils.py +0 -0
|
@@ -5,6 +5,40 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [1.2.5]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A stray scalar coordinate no longer overwrites a swept axis.** A
|
|
13
|
+
sweep's dropped coordinates were collected from every coordinate an
|
|
14
|
+
input array carried, not just the ones indexing its own dimensions.
|
|
15
|
+
A field read back from a scene that already holds per-wavelength
|
|
16
|
+
results, e.g. for `fit_psf`'s own forward pass, inherits a scalar
|
|
17
|
+
`wl` tag from that scene; merged in, it silently replaced the real,
|
|
18
|
+
one-row-per-band `wl` axis, and the next lookup by wavelength raised
|
|
19
|
+
`ValueError: dimension 'wl' already exists as a scalar variable`.
|
|
20
|
+
- **A loader's atmosphere and geometry now survive a cache hit.**
|
|
21
|
+
`ProductLoader` declared a dynamic `output_vars` property to aggregate
|
|
22
|
+
what its mixins contribute, but `SceneModule`'s caching reads
|
|
23
|
+
`_output_vars`, a different name it never set; a cache write therefore
|
|
24
|
+
persisted nothing, and the next read restored an empty scene. The first
|
|
25
|
+
run of a script looked fine, since `_compute` writes straight into the
|
|
26
|
+
scene; only a second run, or any read of a pre-existing cache, raised
|
|
27
|
+
`MissingVariableError` on `aot`, `h`, `vza` and the rest.
|
|
28
|
+
|
|
29
|
+
## [1.2.3]
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **A radiative sweep over maps keeps the scene's coordinates.** xsweep
|
|
34
|
+
labels by position the dims it does not sweep, so a scene whose `aot`
|
|
35
|
+
and `h` are maps got its terms aligned on those labels: every pixel
|
|
36
|
+
came back NaN but the handful whose index matched one. `load_maja(...,
|
|
37
|
+
as_map=True, compute_radiatives=True)` could not produce anything.
|
|
38
|
+
- **A sweep restores its own coordinates.** A sampler used to do it
|
|
39
|
+
itself, in its own `_compute`, which is what the fix above forgot in
|
|
40
|
+
one of five places. There is now one place.
|
|
41
|
+
|
|
8
42
|
## [1.2.2]
|
|
9
43
|
|
|
10
44
|
### Fixed
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
3. [Atmosphere and geometry](#3-atmosphere-and-geometry)
|
|
6
6
|
4. [Scene modules and pipelines](#4-scene-modules-and-pipelines)
|
|
7
7
|
5. [Radiative samplers](#5-radiative-samplers)
|
|
8
|
-
6. [
|
|
9
|
-
7. [
|
|
10
|
-
8. [
|
|
11
|
-
9. [
|
|
12
|
-
10. [
|
|
13
|
-
11. [
|
|
14
|
-
12. [
|
|
15
|
-
13. [
|
|
8
|
+
6. [Writing a sampler](#6-writing-a-sampler)
|
|
9
|
+
7. [PSF models](#7-psf-models)
|
|
10
|
+
8. [Correction](#8-correction)
|
|
11
|
+
9. [Fitting a PSF](#9-fitting-a-psf)
|
|
12
|
+
10. [High-level API](#10-high-level-api)
|
|
13
|
+
11. [Analysis and the `.adjeff` accessor](#11-analysis-and-the-adjeff-accessor)
|
|
14
|
+
12. [Cache](#12-cache)
|
|
15
|
+
13. [Logging](#13-logging)
|
|
16
|
+
14. [Installation details](#14-installation-details)
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -180,7 +181,51 @@ data, or Smart-G returns NaN, which the modules turn into an error.
|
|
|
180
181
|
|
|
181
182
|
> Worked through in [notebook 03](../notebooks/03-compute-radiative-quantities.ipynb).
|
|
182
183
|
|
|
183
|
-
## 6.
|
|
184
|
+
## 6. Writing a sampler
|
|
185
|
+
|
|
186
|
+
A sampler is a combination of a physics function and a sentence saying how to call it.
|
|
187
|
+
Four declarations are required, and then the base class does the rest: cache, batching,
|
|
188
|
+
deduplication, provenance, and putting the coordinates back.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
class SphAlbSampler(AtmoSampler):
|
|
192
|
+
_output_vars = ["sph_alb"]
|
|
193
|
+
contract = "batch(aot, rh, h, href) vec(wl) -> sph_alb(wl)"
|
|
194
|
+
point_fn = staticmethod(sph_alb)
|
|
195
|
+
default_n_ph = 20000000
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`point_fn` is the physics: it takes one state and returns one result.
|
|
199
|
+
The contract says what a state is.
|
|
200
|
+
|
|
201
|
+
### The contract
|
|
202
|
+
|
|
203
|
+
| Word | Meaning |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `vec(...)` | these parameters travel together in one call, the physics gets arrays |
|
|
206
|
+
| `batch(...)` | these parameters are grouped into calls of `batch_size` states |
|
|
207
|
+
| `loop(...)` | one call per value, nothing else is possible |
|
|
208
|
+
| `-> name(dims)` | the variable written, and the dims it comes back with |
|
|
209
|
+
|
|
210
|
+
### Writing the output
|
|
211
|
+
|
|
212
|
+
A sampler whose contract carries `wl` is swept once for every band. One
|
|
213
|
+
that reads the scene, like `RhoToaSymSampler`, sweeps per band instead
|
|
214
|
+
and passes the scene to the physics:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
def _compute(self, scene):
|
|
218
|
+
for band in scene.bands:
|
|
219
|
+
scene[band][self._slot("rho_toa")] = self._sweep(
|
|
220
|
+
rho_s=scene[band], band=band
|
|
221
|
+
)
|
|
222
|
+
return scene
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
What is bound this way stays out of the sweep space, so the physics can
|
|
226
|
+
read a map the sweep knows nothing about.
|
|
227
|
+
|
|
228
|
+
## 7. PSF models
|
|
184
229
|
|
|
185
230
|
Analytical PSFs are `torch.nn.Module`s with bounded parameters, sampled
|
|
186
231
|
on an odd, square `PSFGrid`.
|
|
@@ -213,7 +258,7 @@ psf_params(tree, S2Band.B02) # {"sigma": DataArray}
|
|
|
213
258
|
tree.to_zarr("psf.zarr", mode="w")
|
|
214
259
|
```
|
|
215
260
|
|
|
216
|
-
##
|
|
261
|
+
## 8. Correction
|
|
217
262
|
|
|
218
263
|
For a uniform surface, `Unif2Toa` applies the 5S formula and `Toa2Unif`
|
|
219
264
|
inverts it, giving `rho_unif`, the reflectance a uniform surface would
|
|
@@ -234,7 +279,7 @@ from adjeff.modules.models import Unif2Surface
|
|
|
234
279
|
scene = Unif2Surface(kernels=tree)(scene)
|
|
235
280
|
```
|
|
236
281
|
|
|
237
|
-
##
|
|
282
|
+
## 9. Fitting a PSF
|
|
238
283
|
|
|
239
284
|
`fit` optimises the PSF of a model over reference scenes, once per
|
|
240
285
|
band and atmospheric state, and returns the frozen tree.
|
|
@@ -275,7 +320,7 @@ encircled-energy radii over a list of PSFs, e.g. a parameter grid.
|
|
|
275
320
|
|
|
276
321
|
> Worked through in [notebook 05](../notebooks/05-learn-psf.ipynb).
|
|
277
322
|
|
|
278
|
-
##
|
|
323
|
+
## 10. High-level API
|
|
279
324
|
|
|
280
325
|
`adjeff` exports one-call functions for the common paths:
|
|
281
326
|
|
|
@@ -294,7 +339,7 @@ encircled-energy radii over a list of PSFs, e.g. a parameter grid.
|
|
|
294
339
|
|
|
295
340
|
> Worked through in [notebook 04](../notebooks/04-simulate-rho-toa.ipynb).
|
|
296
341
|
|
|
297
|
-
##
|
|
342
|
+
## 11. Analysis and the `.adjeff` accessor
|
|
298
343
|
|
|
299
344
|
`adjeff.analysis` quantifies a PSF or a 2-D field:
|
|
300
345
|
|
|
@@ -322,7 +367,7 @@ da.adjeff.untidy() # and back, before xr.merge
|
|
|
322
367
|
|
|
323
368
|
> Worked through in [notebook 06](../notebooks/06-evaluate-a-psf.ipynb).
|
|
324
369
|
|
|
325
|
-
##
|
|
370
|
+
## 12. Cache
|
|
326
371
|
|
|
327
372
|
Every module takes a `CacheStore`. Results are stored in Zarr, keyed by
|
|
328
373
|
the module, its configuration and the provenance of its inputs; an
|
|
@@ -340,7 +385,7 @@ scene = pipeline(scene) # read back, no GPU call
|
|
|
340
385
|
|
|
341
386
|
Without a cache, every output stays in RAM.
|
|
342
387
|
|
|
343
|
-
##
|
|
388
|
+
## 13. Logging
|
|
344
389
|
|
|
345
390
|
Adjeff prints nothing until asked:
|
|
346
391
|
|
|
@@ -362,7 +407,7 @@ are capped at `warning`, and Python warnings join the stream. Lines are
|
|
|
362
407
|
Everything is an ordinary `logging` record under `adjeff`, so an
|
|
363
408
|
application's own handlers receive it.
|
|
364
409
|
|
|
365
|
-
##
|
|
410
|
+
## 14. Installation details
|
|
366
411
|
|
|
367
412
|
| Environment | GPU | Contents |
|
|
368
413
|
|---|---|---|
|
|
@@ -116,13 +116,19 @@ class ProductLoader(SceneSource, ABC):
|
|
|
116
116
|
_BASE_OUTPUT_VARS: ClassVar[list[str]] = ["rho_s"]
|
|
117
117
|
|
|
118
118
|
@property
|
|
119
|
-
def
|
|
120
|
-
"""Aggregate output
|
|
119
|
+
def _output_vars(self) -> list[str]: # type: ignore[override]
|
|
120
|
+
"""Aggregate output roles declared by active mixins.
|
|
121
|
+
|
|
122
|
+
Roles, not slots: ``SceneModule`` turns these into slots itself,
|
|
123
|
+
for caching, provenance and the public ``output_vars`` it reads
|
|
124
|
+
them from. A plain ``ClassVar`` cannot do this, since which
|
|
125
|
+
mixins are active is only known once this class is built.
|
|
126
|
+
"""
|
|
121
127
|
roles = list(self._BASE_OUTPUT_VARS)
|
|
122
128
|
for cls in type(self).__mro__:
|
|
123
129
|
contrib: list[str] = cls.__dict__.get("_output_vars_contribution", [])
|
|
124
130
|
roles.extend(contrib)
|
|
125
|
-
return
|
|
131
|
+
return roles
|
|
126
132
|
|
|
127
133
|
def __init__(
|
|
128
134
|
self,
|
|
@@ -126,11 +126,7 @@ class BrdfSampler(AtmoSampler):
|
|
|
126
126
|
name = self._output_vars[0]
|
|
127
127
|
for band in self.spectral_config.bands:
|
|
128
128
|
ds = scene[band]
|
|
129
|
-
|
|
130
|
-
# xsweep numbers a dim it did not sweep from zero, which no
|
|
131
|
-
# longer matches the scene's own labels.
|
|
132
|
-
arr = self._restore_coords(raw.sel(wl=band.wl_nm), ds)
|
|
133
|
-
ds[self._slot(name)] = self._normalise(arr, ds)
|
|
129
|
+
ds[self._slot(name)] = self._normalise(raw.sel(wl=band.wl_nm), ds)
|
|
134
130
|
return scene
|
|
135
131
|
|
|
136
132
|
def _t_sun(self, ds: xr.Dataset) -> xr.DataArray:
|
|
@@ -140,6 +140,6 @@ class RhoToaSampler(SweepSampler):
|
|
|
140
140
|
# the two travel together rather than through the sweep space.
|
|
141
141
|
for band in scene.bands:
|
|
142
142
|
arr = self._sweep(rho_s=scene[band], band=band)
|
|
143
|
-
scene[band]["rho_toa"] =
|
|
143
|
+
scene[band]["rho_toa"] = arr
|
|
144
144
|
|
|
145
145
|
return scene
|
|
@@ -125,6 +125,6 @@ class RhoToaSymSampler(SweepSampler):
|
|
|
125
125
|
# the two travel together rather than through the sweep space.
|
|
126
126
|
for band in scene.bands:
|
|
127
127
|
arr = self._sweep(rho_s=scene[band], band=band)
|
|
128
|
-
scene[band][self._slot("rho_toa")] =
|
|
128
|
+
scene[band][self._slot("rho_toa")] = arr
|
|
129
129
|
|
|
130
130
|
return scene
|
|
@@ -133,6 +133,9 @@ class SceneModule:
|
|
|
133
133
|
|
|
134
134
|
outcome["cached"] = False
|
|
135
135
|
scene = self._compute(scene)
|
|
136
|
+
for band in scene.bands:
|
|
137
|
+
ds = scene[band]
|
|
138
|
+
print(ds)
|
|
136
139
|
self._reject_non_finite(scene)
|
|
137
140
|
self._stamp_provenance(scene, key)
|
|
138
141
|
self._cache.save_vars(key, self._role_view(scene), self._output_vars)
|
|
@@ -162,6 +165,7 @@ class SceneModule:
|
|
|
162
165
|
values = ds[slot]
|
|
163
166
|
if values.dtype.kind not in "fc":
|
|
164
167
|
continue
|
|
168
|
+
print(values)
|
|
165
169
|
# Reduced rather than materialised: a lazy output is read
|
|
166
170
|
# back one chunk at a time instead of all at once.
|
|
167
171
|
bad = int((~np.isfinite(values)).sum())
|
|
@@ -78,14 +78,26 @@ class SweepSampler(SceneModule):
|
|
|
78
78
|
def _space(self) -> xr.Dataset:
|
|
79
79
|
"""Collect the swept parameters from the configs into one Dataset.
|
|
80
80
|
|
|
81
|
-
Coordinates are dropped
|
|
82
|
-
otherwise claim conflicting labels for it
|
|
81
|
+
Coordinates are dropped, two arrays sharing a spatial dim would
|
|
82
|
+
otherwise claim conflicting labels for it, and kept aside for
|
|
83
|
+
:meth:`_sweep` to put back.
|
|
83
84
|
"""
|
|
84
85
|
wanted = set(self._contract.inputs)
|
|
85
86
|
arrays: dict[str, xr.DataArray] = {}
|
|
87
|
+
self._dropped: dict[str, xr.DataArray] = {}
|
|
86
88
|
for config in self._get_configs():
|
|
87
89
|
for name, array in config._arrays.items():
|
|
88
90
|
if name in wanted and name not in arrays:
|
|
91
|
+
# Only an array's own dimension coordinates: a scalar
|
|
92
|
+
# tag of the same name, picked up from an earlier
|
|
93
|
+
# pipeline stage sharing this Dataset, is not one.
|
|
94
|
+
self._dropped.update(
|
|
95
|
+
{
|
|
96
|
+
str(d): array.coords[d]
|
|
97
|
+
for d in array.dims
|
|
98
|
+
if d in array.coords
|
|
99
|
+
}
|
|
100
|
+
)
|
|
89
101
|
arrays[name] = array.drop_vars(list(array.coords), errors="ignore")
|
|
90
102
|
return xr.Dataset(arrays)
|
|
91
103
|
|
|
@@ -96,19 +108,29 @@ class SweepSampler(SceneModule):
|
|
|
96
108
|
|
|
97
109
|
return coerce(type(self).contract)
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
def _restore_coords(
|
|
112
|
+
self, arr: xr.DataArray, source: xr.Dataset | None = None
|
|
113
|
+
) -> xr.DataArray:
|
|
114
|
+
"""Give *arr* back the coordinates the sweep space was stripped of.
|
|
102
115
|
|
|
103
116
|
xsweep labels the dims it does not sweep (``y``, ``x``) by
|
|
104
117
|
position; assigned as they are, they would misalign with the
|
|
105
|
-
scene and leave NaN.
|
|
118
|
+
scene and leave NaN. *source* names the scene the physics was
|
|
119
|
+
bound to, whose own dims are not in the space.
|
|
106
120
|
"""
|
|
107
121
|
shared = {
|
|
108
|
-
|
|
109
|
-
for
|
|
110
|
-
if
|
|
122
|
+
name: coord
|
|
123
|
+
for name, coord in getattr(self, "_dropped", {}).items()
|
|
124
|
+
if name in arr.dims
|
|
111
125
|
}
|
|
126
|
+
if source is not None:
|
|
127
|
+
shared.update(
|
|
128
|
+
{
|
|
129
|
+
str(dim): source.coords[dim]
|
|
130
|
+
for dim in arr.dims
|
|
131
|
+
if dim in source.coords and dim in source.dims
|
|
132
|
+
}
|
|
133
|
+
)
|
|
112
134
|
return arr.assign_coords(shared) if shared else arr
|
|
113
135
|
|
|
114
136
|
def _sweep(self, **bound: Any) -> xr.DataArray:
|
|
@@ -137,7 +159,7 @@ class SweepSampler(SceneModule):
|
|
|
137
159
|
self._log.info("sweep.plan", **plan)
|
|
138
160
|
result = sweeper(space, policy=self._policy(space, bound), **statics)
|
|
139
161
|
out: xr.DataArray = result[self._contract.outputs[0]]
|
|
140
|
-
return out
|
|
162
|
+
return self._restore_coords(out, bound.get("rho_s"))
|
|
141
163
|
|
|
142
164
|
_SPATIAL: ClassVar[frozenset[str]] = frozenset({"x", "y", "x_psf", "y_psf"})
|
|
143
165
|
|
|
@@ -125,7 +125,6 @@ class WuPsfSampler(SweepSampler):
|
|
|
125
125
|
for band in scene.bands:
|
|
126
126
|
ds = scene[band]
|
|
127
127
|
arr = self._sweep(rho_s=ds, band=band)
|
|
128
|
-
arr = self._restore_coords(arr, ds)
|
|
129
128
|
# The store allocates from the contract and labels a dimension
|
|
130
129
|
# it never swept by position, so the grid is put back here.
|
|
131
130
|
grid = PSFGrid(res=ds["rho_s"].adjeff.res, n=ds["rho_s"].adjeff.n)
|
|
@@ -305,3 +305,63 @@ class TestSweepPolicy:
|
|
|
305
305
|
for ds in scenes
|
|
306
306
|
]
|
|
307
307
|
assert stores[0] != stores[1]
|
|
308
|
+
|
|
309
|
+
@pytest.mark.integration
|
|
310
|
+
def test_a_mapped_sweep_keeps_the_scene_coordinates(self):
|
|
311
|
+
"""Every pixel gets a value, not the few whose index matches a label."""
|
|
312
|
+
import numpy as np
|
|
313
|
+
|
|
314
|
+
from adjeff.api import run_radiatives_from_scene
|
|
315
|
+
from adjeff.core import gaussian_image_dict
|
|
316
|
+
|
|
317
|
+
band, size = S2Band.B02, 5
|
|
318
|
+
scene = gaussian_image_dict(sigma=1.0, res_km=0.5, bands=[band], n=size)
|
|
319
|
+
ds = scene[band]
|
|
320
|
+
rng = np.random.default_rng(0)
|
|
321
|
+
ds["aot"] = (("y", "x"), rng.uniform(0.05, 0.4, (size, size)))
|
|
322
|
+
ds["h"] = (("y", "x"), np.zeros((size, size)))
|
|
323
|
+
for name, value in (
|
|
324
|
+
("rh", 50.0),
|
|
325
|
+
("href", 2.0),
|
|
326
|
+
("sza", 17.0),
|
|
327
|
+
("saa", 117.0),
|
|
328
|
+
("vza", 3.2),
|
|
329
|
+
("vaa", 207.0),
|
|
330
|
+
):
|
|
331
|
+
ds[name] = xr.DataArray([value], dims=[name])
|
|
332
|
+
scene[band] = ds
|
|
333
|
+
|
|
334
|
+
pytest.importorskip("smartg")
|
|
335
|
+
out = run_radiatives_from_scene(scene, n_bins=2)
|
|
336
|
+
assert bool(np.isfinite(out[band]["tdir_down"]).all())
|
|
337
|
+
|
|
338
|
+
def test_a_stray_scalar_coordinate_does_not_overwrite_a_real_axis(self):
|
|
339
|
+
"""An array carrying a stray same-named scalar coord keeps the real one."""
|
|
340
|
+
from adjeff.modules.samplers import TdirDownSampler
|
|
341
|
+
|
|
342
|
+
spectral = atmo.SpectralConfig.from_bands([S2Band.B8A])
|
|
343
|
+
geo = atmo.GeoConfig(
|
|
344
|
+
sza=xr.DataArray([17.0], dims=["sza"]),
|
|
345
|
+
vza=xr.DataArray([3.0], dims=["vza"]),
|
|
346
|
+
saa=xr.DataArray([117.0], dims=["saa"]),
|
|
347
|
+
vaa=xr.DataArray([207.0], dims=["vaa"]),
|
|
348
|
+
)
|
|
349
|
+
# aot carries a stray scalar 'wl' coordinate, as if inherited from
|
|
350
|
+
# a Dataset that already held per-wl radiative terms.
|
|
351
|
+
aot = xr.DataArray([0.2], dims=["aot"]).assign_coords(wl=865.0)
|
|
352
|
+
sampler = TdirDownSampler(
|
|
353
|
+
atmo_config=atmo.AtmoConfig(
|
|
354
|
+
aot=aot,
|
|
355
|
+
rh=xr.DataArray([50.0], dims=["rh"]),
|
|
356
|
+
h=xr.DataArray([0.1], dims=["h"]),
|
|
357
|
+
href=xr.DataArray([2.0], dims=["href"]),
|
|
358
|
+
species={"sulphate": 1.0},
|
|
359
|
+
),
|
|
360
|
+
geo_config=geo,
|
|
361
|
+
spectral_config=spectral,
|
|
362
|
+
remove_rayleigh=False,
|
|
363
|
+
)
|
|
364
|
+
sampler._space()
|
|
365
|
+
wl = sampler._dropped["wl"]
|
|
366
|
+
assert wl.dims == ("wl",)
|
|
367
|
+
assert float(wl.values[0]) == 865.0
|
|
@@ -79,3 +79,51 @@ class TestMissingInputs:
|
|
|
79
79
|
|
|
80
80
|
with pytest.raises(ImageIOError, match="Sun angles"):
|
|
81
81
|
loader._sza_saa()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestOutputVarsCaching:
|
|
85
|
+
"""A loader's mixin-contributed variables must round-trip through cache."""
|
|
86
|
+
|
|
87
|
+
def test_atmosphere_and_geometry_survive_a_cache_hit(self, tmp_path):
|
|
88
|
+
"""The second call, reading the cache, still carries aot, h, vza..."""
|
|
89
|
+
from adjeff.core import S2Band
|
|
90
|
+
from adjeff.modules.loaders.product_loader import (
|
|
91
|
+
AtmosphereMixin,
|
|
92
|
+
ElevationMixin,
|
|
93
|
+
GeometryMixin,
|
|
94
|
+
ProductLoader,
|
|
95
|
+
)
|
|
96
|
+
from adjeff.utils import CacheStore
|
|
97
|
+
|
|
98
|
+
class FullLoader(GeometryMixin, AtmosphereMixin, ElevationMixin, ProductLoader):
|
|
99
|
+
def __init__(self, product_path, bands, res, cache=None):
|
|
100
|
+
AtmosphereMixin.__init__(self)
|
|
101
|
+
ProductLoader.__init__(self, product_path, bands, res, cache=cache)
|
|
102
|
+
|
|
103
|
+
def reflectance(self, band):
|
|
104
|
+
return _make_ref()
|
|
105
|
+
|
|
106
|
+
def _vza_vaa(self, band):
|
|
107
|
+
return xr.DataArray([3.0]), xr.DataArray([200.0])
|
|
108
|
+
|
|
109
|
+
def _sza_saa(self):
|
|
110
|
+
return xr.DataArray([17.0]), xr.DataArray([117.0])
|
|
111
|
+
|
|
112
|
+
def _aot(self, ref):
|
|
113
|
+
return xr.DataArray([0.2])
|
|
114
|
+
|
|
115
|
+
def _rh(self):
|
|
116
|
+
return xr.DataArray([50.0])
|
|
117
|
+
|
|
118
|
+
def _h(self, ref):
|
|
119
|
+
return xr.DataArray([0.1])
|
|
120
|
+
|
|
121
|
+
cache = CacheStore(tmp_path)
|
|
122
|
+
product_path = tmp_path / "product"
|
|
123
|
+
product_path.mkdir()
|
|
124
|
+
bands = [S2Band.B02]
|
|
125
|
+
|
|
126
|
+
for _ in range(2): # first run computes, second reads the cache
|
|
127
|
+
scene = FullLoader(product_path, bands, res=0.12, cache=cache)()
|
|
128
|
+
for role in ("rho_s", "aot", "rh", "href", "h", "vza", "vaa", "sza", "saa"):
|
|
129
|
+
assert role in scene[S2Band.B02], role
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|