feat: implement scale-offset and data type casting via codecs - #154
Conversation
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class ScaleOffset(ArrayArrayCodec): |
There was a problem hiding this comment.
I did not make a stand-alone library for this codec, because it is so simple.
There was a problem hiding this comment.
ok but will it live in zarr-python or will users have to import eopf-geozarr to use the scale_offset?
There was a problem hiding this comment.
in the current state users have to import eopf-geozarr, but I don't like that outcome. I will see if we can get this into zarr-python
emmanuelmathot
left a comment
There was a problem hiding this comment.
great. Couple of comments only.
| @@ -1197,6 +1202,7 @@ def convert_s2_optimized_command(args: argparse.Namespace) -> None: | |||
| compression_level=args.compression_level, | |||
| validate_output=not args.skip_validation, | |||
| keep_scale_offset=args.keep_scale_offset, | |||
There was a problem hiding this comment.
The experimental_scale_offset_codec flag only activates when keep_scale_offset=False. if keep_scale_offset=True, the cdec branch is silently skipped. This may be surprising.
There was a problem hiding this comment.
@emmanuelmathot we could remove the "keep_scale_offset" parameter, either here in a separate PR.
|
|
||
| # Strip CF keys — the codecs handle encoding/decoding now | ||
| keep_keys = keep_keys - CF_SCALE_OFFSET_KEYS - {"_FillValue"} | ||
| var_encoding["fill_value"] = float("nan") |
There was a problem hiding this comment.
fill_value is hardcoded to float("nan") in the codec path, which may not be appropriate for integer-stored arrays.
|
Hi @d-v-b and @emmanuelmathot . I deployed this branch pinned to Claude traced it to the following: Root cause: In var_encoding["filters"] = (so_codec, cv_codec) # CastValueRustV1(data_type="uint16")
var_encoding["fill_value"] = float("nan") # ← incompatibleWhen zarr initialises the codec pipeline, This was already flagged in @emmanuelmathot's review comment. Why the conversion exits 0 The exception is raised inside a dask-distributed compute path ( This is maybe a second bug, I think a hard failure in the codec pipeline should not silently produce wrong output? Suggested fix by Claude Two options:
Option 2 seems to aligns better with the codec's design intent. Separately, could |
unpleasant! it definitely makes sense to make stream_write_dataset noisily error!
IMO we want the (decoded, float) fill value to be NaN. I think the fix here is to declare a scalar map that sends NaN -> 0, and ensures that the minimum real data value is 1 |
- use scalar map for handling NaN - ensure that downsampled arrays use scalar map + cast value - improve tests across parametrization of relelvant functions
| # CastValue refuses to cast NaN to integer without an explicit | ||
| # mapping, so we need a packed-dtype sentinel for NaN. Prefer | ||
| # the source's existing `_FillValue` (it already encodes the | ||
| # "no data" semantic via xarray's CF mask_and_scale loop), and | ||
| # fall back to the dtype's lowest representable integer. | ||
| packed_np_dtype = np.dtype(packed_dtype) | ||
| source_fill = var_data.encoding.get("_FillValue") | ||
| if source_fill is not None: | ||
| nan_sentinel = int(source_fill) | ||
| else: | ||
| nan_sentinel = int(np.iinfo(packed_np_dtype).min) | ||
| cv_codec = CastValueRustV1( | ||
| data_type=packed_np_dtype.name, | ||
| rounding="nearest-even", | ||
| scalar_map={ | ||
| "encode": [("NaN", nan_sentinel)], | ||
| "decode": [(nan_sentinel, "NaN")], | ||
| }, | ||
| ) | ||
| var_encoding["filters"] = (so_codec, cv_codec) |
There was a problem hiding this comment.
flagging this section as the location where we pick the int value that NaN gets mapped to. I first check and see if there's an existing _FillValue field in metadata, and use that if it's available. Otherwise, I use the lowest representable value in the output data type. If we want to make this configurable, we can expose it as an option.
|
@lhoupert have you had a chance to test the latest changes in the staging env? If you send me the command I could also test locally |
|
Yes! I was going to message. I ran it locally and created a notebook to validate the file generated. Will deploy that in staging as soon https://github.com/EOPF-Explorer/data-pipeline/blob/feat/181-scale-offset-codec-staging/operator-tools/codec/validate_conversion.ipynb |
|
Could we have the samples on S3 to test all the tools with it? (TiTiler, OL, GDAL?) |
|
I updated to use the latest zarr-python release, which includes scale-offset and cast-value! so we can remove some code from this PR :) |
…to feat/scale-offset-codecs
|
a few off-target changes incoming, since xarray versions are causing some skew in fill values. I'm going to fix this by setting the fill value explicitly for all variables, and not exposing us to xarray defaults. |
Branch summary (from Claude)This is a recap of everything that landed on 1. CF scale-offset, optionally pushed into the zarr codec pipelineNew CLI flag for When on (and The codecs themselves come from NaN handling: 2. Encoding-loss fixes around
|
this PR defines the scale-offset transformation as a pair of codecs: a
ScaleOffsetcodec, which implements the Zarr V3scale-offsetcodec , and theCastValueRustV1codec fromcast-value.py, which implements the Zarr V3cast_valuecodec. This means float data can be decoded as float, but stored as ints, in a cf- and xarray-independent manner.Right now it's exposed via a boolean parameter to
convert_s2_optimized, and command-line flag--experimental-scale-offset-codec.caveat: downstream consumers will need the
ScaleOffsetandCastValuecodecs available in order to decode the data. We should work on ways to make this straightforward.