Skip to content

Commit 164370d

Browse files
authored
Add sel helper for named-dimension selection (#382)
Zarr v3 arrays can have `dimension_names` in their metadata, but `get` and `set` only accept positional selection arrays. This means users need to know the exact dimension order and manually map names to indices, which is error-prone and tightly couples code to a specific array layout. `sel` converts a record of dimension names to slices/indices into the positional array that `get`/`set` already accept, using the array's `dimensionNames` metadata. Unspecified dimensions default to `null` (select all). Unknown dimension names throw immediately to catch typos. ```ts let arr = await zarr.open(store, { kind: "array" }); // arr.dimensionNames -> ["time", "lat", "lon"] let selection = zarr.sel(arr, { lat: zarr.slice(100, 200), time: 0 }); // -> [0, slice(100, 200), null] let result = await zarr.get(arr, selection); ```
1 parent 5038e34 commit 164370d

4 files changed

Lines changed: 80 additions & 0 deletions

File tree

.changeset/add-sel-helper.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"zarrita": minor
3+
---
4+
5+
Add `sel` helper for named-dimension selection. Converts a record of dimension names to the positional selection array that `get` and `set` accept, using the array's `dimensionNames` metadata. Throws for unknown dimension names.
6+
7+
```ts
8+
let arr = await zarr.open(store, { kind: "array" });
9+
// arr.dimensionNames -> ["time", "lat", "lon"]
10+
11+
let selection = zarr.sel(arr, { lat: zarr.slice(100, 200), time: 0 });
12+
// -> [0, slice(100, 200), null]
13+
14+
let result = await zarr.get(arr, selection);
15+
```

packages/zarrita/__tests__/util.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
2+
import * as zarr from "../src/index.js";
3+
import { sel, slice } from "../src/indexing/util.js";
24
import type { ArrayMetadata, DataType } from "../src/metadata.js";
35
import {
46
BoolArray,
@@ -264,6 +266,46 @@ describe("ensure_correct_scalar", () => {
264266
});
265267
});
266268

269+
describe("sel", () => {
270+
async function make_array(dimension_names?: string[]) {
271+
let h = zarr.root();
272+
return zarr.create(h.resolve("/test"), {
273+
shape: [100, 200, 300],
274+
chunk_shape: [10, 20, 30],
275+
data_type: "float32",
276+
dimension_names,
277+
});
278+
}
279+
280+
test("maps named dimensions to positional selection", async () => {
281+
let arr = await make_array(["time", "lat", "lon"]);
282+
expect(sel(arr, { lat: slice(10, 20), time: 0 })).toStrictEqual([
283+
0,
284+
{ start: 10, stop: 20, step: null },
285+
null,
286+
]);
287+
});
288+
289+
test("unspecified dimensions default to null", async () => {
290+
let arr = await make_array(["time", "lat", "lon"]);
291+
expect(sel(arr, { lon: 5 })).toStrictEqual([null, null, 5]);
292+
});
293+
294+
test("throws for unknown dimension name", async () => {
295+
let arr = await make_array(["time", "lat", "lon"]);
296+
expect(() => sel(arr, { bad: 0 })).toThrowError(
297+
/Unknown dimension name: "bad"/,
298+
);
299+
});
300+
301+
test("throws when array has no dimension_names", async () => {
302+
let arr = await make_array();
303+
expect(() => sel(arr, { time: 0 })).toThrowError(
304+
/does not have dimension_names/,
305+
);
306+
});
307+
});
308+
267309
describe("v2_to_v3_array_metadata", () => {
268310
let v2meta = {
269311
zarr_format: 2 as const,

packages/zarrita/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type {
2626
Slice,
2727
} from "./indexing/types.js";
2828
export {
29+
sel,
2930
slice,
3031
slice_indices as _zarrita_internal_slice_indices,
3132
} from "./indexing/util.js";

packages/zarrita/src/indexing/util.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { Readable } from "@zarrita/storage";
2+
import type { Array as ZarrArray } from "../hierarchy.js";
3+
import type { DataType } from "../metadata.js";
14
import type { ChunkQueue, Indices, Slice } from "./types.js";
25

36
/** Similar to python's `range` function. Supports positive ranges only. */
@@ -132,6 +135,25 @@ export function slice(
132135
};
133136
}
134137

138+
/** @category Utility */
139+
export function sel<D extends DataType, Store extends Readable>(
140+
arr: ZarrArray<D, Store>,
141+
selection: Record<string, Slice | number | null>,
142+
): (Slice | number | null)[] {
143+
let names = arr.dimensionNames;
144+
if (!names) {
145+
throw new Error("Array does not have dimension_names in its metadata.");
146+
}
147+
for (let key of Object.keys(selection)) {
148+
if (!names.includes(key)) {
149+
throw new Error(
150+
`Unknown dimension name: "${key}". Available dimensions: ${names.map((n) => `"${n}"`).join(", ")}`,
151+
);
152+
}
153+
}
154+
return names.map((name) => selection[name] ?? null);
155+
}
156+
135157
/** Built-in "queue" for awaiting promises. */
136158
export function create_queue(): ChunkQueue {
137159
const promises: Promise<void>[] = [];

0 commit comments

Comments
 (0)