Repository navigation
feat!: one scale/offset encoding switch - #287
Conversation
Packed arrays keep their source integer dtype. By default the Zarr scale_offset + cast_value codecs store them; scale_offset_codec=False (--no-scale-offset-codec) writes the ESA layout with CF scale_factor, add_offset and _FillValue, and STAC raster:scale/raster:offset/nodata on the reflectance asset. The decoded float32 output is removed. Raw (CPM, mask_and_scale=False) and decoded input give the same output. Raw integer pyramid levels are rounded instead of truncated, a 0 fill value is honoured, finer bands are decoded before injection so unmasked nodata is not averaged in, and the trivial scale 1 / offset 0 on SCL is dropped. BREAKING CHANGE: keep_scale_offset is removed from convert_s2_optimized, create_multiscale_from_datatree, create_uniform_encoding, convert_olci_optimized and the CLI; the CLI flag --experimental-scale-offset-codec is removed. The CPM writer still accepts keep_scale_offset for one release with a DeprecationWarning. Closes #281 Co-Authored-By: Claude Opus 5.5 <[email protected]>
211eb8c to
ce7ae54
Compare
lhoupert
left a comment
There was a problem hiding this comment.
Thanks, this covers my comments from #277 and #281:
One non-blocking detail picked up by Claude:
packing_ofalso looks atvar.encoding["fill_value"], but xarray (2026.4) doesn't set that key for Zarr v3 arrays. So a v3 input with nodata only in the Zarrfill_value(no EOPF attribute) isn't masked, and DN 0 is packed as a valid -0.1. CPM products aren't affected. Either read the Zarr array'sfill_value, or drop that candidate and the "or the Zarrfill_value" in the docstring.
Is it worth raising an issue for it?
- Updated `test_convert_command_routes_s2_to_optimized` to reflect changes in default encoding from Zarr scale-offset codecs to ESA layout with CF attributes. - Renamed `test_write_s2_default_uses_scale_offset_codecs` to `test_write_s2_default_writes_esa_layout` and adjusted assertions to verify the ESA layout. - Removed the test for writing without scale offset codec and added a new test `test_write_s2_scale_offset_codec` to validate the behavior when `scale_offset_codec=True`. - Updated the deprecation warning test for `keep_scale_offset` to reflect the new codec handling logic.
|
@claxn @shollendEODC can be merged as soon as you review it |
|
I am currently checking the code and am verifying everything works as expected. |
|
Alright, I had a quick look and applied the conversion to For either options, the measurements and injected bands (as they have decoding attributes) get fed through Of course, a casting already occurs during the coarsening of variables to calculate the multiscales (no data handling/masking), but as this would only afflict certain products (and only the coarsened bands themselves), and as we aim to generalize and harmonize the product-specific functions into the utilities I would refrain from implementing a float cast for all products. I am not fully aware of the codec implementations and the introduces casting, these observations are based on my personal (development)/user perspective. |
|
As this PR should be resolved before merging #292 I wanted to ask for your input on my previous comment @emmanuelmathot? many thanks |
claxn
left a comment
There was a problem hiding this comment.
Thanks @emmanuelmathot for the quick work - I left two small remarks, which would be good to be addressed/discussed before merging.
| fill_value = var_data.attrs.get("fill_value") | ||
| if fill_value is None: | ||
| fill_value = var_data.encoding.get("_FillValue") |
There was a problem hiding this comment.
Would it be possible to replace those lines with a new function, e.g. find_fill_value(), which is based on the following lines:
data-model/src/eopf_geozarr/s2_optimization/s2_multiscale.py
Lines 100 to 106 in 8beecb3
| for var_name in list(dataset.data_vars): | ||
| packing = packing_of(dataset[var_name]) | ||
| if packing is not None: | ||
| dataset[var_name] = _decode_packed(dataset[var_name], packing) |
There was a problem hiding this comment.
As @shollendEODC already mentioned this call is executed no matter how scale_offset_codec is set. I would suggest to include scale_offset_codec here in this if statement and only execute _decode_packed if scale_offset_codec=True (and we need to verify that this does not cause any issues further down the pipeline, e.g. when computing the multiscales)
…tiscale processing
|
Sorry, i was busy whole day. Great catches. Fixing both. |
Security Audit ReportBandit — Static Security Analysis (Security tab)12 issue(s) found: 12 low ✅ No issues at or above HIGH severity. 12 low issue(s) below threshold not shown in table. pip-audit — Dependency Vulnerabilities (Security tab)
14 vulnerability/vulnerabilities found (14 fixable) across 3 package(s). Result: ❌ Blocking issues found — see details above. |
…c parameterization
|
merged to unblock #292. If some items remain, let's open new issues. |
Closes #281. Closes #246.
Summary
One switch now selects how packed arrays (for example S2 reflectance:
uint16,scale_factor0.0001,add_offset-0.1, nodata 0) are stored. The decoded float32 output is gone.scale_offset_codec=True/--scale-offset-codecscale_offset+cast_valuecodecs (from #154); logical dtypefloat32, fill value NaNscale_factor,add_offset,_FillValueon arrays;raster:scale,raster:offset,nodataon the STACreflectanceasset (#267)Changes
create_uniform_encoding):packing_of()finds the CF packing in.encoding(decoded input) or.attrs(raw input, as in the CPM path). Each packed array is first normalised to decoded float32 with NaN for nodata, so both input forms give the same output. Then:scale_offset+cast_valuefiltersscale_factor1 /add_offset0 pair that Zarr v3 CPM products declare on SCL is dropped, so SCL staysuint8and is not decoded to float.fill_valueattribute)convert_s2_optimized(scale_offset_codec=...)andcreate_multiscale_from_datatree(scale_offset_codec=...)(defaultTrue) replacekeep_scale_offsetconvert-s2-optimized --no-scale-offset-codecreplaces--keep-scale-offset--experimental-scale-offset-codecis removed, because the codec is now the defaultkeep_scale_offset; OLCI already writes rawuint16with CF attributesscale_offset_codecoption (defaultTrue) andeopf convert-geozarr --no-scale-offset-codec.keep_scale_offsetis still accepted for one release with aDeprecationWarning:keep_scale_offset=Trueisscale_offset_codec=False(ESA layout).docs/converter.md, a short note inREADME.md.Breaking changes
convert-s2-optimized,convert(S2 route) and the CPM writer changes from decoded float32 to codec-packed integers. Decoded values are the same within one packing step.keep_scale_offsetis removed fromconvert_s2_optimized,create_multiscale_from_datatree,create_uniform_encoding,convert_olci_optimizedand the CLI. The CPM writer keeps it for one release.--experimental-scale-offset-codecis removed. The S2 convert templates inplatform-deploystill pass it, so they must drop it before they use this release (their output stays the same, because the codec is now the default).Tests
test_create_measurements_encoding: both modes × raw and decoded input. This replaces theadd_offsetcheck that never ran (@lhoupert's comment in feat!: reintegrate cpm-geozarr into main #277).test_create_multiscale_from_datatree_behavior: both modes × raw (CPM v3 style, nodata only infill_value) and decoded input. At every level r10m–r720m it checks dtype, codecs, CF attributes, decoded values within one packing step, and that a nodata block stays NaN.test_inject_missing_bands_skips_unmasked_nodata: new test.scale_offset_codec=Falsewith its STAC fields, and thekeep_scale_offset=Truedeprecation.REGENERATE_SNAPSHOTS=1). Only the reflectance arrays and their group metadata change:float32logical dtype, NaN fill, codecs nested in the sharding codec, no CF scale attributes. The snapshot comparison now uses JSON, because a NaN fill value never equals itself.--extra cpm, and ruff/pyright all pass.🤖 Generated with Claude Code