Repository navigation
Conversation
A script writes small netCDF4 files that differ in codec, chunk length, inner chunking, CF scaling and dtype, virtualizes each with VirtualiZarr, and tries to combine them along time: four cases fail, and the per-file scale_factor case combines and reads silently wrong values. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
variant.py builds one array from VirtualiZarr's per-file manifests: a chunk_variant codec whose configuration lists how chunks are stored, and refkv reference values that prefix each chunk with its variant's index as a literal byte before the file's own bytes. With the rectilinear chunk grid, all five cases (codec eras, month-length files, ragged inner chunks, per-file CF scaling, varying dtype) read back equal to the files, where VirtualiZarr fails four and silently misreads the fifth. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…codec Each per-file difference is its own codec: the file's own compression, zarr-extensions cast_value and scale_offset, and a new crop for whole-stored edge chunks. A generic switch codec chooses, per chunk, which chain applies, by a varint index that refkv's reference values prefix to the file's bytes. The README explains why the chooser stays format-agnostic, how it composes, and its limits. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…than clamping Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry case Each case's files now hold one smooth field that continues across them, so a misdecoded file shows as a break at its boundary. Results are compared with the files bit for bit (or the largest difference is reported), and figures/ gets a plot per case: the files, refkv and VirtualiZarr along time, and refkv's whole field. Every refkv case is bit-identical; VirtualiZarr's per-file-scaling case is off by up to 19.6. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts own with uv run Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y can express The concatenated virtual datasets are "one Zarr array today": VirtualiZarr parses every file, and its refusals report what a single array can't hold, which is the gap the issue describes. Case 4's values come from xarray's default combine_attrs="override" keeping the first file's attributes; "no_conflicts" refuses and "drop_conflicts" drops the scaling. The refkv side is a sketch of the issue's design 2.1, built on VirtualiZarr's per-file manifests. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and measure scaling
A switch case is now a codec chain that may leave numbers open
({"parameter": k}); a chunk's header carries its case index and those
numbers. zarr.json holds the kinds of encoding, and per-chunk numbers travel
in each chunk's reference: case 4 needs one case instead of three.
scaling.py packs every chunk differently: at 100,000 chunks, one case per
chunk is a 37.7 MB zarr.json and 2.3 s to open, against 0.8 kB and 0.3 s
with one parameterized case; both read bit for bit.
Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut rewriting chunks A case's key is the first 8 bytes of the SHA-256 of its canonical JSON, and a chunk's header starts with that key. Concatenating two combined arrays merges their case tables by union and copies every chunk header unchanged; case 6 checks the result equals combining all files at once, bit for bit. The scaling run now makes every chunk's packing unique, since content keys merge repeated chains. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one file Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…py scaling` Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI text below 🤖
This adds an executable demo,
design/experiments/inhomogeneous/, of the five many-file cases in zarr-developers/zarr-specs#399, and a sketch of the issue's design 2.1 (per-chunk codec selection) that expresses each case as one Zarr array. VirtualiZarr parses every file in both paths. The demo shows what a single Zarr array can and can't hold today, and how per-chunk context in refkv reference values could fill the gap.scale_factor/add_offsetIn cases 1, 2, 3 and 5, VirtualiZarr refuses to concatenate and says what is missing. That is the safe behavior, and the gap it reports is the issue's subject.
Case 4 isn't specific to any tool. CF scaling is array-level attributes, so concatenating has to pick one set:
"override"(the default inxr.concatandopen_virtual_mfdataset) keeps the first file's, and the other files decode off by up to 19.6."no_conflicts"refuses."drop_conflicts"leaves the raw integers.The sketch (its own section of demo.py) works like this:
cast_valueandscale_offset, and a newcropfor HDF5's whole-stored edge chunks.switchcodec picks, per chunk, which chain applies.{"parameter": k}).zarr.jsonholds the kinds of encoding; per-chunk numbers travel with each chunk. Case 4's three files share one case.Concat(Range(data=<varint>), Range(source=<file>, offset, length)), so no store injects anything at read time and no codec does I/O.rectilinearchunk grid.Scaling (
uv run demo.py scaling): every chunk is packed with its own CF scale and offset, as GRIB packs each message.zarr.json/ openzarr.json/ openBoth read 200 sampled chunks bit for bit, and reading one chunk takes about 2 ms either way. With parameters, what still grows is the archive's own key index, which any archive of 100,000 chunks has.
The README explains why the chooser stays format-agnostic, how it composes (outer codecs, nested
switch, sharding, theconditionalprecedent, manifest-level concatenation), and its limits.How results are checked:
_FillValue.np.array_equal, NaN equal to NaN); otherwise the largest difference is reported.figures/.Notes
cd design/experiments/inhomogeneous && uv run demo.py(anduv run demo.py scaling). The dependencies are declared inline (PEP 723): refkv from PyPI, VirtualiZarr,cast-value-rs(which zarr-python'scast_valueneeds) and matplotlib. uv builds an environment for this script alone. The agent checked the run by copyingdemo.pyalone into an empty directory.cast_valuemetadata whose fill value can't round-trip, which the cast_value spec says MUST be an error. The fix is the draft zarr-developers/zarr-python#4510. The demo maps NaN to each file's CF_FillValuewithscalar_map.switchandcropare defined only in this demo; they are not registered zarr extensions.design/experiments/inhomogeneous/, with no dependency on other PRs.🤖 Generated with Claude Code