Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions web/src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,17 @@
// one chunk, as the routes serve it. Its OME metadata follows worldarray's ome_multiscales
// (web/test/metadata.test.ts runs worldarray's fixture of it), and it imports only what
// Node's tests can run.
import { spatialAxes } from "./placement.ts";
import { axisTypes, TO_SECONDS } from "./placement.ts";
import type { ViewAxis, ViewInfo } from "./types.ts";

export const TO_SECONDS: Record<string, number> = { s: 1, second: 1, millisecond: 1e-3, ms: 1e-3, minute: 60, hour: 3600, day: 86400 };
export { TO_SECONDS };
const OME_UNIT: Record<string, string> = { nm: "nanometer", um: "micrometer", "µm": "micrometer", m: "meter", s: "second" };
/** zarr v2's dtypes, by numpy's name (the engine's dtypes); little endian. */
export const V2_DTYPE: Record<string, string> = {
uint8: "|u1", int8: "|i1", uint16: "<u2", int16: "<i2", uint32: "<u4", int32: "<i4", uint64: "<u8", int64: "<i8", float32: "<f4", float64: "<f8",
};
export const isTime = (a: ViewAxis) => a.unit in TO_SECONDS;

/** Each axis's OME type, as worldarray types it: time when named so or in a time unit,
* channel when named c, space when spatial (named z, y or x, else the last three), else
* none. */
function axisTypes(axes: ViewAxis[]): (string | undefined)[] {
const spatial = new Set(spatialAxes(axes.map((a) => a.name)));
return axes.map((a, i) => (a.name === "t" || a.name === "time" || isTime(a) ? "time" : a.name === "c" ? "channel" : spatial.has(i) ? "space" : undefined));
}

