adjeff 1.2.2__tar.gz → 1.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. {adjeff-1.2.2 → adjeff-1.2.3}/CHANGELOG.md +13 -0
  2. {adjeff-1.2.2 → adjeff-1.2.3}/PKG-INFO +1 -1
  3. {adjeff-1.2.2 → adjeff-1.2.3}/docs/guide.md +61 -16
  4. {adjeff-1.2.2 → adjeff-1.2.3}/pyproject.toml +1 -1
  5. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/loaders/maja_loader.py +1 -0
  6. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/_brdf_sampler.py +1 -5
  7. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/rho_toa.py +1 -1
  8. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/rho_toa_sym.py +1 -1
  9. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/scene_module.py +4 -0
  10. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/sweep_sampler.py +23 -10
  11. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/reference/wu2024.py +0 -1
  12. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_atmo_samplers.py +29 -0
  13. {adjeff-1.2.2 → adjeff-1.2.3}/.gitattributes +0 -0
  14. {adjeff-1.2.2 → adjeff-1.2.3}/.github/workflows/ci.yml +0 -0
  15. {adjeff-1.2.2 → adjeff-1.2.3}/.github/workflows/publish.yml +0 -0
  16. {adjeff-1.2.2 → adjeff-1.2.3}/.gitignore +0 -0
  17. {adjeff-1.2.2 → adjeff-1.2.3}/.pre-commit-config.yaml +0 -0
  18. {adjeff-1.2.2 → adjeff-1.2.3}/LICENSE +0 -0
  19. {adjeff-1.2.2 → adjeff-1.2.3}/README.md +0 -0
  20. {adjeff-1.2.2 → adjeff-1.2.3}/docs/plan-observabilite-0.13.0.md +0 -0
  21. {adjeff-1.2.2 → adjeff-1.2.3}/docs/plan-smartg-2.0-0.15.0.md +0 -0
  22. {adjeff-1.2.2 → adjeff-1.2.3}/docs/revue-simplification-2026-08.md +0 -0
  23. {adjeff-1.2.2 → adjeff-1.2.3}/docs/roadmap.md +0 -0
  24. {adjeff-1.2.2 → adjeff-1.2.3}/notebooks/01-create-and-display-image.ipynb +0 -0
  25. {adjeff-1.2.2 → adjeff-1.2.3}/notebooks/02-atmospheric-configuration.ipynb +0 -0
  26. {adjeff-1.2.2 → adjeff-1.2.3}/notebooks/03-compute-radiative-quantities.ipynb +0 -0
  27. {adjeff-1.2.2 → adjeff-1.2.3}/notebooks/04-simulate-rho-toa.ipynb +0 -0
  28. {adjeff-1.2.2 → adjeff-1.2.3}/notebooks/05-learn-psf.ipynb +0 -0
  29. {adjeff-1.2.2 → adjeff-1.2.3}/notebooks/06-evaluate-a-psf.ipynb +0 -0
  30. {adjeff-1.2.2 → adjeff-1.2.3}/pixi.lock +0 -0
  31. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/__init__.py +0 -0
  32. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/_logging.py +0 -0
  33. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/accessor.py +0 -0
  34. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/analysis/__init__.py +0 -0
  35. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/analysis/_energy.py +0 -0
  36. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/analysis/_error.py +0 -0
  37. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/analysis/_plane.py +0 -0
  38. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/analysis/_radial.py +0 -0
  39. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/analysis/_vectorise.py +0 -0
  40. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/api/__init__.py +0 -0
  41. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/api/_config.py +0 -0
  42. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/api/_load.py +0 -0
  43. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/api/_pipeline.py +0 -0
  44. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/api/_psf.py +0 -0
  45. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/atmosphere/__init__.py +0 -0
  46. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/atmosphere/atmo_config.py +0 -0
  47. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/atmosphere/atmo_factory.py +0 -0
  48. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/atmosphere/geo_config.py +0 -0
  49. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/atmosphere/spectral_config.py +0 -0
  50. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/atmosphere/surface.py +0 -0
  51. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/core/__init__.py +0 -0
  52. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/core/_psf.py +0 -0
  53. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/core/analytical_psf.py +0 -0
  54. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/core/bands.py +0 -0
  55. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/core/image_dict.py +0 -0
  56. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/core/image_generator.py +0 -0
  57. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/core/non_analytical_psf.py +0 -0
  58. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/core/psf_tree.py +0 -0
  59. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/exceptions.py +0 -0
  60. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/__init__.py +0 -0
  61. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/classic/__init__.py +0 -0
  62. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/classic/toa_to_unif.py +0 -0
  63. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/classic/unif_to_toa.py +0 -0
  64. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/loaders/__init__.py +0 -0
  65. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/loaders/product_loader.py +0 -0
  66. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/models/__init__.py +0 -0
  67. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/models/psf_conv_module.py +0 -0
  68. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/models/unif2surface.py +0 -0
  69. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/pipeline.py +0 -0
  70. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/__init__.py +0 -0
  71. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/_atmo_sampler.py +0 -0
  72. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/_ensure.py +0 -0
  73. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/_smartg.py +0 -0
  74. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/radiatives.py +0 -0
  75. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/rho_atm.py +0 -0
  76. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/sph_alb.py +0 -0
  77. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/sph_alb_brdf.py +0 -0
  78. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/tdif_down.py +0 -0
  79. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/tdif_up.py +0 -0
  80. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/tdif_up_brdf.py +0 -0
  81. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/tdir_down.py +0 -0
  82. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/samplers/tdir_up.py +0 -0
  83. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/modules/scene_source.py +0 -0
  84. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/__init__.py +0 -0
  85. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/_combo_stage.py +0 -0
  86. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/_config.py +0 -0
  87. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/adam_optimizer.py +0 -0
  88. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/fit.py +0 -0
  89. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/landscape.py +0 -0
  90. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/lbfgs_optimizer.py +0 -0
  91. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/loss.py +0 -0
  92. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/metrics.py +0 -0
  93. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/optim/training_set.py +0 -0
  94. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/py.typed +0 -0
  95. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/reference/__init__.py +0 -0
  96. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/utils/__init__.py +0 -0
  97. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/utils/_config.py +0 -0
  98. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/utils/cache_store.py +0 -0
  99. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/utils/convolve.py +0 -0
  100. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/utils/radial.py +0 -0
  101. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/utils/smartgutils.py +0 -0
  102. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/utils/torchutils.py +0 -0
  103. {adjeff-1.2.2 → adjeff-1.2.3}/src/adjeff/utils/xrutils.py +0 -0
  104. {adjeff-1.2.2 → adjeff-1.2.3}/tests/_test_module.py +0 -0
  105. {adjeff-1.2.2 → adjeff-1.2.3}/tests/conftest.py +0 -0
  106. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_accessor.py +0 -0
  107. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_analysis.py +0 -0
  108. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_api_docs.py +0 -0
  109. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_api_psf.py +0 -0
  110. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_atmosphere_config.py +0 -0
  111. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_brdf_samplers.py +0 -0
  112. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_classic_modules.py +0 -0
  113. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_config.py +0 -0
  114. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_convolve.py +0 -0
  115. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_image_dict.py +0 -0
  116. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_image_generator.py +0 -0
  117. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_import.py +0 -0
  118. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_integration.py +0 -0
  119. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_landscape.py +0 -0
  120. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_loaders.py +0 -0
  121. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_logging.py +0 -0
  122. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_optim.py +0 -0
  123. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_psf.py +0 -0
  124. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_psf_tree.py +0 -0
  125. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_radial.py +0 -0
  126. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_scene_module.py +0 -0
  127. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_smartg_api.py +0 -0
  128. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_smartg_defaults.py +0 -0
  129. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_smartgutils.py +0 -0
  130. {adjeff-1.2.2 → adjeff-1.2.3}/tests/test_xrutils.py +0 -0
