moczarr.ragged
Generic zagg-ragged/1 vlen decode layer — product-agnostic (issue #19).
The read side of zagg's ragged store spec (docs/specification.md §1 in
englacial/zagg, normative on main): a kind: ragged field is ONE
variable_length_bytes zarr v3 array on the cells axis. Each populated
cell holds the raw little-endian bytes of its (n, *inner_shape)
payload; empty cells keep the b"" fill; an all-empty inner chunk is
omitted from disk entirely. The element interpretation is self-describing in
the payload array's attrs under the versioned ragged block (§1.2), and a
located field declares its row-aligned uint64 sibling there too —
readers bind the sibling by metadata, never by reconstructing the
{field}_locations naming convention. A temporal field (spec §8.3,
zagg#410) declares a second row-aligned uint64 sibling — per-centroid
toc words — under the spec-owned top-level times attrs key, a sibling
of the ragged block rather than a member of it (the block grammar is
unchanged by that revision). Companion siblings self-describe their word
grammar in their own attrs — the §8 temporal / §9 located
declaration blocks (:func:parse_companion_attrs): strict-checked when
present; an absent located block is §2.2 verbatim and never a refusal.
This layer decodes companion words as bytes (row-aligned uint64); the
word semantics (mortie's morton / toc grammars) live above it.
Pin record — re-checked at d52e3063, the englacial/zagg#463 merge (§1
landed with the #346 rebase merge; the interim pin 9e11e65 is
historical). The delta between those two shas was re-read section by
section for this advance:
- §1 — §1.1 and §1.2 only. §1.1 carries the §8.3 temporal surface this
module now decodes: the sibling inventory goes three to four with
{group}/{field}_times, row-aligned and bound by the payload's top-leveltimesattrs key (a field may carry either sibling, both, or neither). §1.2 extends its spec-owned-key discipline past theraggedblock itself — the §2.0weightskey and §8.3'stimesbinding are writer-stamped on the payload array, as are the §8/§9temporalandlocateddeclaration blocks on the companion arrays that carry them — and restates the sibling rule as no user attrs, only the spec-owned declaration its own section defines. §1.3/§1.4 wire framing and the §1.5 storage-geometry/subtree-span contracts are byte-identical across the delta, as is §1.6. - §2 — three items ride this pin, none of which this layer
mis-decodes. §2.0 is new: a payload declares its weight column as
counts(which an absent key MUST be read as) orflux, and this layer now gates on it (issue #43): :func:parse_ragged_attrsreads the spec-owned TOP-LEVELweightskey off every payload array with the absent-⇒-countsdefault, refuses an unknown value — §2.0's "MUST be refused, never read as either defined value" — and surfaces the declaration as :attr:RaggedElement.weights, which only :func:open_raggedreturns. The value-yielding paths (:func:read_ragged, :func:read_cell, :func:moczarr.hhdc.read_tensors) carry no element, so they GATE on the declaration rather than surface it: each refuses an unknown value through this same call, andread_tensors— the one surface here that does ARITHMETIC on the weight column — additionally refuses an integer tensor dtype over afluxpayload, whose rounding would hand back a photoelectron estimate presented as a count. Digest payload bytes decode identically under either declaration, so the gate changes no values; it changes what a consumer may CLAIM about them — underflux,sum(weights)is a photoelectron estimate, never an observation count. §2.0's same-declaration merge rule stays vacuous here, since moczarr has no payload-merge entry point at all (:mod:moczarr.compositiondeclines a read-side merge by design); the surfaced declaration is the value any future combining surface must match on, mirroring zagg'scheck_weights_matchposture (merges are legal only between matching declarations — an absent key iscountsfor that rule too). §2.1 rescopes its exact-count MUST tocountsand adds afluxbullet (sum(weights)is a float32 photoelectron estimate: the exact-count recovery is undefined there and no integrality holds). §2.2 is substantially rewritten, and two of its new clauses are ones this module leans on: a reader MUST decode each word's order and kind from the word itself and MUST NOT assume a uniform order per array, per cell, or per store — satisfied by construction here, which yields companion words as opaque row-aligneduint64— and the sentence that an absentlocatedblock is this section verbatim, "never a refusal (§9)", which is spec text at this pin where it was an inference at the old one. - §6 — not pinned by this module, but read through in the same pass
because the
/2carve-out below cites it: §6.1 extends the/2element declaration to the{field}_timestemporal sibling (the sameuint64/()pair as a locations sibling, the two differing only in the declaration they carry), and §6.3 notes that §8.3'stimesbinding and §2.0'sweightsare top-level keys beside theraggedblock and so ride the/2migration unchanged. §6.2's byte-identity contract is unmoved, and neither hunk changes anything here — a/2array is still refused as a newer revision.
Zero product knowledge lives here: this module decodes any conforming
element declaration (a t-digest's float32 (n, 2), a locations sibling's
uint64 (n,), or anything else a future writer declares) and never
imports zagg. The genericity is over the ELEMENT, and the scope is bounded
on the indexing side: :func:read_ragged and the profiles read a 1-D cells
axis with a sibling morton coordinate — every conforming ragged field of
a morton-hive (HEALPix) product, which is moczarr's whole domain. zagg's
rectilinear grid writes kind: ragged fields too, but on a 2-D (y, x)
cell grid with no per-cell morton coordinate (its shard keys are packed
(rb, cb) tile ints), so a rect ragged field is out of scope for this
layer and would need its own cell-identity binding. The element gates
(:func:parse_ragged_attrs, :func:decode_cell) carry no such assumption.
The HHDC tensor profile (:mod:moczarr.hhdc) composes on top.
The sweeps accept subtree= (issue #29): the nested cells axis makes
"everything below one morton node" a contiguous index span (zagg spec §1.5),
so a restricted read fetches only the stored objects covering that span —
never a whole-array sweep. Span grammar in
:func:moczarr.convention.subtree_cell_span.
Postures, inherited from the spec's conformance rules:
- Strict attrs gate.
specis strict-checked: a missing block, a foreign marker, or a newer revision raises — a reader must never half-parse under a guessed layout (a mis-guessed element dtype misinterprets every payload). Contrast the coverage envelopes' tolerantNone: those are caches that degrade to the walk; ragged attrs are truth. The one marker-absence carve-out is the/2revision, whose typed dtype IS the signal (§1.6/§6.1): :func:open_raggednames it from the declared data type before the attrs gate, so a/2array is refused as a newer revision rather than mis-diagnosed as unsignaled. - One code path for both storage geometries (§1.5). Sharded
(
sharding_indexed; every hive leaf) and per-inner-chunk (regular array; the unsharded flat path) layouts are self-describing in the array's own metadata, so the sweep derives the stored span fromarr.shardswhen present, elsearr.chunks. - Debris-tolerant listing. The sweep LISTs the array's
c/prefix, so unlike zagg's own open-by-name readers it does meet the in-leaf debris the spec's §5.1 posture names (a foreign array prefix, a.zarr.status/prefix, editor/OS files). A key underc/that is not a chunk ordinal is skipped with a warning — never coerced to an int. - Store-scoped. The readers never traverse the hive digit tree — leaf
discovery stays with :func:
moczarr.open_hive/ the coverage MOC. Open the leaf store and pass the in-leaf array path (e.g."6/h_tdigest").
CompanionDeclaration
dataclass
One companion array's parsed §8/§9 word-typed coordinate declaration.
Source code in src/moczarr/ragged.py
339 340 341 342 343 344 345 | |
RaggedElement
dataclass
One array's parsed §1.2 element declaration.
Attributes:
| Name | Type | Description |
|---|---|---|
dtype |
dtype
|
Element dtype, coerced little-endian (the wire byte order, independent of the producing machine). |
inner_shape |
tuple of int
|
Trailing element shape; a cell decodes as |
locations |
str or None
|
Sibling array name carrying the per-row |
times |
str or None
|
Sibling array name carrying the per-row |
weights |
str
|
The §2.0 weights declaration governing the payload's weight column,
read from the spec-owned TOP-LEVEL |
Source code in src/moczarr/ragged.py
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
decode_cell(raw, element)
One cell's (n, *inner_shape) payload from its raw vlen bytes.
The §1.2 reconstruction: np.frombuffer(raw, dtype).reshape(-1,
*inner_shape) with the dtype read little-endian. None and b""
(an absent/empty cell) decode to the zero-length (0, *inner_shape)
array.
The result is a read-only, non-owning VIEW of those bytes
(flags.writeable and flags.owndata both False — parity-faithful
with zagg's _decode_cell): in-place work on a decoded payload raises,
and keeping one alive pins its span's buffer. Copy if you need either.
Source code in src/moczarr/ragged.py
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 | |
iter_populated_chunks(arr, *, span=None, spans=None)
Yield (chunk_start, [(rank, raw_bytes), ...]) per populated read chunk.
The bulk-sweep primitive shared by :func:read_ragged and the profile
readers (:mod:moczarr.hhdc). Restricted to the stored objects
(:func:stored_chunk_spans), each read in ONE slice: slicing per inner
chunk would re-fetch a sharded object's index suffix on every
__getitem__ (~K redundant GETs per shard), while the full-span slice
fetches the stored object once (zarr reads a whole outer chunk in a
single GET and splits it locally). The held cost is one span's decoded
payload. Chunks whose cells are all absent are skipped — absence is
b"" or None (:func:_is_empty, the §5.2 normalization), and
includes the §1.5 shard-index absence sentinel. rank is the cell's
chunk-local NESTED rank — the same index the writer placed it at on the
nested-ordered cells axis, never a row-major position.
span (issue #29) restricts the sweep to the stored objects
overlapping the [start, stop) cell span, clipped to whole read
chunks — the spec §1.5 contiguous-slice read plan: only the covering
portion of each overlapping object is sliced, and non-overlapping
stored objects are skipped without a fetch. The restriction is
WHOLE-CHUNK granular: a span that does not fall on chunk boundaries is
rounded OUTWARD, so the yielded chunks carry cells outside it. A
finer-than-chunk restriction is the caller's rank filter — this
generator always yields whole read chunks. spans threads in an
ALREADY-LISTED :func:stored_chunk_spans result (the one
:func:_subtree_span resolved against) so a restricted read LISTs the
array's data keys once, not twice; None lists here. Both are
keyword-only: they are adjacent, same-shaped and easy to swap, and a
swap reads as a valid call.
Source code in src/moczarr/ragged.py
569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 | |
open_ragged(store, field, *, zarr_format=3)
Open a ragged vlen array and its parsed element declaration, strictly.
Raises ValueError when the array is not a 1-D vlen-bytes cells axis,
fails the :func:parse_ragged_attrs gate, or is the :data:RAGGED2_SPEC
revision — which is ruled out BEFORE marker absence is blamed on a
pre-spec store, since a /2 array retires the marker by design and
deserves the actionable error §6.3 mandates instead. The declared data
type is read from the metadata object only on those two paths (an
unopenable dtype, or a missing marker), never on the happy one.
Source code in src/moczarr/ragged.py
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 | |
parse_companion_attrs(attrs, *, domain, field='<array>', shapes=(_COMPANION_SHAPE,))
The §8/§9 declaration gate on a companion array's own attrs.
domain is "temporal" (attrs key temporal, spec §8) or
"located" (attrs key located, spec §9) — any other value raises
ValueError naming both, and every refusal cites the section that
DEFINES the domain. Returns None when the
block is absent — never a refusal: an absent located key is §2.2
verbatim, an absent temporal key the legacy encoding (§8), and both
pre-declaration store populations are conformant as they stand. When the
block IS present it is strict-checked per §8's conformance rules: an
unknown or future spec raises, a shape outside shapes raises
(guessing never being an option), and an unimplemented grammar
raises. Keys beyond {spec, shape, grammar} are ignored rather than
refused (§9: "informative extra keys ignored").
shapes is the caller naming the §8 shape vocabulary values ITS path
implements — §8 hangs the refusal on the implementation ("a reader MUST
refuse a shape it does not implement"), which this parser cannot
know for a caller it does not decode for. The default is the ragged
sibling path's ("per-centroid",), so :func:read_ragged's two call
sites take it unchanged; a caller decoding the §8.2 dense per-cell
channel passes shapes=("per-cell",) and gets the same strict
spec/grammar gate over a declaration this module would otherwise
refuse on its sibling path's behalf.
Source code in src/moczarr/ragged.py
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 | |
parse_ragged_attrs(attrs, *, field='<array>')
The strict §1.2 gate: attrs → :class:RaggedElement, or raise.
field names the array in errors. Raises ValueError when the
ragged block is missing (not a zagg-ragged/1 array — pre-spec CSR
stores are a hard break), when spec is foreign or a future revision
(never half-parse), or when the element declaration is malformed
(shape must be [-1, *inner_shape]). The §2.0 weights gate rides
the same call (issue #43): an unknown TOP-LEVEL weights value is a
future revision of that section and raises — never read as either
defined value — while an absent key reads as "counts", so every
pre-declaration store parses verbatim. The missing-block branch
assumes the caller has already ruled out :data:RAGGED2_SPEC, whose
marker is retired by design — :func:open_ragged does that by dtype.
Source code in src/moczarr/ragged.py
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
read_cell(store, field, cell, *, zarr_format=3)
Random-access ONE cell's ragged payload, decoded per the element attrs.
Indexing the vlen array reads exactly two ranged GETs on a sharded store
— the shard-index suffix, then the one inner chunk holding the cell —
never the whole shard object (spec §1.5); one GET on the flat layout.
cell is the cell's global position on the array's cells axis (no
negative-index wrap). An absent/empty cell returns the zero-length
(0, *inner_shape) array; an out-of-range index raises IndexError
naming the valid range (zarr basic selection would silently clamp the
slice). Works on any conforming array — a companion sibling (located,
§9, or temporal, §8.3) included: both decode as (n,) uint64 words.
Source code in src/moczarr/ragged.py
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 | |
read_ragged(store, field, *, locations=False, times=False, subtree=None, zarr_format=3)
Yield every populated cell of a ragged field, decoded per its attrs.
The generic whole-store sweep: one LIST of the array's stored data
objects, then one slice per stored span (sharded and flat §1.5
geometries through the same path — no per-inner-chunk re-fetch), with
the sibling morton coordinate served from one cached window per
stored coordinate object (:class:_MortonWords) rather than re-sliced
per span. Yields (morton_word, values) per populated cell, extended
by the requested companion channels in a fixed order — (morton_word,
values, location_words) with locations=True, (morton_word,
values, time_words) with times=True, and (morton_word, values,
location_words, time_words) with both — where morton_word is the
cell's own packed uint64 coordinate from the sibling morton
array and values is the cell's decoded (n, *inner_shape)
payload. What a weight column in values MEANS is the payload's
§2.0 declaration (issue #43), surfaced as :attr:RaggedElement.weights
by :func:open_ragged: under "flux" a weight sum estimates
detected photoelectrons and recovers no observation count. An unknown
declaration refuses before any cell decodes
(:func:parse_ragged_attrs).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
Store
|
Zarr store holding the ragged array (a hive leaf store, or a flat store root). |
required |
field
|
str
|
The payload array's path (e.g. |
required |
locations
|
bool
|
Also decode the located sibling (spec §1.1/§2.2), bound by the
payload array's |
False
|
times
|
bool
|
Also decode the temporal sibling (spec §8.3) — one |
False
|
subtree
|
int or str
|
Restrict the sweep to the cells below this morton ancestor — a
packed area word ( |
None
|
zarr_format
|
int
|
Zarr format version (default 3). |
3
|
Raises:
| Type | Description |
|---|---|
ValueError
|
On the strict attrs gate (:func: |
Source code in src/moczarr/ragged.py
833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 | |
stored_chunk_spans(arr)
Cell spans (start, stop) of the array's STORED objects, ascending.
The whole-store read plan: one LIST of the array's c/<ordinal> data
keys instead of probing every chunk of a mostly-fill array. Under the
sharding_indexed codec an object spans a whole shard
(arr.shards); on the per-inner-chunk flat layout it is one read
chunk (arr.chunks) — both self-describing in the array's own
metadata, so the two §1.5 geometries share this path.
zarr.core.sync.sync runs the store's async listing on zarr's own
event loop (zarr 3 exposes no public sync listing).
Because this path LISTs, it meets in-leaf debris the spec's §5.1 posture
names as expected (a foreign array prefix, a .zarr.status/ prefix,
editor/OS files): a key under c/ that is not a chunk ordinal is
skipped with a warning, never coerced to an int. Debris is inert for
zagg's own readers because they open arrays by name; a LISTing reader has
to state the same tolerance in code.
Source code in src/moczarr/ragged.py
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 | |