/** A level of a view as one of a group's datasets: its path in the group, and which of the
* view's levels it is; `lo` (a box of the level, materialize.ts): the level's voxel that is
* the dataset's voxel 0, on the level's own axes (none: the level's voxel 0). */
Expand All @@ -46,7 +38,7 @@ export const translation = (v: ViewInfo, level: number, lo?: number[]) => {
* lands where the view does; a dataset that starts at voxel `lo` of its level is translated
* by `lo` voxels more, so its voxel 0 lands on the level's voxel `lo`. */
export function multiscale(name: string, v: ViewInfo, datasets: Dataset[]) {
const added = (v.added ?? []).map(() => 0), types = axisTypes(v.axes);
const added = (v.added ?? []).map(() => 0), types = axisTypes(v.axes.map((a) => a.name), v.axes.map((a) => a.unit));
return {
name,
axes: [...added.map(() => ({ name: "c", type: "channel" })),
Expand Down
12 changes: 6 additions & 6 deletions web/src/ome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// sharded or not, with the usual codecs (zstd and blosc through numcodecs).
import * as zarr from "zarrita";
import { asReals, TYPED, type Numbers, type Reals, type Settable, type TypedCtor } from "./asread";
import { multiscalesOf, placement, placeZarr, type Attrs, type Placement } from "./placement";
import { axisTypes, multiscalesOf, placement, placeZarr, type Attrs, type Placement } from "./placement";
import type { ControlGrid } from "./types";
import { fetchUrl } from "./urls";

Expand Down Expand Up @@ -46,7 +46,7 @@ export function fetchStore(url: string): zarr.FetchStore {
* worldarray places it (placement.ts). */
export async function openImage(url: string): Promise<Image> {
const root = zarr.root(fetchStore(url.replace(/\/+$/, "")));
const attrs = (await zarr.open(root, { kind: "group" })).attrs as Attrs;
const attrs = ((await zarr.open(root, { kind: "group" })).attrs ?? {}) as Attrs;
const ms = multiscalesOf(attrs)[0];
if (!ms) throw new Error(`${url}: no OME-Zarr multiscales metadata`);
const levels = await Promise.all((ms.datasets as { path: string }[]).map(async (d) => {
Expand All @@ -58,13 +58,13 @@ export async function openImage(url: string): Promise<Image> {
return img;
}

/** An image of `levels`, each an array and where it is placed; its axes and units are the
* first level's. */
/** An image of `levels`, each an array and where it is placed; its axes, their units and
* OME types (which pages writing OME metadata for it keep) are the first level's. */
export function image(url: string, levels: { arr: zarr.Array<zarr.DataType, zarr.Readable>; placed: Placement }[]): Image {
const { axes: names, units } = levels[0].placed;
const { axes: names, units } = levels[0].placed, types = axisTypes(names, units);
return {
url: url.replace(/\/+$/, ""), names, dtype: levels[0].arr.dtype, lead: names.length - 3,
axes: names.map((name, a) => (units[a] ? { name, unit: units[a] } : { name })),
axes: names.map((name, a) => ({ name, ...(types[a] ? { type: types[a] } : {}), ...(units[a] ? { unit: units[a] } : {}) })),
levels: levels.map(({ arr, placed: { voxel, origin } }) => ({
arr, fullShape: arr.shape, scale: voxel, shift: origin, shape: arr.shape.slice(-3), voxel: voxel.slice(-3), origin: origin.slice(-3),
})),
Expand Down
14 changes: 13 additions & 1 deletion web/src/placement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export function spatialAxes(axes: string[]): number[] {
return named.length ? named : axes.map((_, i) => i).slice(-3);
}

/** Seconds in each time unit the browser knows. */
export const TO_SECONDS: Record<string, number> = { s: 1, second: 1, millisecond: 1e-3, ms: 1e-3, minute: 60, hour: 3600, day: 86400 };

/** Each axis's OME type, as worldarray types it: time when named t or time or in a time
* unit, channel when named c, space when spatial (named z, y or x, else the last three),
* else none. */
export function axisTypes(names: string[], units: string[]): (string | undefined)[] {
const spatial = new Set(spatialAxes(names));
return names.map((a, i) => (a === "t" || a === "time" || units[i] in TO_SECONDS ? "time" : a === "c" ? "channel" : spatial.has(i) ? "space" : undefined));
}

/** Voxel 0's centre from its corner: half a voxel on along the spatial axes. */
export function cornerToCentre(corner: number[], voxel: number[], axes: string[] | null): number[] {
const sp = new Set(spatialAxes(axes ?? defaultAxes(corner.length)));
Expand Down Expand Up @@ -124,7 +135,8 @@ function arrayAttrs(attrs: Attrs, n: number): Found {
/** A zarr array: the parent group's OME multiscales listing it, else its own funlib or COSEM
* attributes, else funlib's keys on the parent; its axes named by _ARRAY_DIMENSIONS or zarr
* v3's dimension_names where nothing else names them. */
export function placeZarr(attrs: Attrs, group: Attrs, name: string, n: number, dimensionNames?: (string | null)[] | null): Found {
export function placeZarr(attrs: Attrs | undefined, group: Attrs, name: string, n: number, dimensionNames?: (string | null)[] | null): Found {
attrs ??= {}; // an array with no attributes of its own
const hit = datasetFor(group, name);
let found = hit ? ome(hit[0], hit[1], n) : none();
if (!found.voxel) {
Expand Down
4 changes: 2 additions & 2 deletions web/src/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ async function arraySource(url: string, select: Record<string, number>): Promise
const base = url.replace(/\/+$/, ""), cut = base.lastIndexOf("/"), parent = base.slice(0, cut);
const arr = await zarr.open(zarr.root(fetchStore(base)), { kind: "array" }), n = arr.shape.length;
if (n < 3) throw new Error(`${url}: an array of ${n} axes; three or more are read`);
const group = await zarr.open(zarr.root(fetchStore(parent)), { kind: "group" }).then((g) => g.attrs as Attrs, () => ({}));
const attrs = arr.attrs as Attrs, placed = placement(placeZarr(attrs, group, base.slice(cut + 1), n, arr.dimensionNames), n);
const group = await zarr.open(zarr.root(fetchStore(parent)), { kind: "group" }).then((g) => (g.attrs ?? {}) as Attrs, () => ({}));
const attrs = (arr.attrs ?? {}) as Attrs, placed = placement(placeZarr(attrs, group, base.slice(cut + 1), n, arr.dimensionNames), n);
if (!placed.placedBy && placed.named) {
const coords = await Promise.all(placed.axes.map((d, a) => (a < n - 3 ? null : coordinate(parent, d, arr.shape[a]))));
coords.forEach((c, a) => { if (c) { placed.voxel[a] = c.step; placed.origin[a] = c.first; placed.units[a] = c.unit; } });
Expand Down
9 changes: 8 additions & 1 deletion web/test/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { attrs2, group3 } from "../src/layout.ts";
import { placement, placeN5, placePrecomputed, placeZarr, type Attrs } from "../src/placement.ts";
import { axisTypes, placement, placeN5, placePrecomputed, placeZarr, type Attrs } from "../src/placement.ts";
import type { ViewInfo } from "../src/types.ts";

const fixture = (name: string): Attrs[] => JSON.parse(readFileSync(new URL(`fixtures/worldarray/${name}`, import.meta.url), "utf8")).cases;
Expand Down Expand Up @@ -54,3 +54,10 @@ for (const c of fixture("write.json").filter((c) => !c.multiscale_type && !c.met
assert.deepEqual(JSON.parse(JSON.stringify(got)), c.expect);
});
}

test("an array with no attributes of its own is placed by its group, its axes typed for OME", () => {
const group = { multiscales: [{ axes: ["t", "c", "z", "y", "x"], datasets: [{ path: "0", coordinateTransformations: [{ type: "scale", scale: [1, 1, 4, 2, 2] }] }] }] };
const p = placement(placeZarr(undefined, group, "0", 5), 5); // zarrita gives an array without .zattrs no attrs
assert.deepEqual([p.voxel, p.placedBy], [[1, 1, 4, 2, 2], "ome"]);
assert.deepEqual(axisTypes(p.axes, p.units), ["time", "channel", "space", "space", "space"]); // the register page writes them
});
Loading