@@ -5,6 +5,19 @@ 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.3]
9
+
10
+ ### Fixed
11
+
12
+ - **A radiative sweep over maps keeps the scene's coordinates.** xsweep
13
+ labels by position the dims it does not sweep, so a scene whose `aot`
14
+ and `h` are maps got its terms aligned on those labels: every pixel
15
+ came back NaN but the handful whose index matched one. `load_maja(...,
16
+ as_map=True, compute_radiatives=True)` could not produce anything.
17
+ - **A sweep restores its own coordinates.** A sampler used to do it
18
+ itself, in its own `_compute`, which is what the fix above forgot in
19
+ one of five places. There is now one place.
20
+
8
21
  ## [1.2.2]
9
22
 
10
23
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: adjeff
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Library that provides utilities to simulate adjacency effects.
5
5
  Project-URL: Homepage, https://github.com/walcark/Adjeff
6
6
  Project-URL: Issues, https://github.com/walcark/Adjeff/issues
@@ -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. [PSF models](#6-psf-models)
9
- 7. [Correction](#7-correction)
10
- 8. [Fitting a PSF](#8-fitting-a-psf)
11
- 9. [High-level API](#9-high-level-api)
12
- 10. [Analysis and the `.adjeff` accessor](#10-analysis-and-the-adjeff-accessor)
13
- 11. [Cache](#11-cache)
14
- 12. [Logging](#12-logging)
15
- 13. [Installation details](#13-installation-details)
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. PSF models
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
- ## 7. Correction
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
- ## 8. Fitting a PSF
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
- ## 9. High-level API
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
- ## 10. Analysis and the `.adjeff` accessor
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
- ## 11. Cache
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
- ## 12. Logging
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
- ## 13. Installation details
410
+ ## 14. Installation details
366
411
 
367
412
  | Environment | GPU | Contents |
368
413
  |---|---|---|
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adjeff"
3
- version = "1.2.2"
3
+ version = "1.2.3"
4
4
  authors = [{ name = "walcark", email = "kevin.walcarius@gmail.com" }]
5
5
  readme = "README.md"
6
6
  description = "Library that provides utilities to simulate adjacency effects."
@@ -195,6 +195,7 @@ class MajaLoader(
195
195
 
196
196
  with rasterio.open(glob_mnt[0]) as src:
197
197
  arr = src.read(1).astype(float)
198
+ arr = np.where(arr < 0.0, 0.0, arr)
198
199
 
199
200
  if self.as_map:
200
201
  return xr.DataArray(
@@ -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
- # Coordinates first: the algebra below aligns by label, and
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"] = self._restore_coords(arr, scene[band])
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")] = self._restore_coords(arr, scene[band])
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,17 @@ 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: two arrays sharing a spatial dim would
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
+ self._dropped.update({str(d): c for d, c in array.coords.items()})
89
92
  arrays[name] = array.drop_vars(list(array.coords), errors="ignore")
90
93
  return xr.Dataset(arrays)
91
94
 
@@ -96,19 +99,29 @@ class SweepSampler(SceneModule):
96
99
 
97
100
  return coerce(type(self).contract)
98
101
 
99
- @staticmethod
100
- def _restore_coords(arr: xr.DataArray, source: xr.Dataset) -> xr.DataArray:
101
- """Give *arr* the coordinates *source* holds for its dims.
102
+ def _restore_coords(
103
+ self, arr: xr.DataArray, source: xr.Dataset | None = None
104
+ ) -> xr.DataArray:
105
+ """Give *arr* back the coordinates the sweep space was stripped of.
102
106
 
103
107
  xsweep labels the dims it does not sweep (``y``, ``x``) by
104
108
  position; assigned as they are, they would misalign with the
105
- scene and leave NaN.
109
+ scene and leave NaN. *source* names the scene the physics was
110
+ bound to, whose own dims are not in the space.
106
111
  """
107
112
  shared = {
108
- str(dim): source.coords[dim]
109
- for dim in arr.dims
110
- if dim in source.coords and dim in source.dims
113
+ name: coord
114
+ for name, coord in getattr(self, "_dropped", {}).items()
115
+ if name in arr.dims
111
116
  }
117
+ if source is not None:
118
+ shared.update(
119
+ {
120
+ str(dim): source.coords[dim]
121
+ for dim in arr.dims
122
+ if dim in source.coords and dim in source.dims
123
+ }
124
+ )
112
125
  return arr.assign_coords(shared) if shared else arr
113
126
 
114
127
  def _sweep(self, **bound: Any) -> xr.DataArray:
@@ -137,7 +150,7 @@ class SweepSampler(SceneModule):
137
150
  self._log.info("sweep.plan", **plan)
138
151
  result = sweeper(space, policy=self._policy(space, bound), **statics)
139
152
  out: xr.DataArray = result[self._contract.outputs[0]]
140
- return out
153
+ return self._restore_coords(out, bound.get("rho_s"))
141
154
 
142
155
  _SPATIAL: ClassVar[frozenset[str]] = frozenset({"x", "y", "x_psf", "y_psf"})
143
156
 
@@ -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,32 @@ 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())
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