Validator#
The DCA validator checks whether a Zarr dataset conforms to the DCA v0.2 specification. Given a local path or S3 URL, it validates each store against OME-NGFF v0.5 structure and then applies DCA-specific MUST and SHOULD requirements.
Multiple stores are validated in parallel.
Install#
Clone the repository and install from source:
git clone https://github.com/chanzuckerberg/dynamic-cell-atlas-specs
cd dynamic-cell-atlas-specs
uv pip install -e .
CLI#
dca-validate PATH [OPTIONS]
Arguments:
PATH Local filesystem path or s3://bucket/prefix URL.
Options:
--spec-version TEXT DCA spec version to validate against [default: 0.1]
--output [text|json] Output format [default: text]
--strict Exit 1 if any SHOULD warnings are present
--strategy [glob|walk] S3 discovery strategy [default: glob]
--check-ontology Resolve every ontology_term_id against its
source registry and check its label. Makes
network requests; off by default.
--wells TEXT Comma-separated well paths to validate,
matching plate.wells[].path exactly (e.g.
'A/1,B/2'). Requires PATH to point directly
at a single HCS plate store.
--field-offset INTEGER RANGE Skip this many fields (in declaration
order) within each selected well [default: 0]
--field-limit INTEGER RANGE Validate at most this many fields (after
--field-offset) per selected well
--list-wells List PATH's wells and their declared field
counts, then exit without validating.
--help Show this message and exit.
Pass --spec-version 0.2 to validate against the v0.2 rules.
Examples
# Validate a single store against v0.2
dca-validate path/to/image.zarr --spec-version 0.2
# Validate all stores under a directory
dca-validate path/to/experiment/ --spec-version 0.2
# Validate an S3 plate (use walk strategy for a single plate root)
dca-validate s3://my-bucket/data/plate.ome.zarr --spec-version 0.2 --strategy walk
# Validate many scattered images on S3 (use glob strategy)
dca-validate s3://my-bucket/data/ --spec-version 0.2 --strategy glob
# Emit JSON output
dca-validate s3://my-bucket/data/plate.ome.zarr --spec-version 0.2 --output json
# Fail on SHOULD warnings as well as MUST errors
dca-validate path/to/image.zarr --spec-version 0.2 --strict
# Additionally resolve every ontology term against its registry (network)
dca-validate path/to/image.zarr --spec-version 0.2 --check-ontology
# List a plate's wells and their field counts without validating
dca-validate s3://my-bucket/data/plate.ome.zarr --spec-version 0.2 --list-wells
# Validate only well A/1
dca-validate s3://my-bucket/data/plate.ome.zarr --spec-version 0.2 --wells A/1
# Chunk a huge well: validate fields 5000-9999 of well A/1
dca-validate s3://my-bucket/data/plate.ome.zarr --spec-version 0.2 \
--wells A/1 --field-offset 5000 --field-limit 5000
Well/field subsetting
For a single HCS plate whose per-well field count is too large to validate in
one run (a single well can be far larger than a typical whole plate),
--wells, --field-offset, and --field-limit scope one invocation to
a subset of the plate’s work:
--wellsrestricts which wells are opened at all. Wells not named are never opened — no checks run against them, and they are not treated as failures.--field-offset/--field-limitslice each selected well’s own field list independently (per well, not globally across wells), in the order fields are declared in that well’s metadata. This is what makes chunking work within one oversized well, not just across wells.--list-wellsis a dry run: it prints every well’s declared field count without validating anything, so you can decide how to split the work before committing to a chunking scheme. It always lists every well — combining it with--wells/--field-offset/--field-limitis a usage error rather than silently ignoring the filter. Respects--output json.
These options require PATH to point directly at a single HCS plate store
— not a discovery root — since subsetting within one already-identified plate
is orthogonal to, and mechanically incompatible with, discovering multiple
stores under a parent prefix in the same invocation.
When any of these narrow what was validated, both output formats carry an
explicit partial-run marker (with wells/fields selected vs. the total in
scope — on-disk fields across every well that got opened, which can be fewer
than a plate’s raw declared count if a field is missing or duplicated) so a
partial result is never mistaken for full plate coverage. Text output prints
a PARTIAL: ... summary line; JSON output adds "partial": true and a
"subset" object to the plate-root entry. Partial-ness is
informational only: it does not change exit-code semantics — the exit code
still reflects whether errors were found among whatever was actually
validated.
S3 discovery strategies
Two strategies are available for discovering Zarr stores under an S3 prefix:
glob— performs a single flat scan for allzarr.jsonfiles under the prefix. Better for many scattered image stores across a large prefix.walk— BFS traversal with early pruning (stops descending once a Zarr root is found). Better for a single plate or a small number of known plate roots.
Exit codes
0— all stores pass (SHOULD warnings permitted unless--strict)1— one or more stores fail, or--strictand warnings are present
Python API#
from dca_helpers.validation import validate
from dca_helpers.validation.result import ValidationRun
run: ValidationRun = validate("path/to/experiment/", spec_version="0.2")
summary = run.summary
print(
f"{summary.stores_passed}/{summary.stores_validated} stores passed "
f"({summary.level_zarr_jsons_validated} scale-level zarr.json files) "
f"in {summary.duration_seconds:.2f}s"
)
for r in run:
status = "PASS" if r.passed else "FAIL"
print(f"{status} {r.node_path}")
for issue in r.issues:
print(f" [{issue.severity.name}] {issue.message}")
validate accepts any path in the hierarchy — a single store, a subdirectory, or an
ancestor containing multiple stores. If the path has a zarr.json at its top level it
is validated directly; otherwise all Zarr stores found under the path are validated in
parallel.
The strategy parameter selects the S3 discovery strategy ("glob" or "walk");
it is ignored for local paths.
# Validate a plate on S3 using the walk strategy
run = validate(
"s3://my-bucket/data/plate.ome.zarr",
spec_version="0.2",
strategy="walk",
)
Interpreting Results#
Each entry in the ValidationRun corresponds to one validated Zarr node:
r.passed—Truewhen there are noERROR-severity issues.r.issues— list ofIssueobjects, each with aseverity(ERRORorWARNING) and amessage.r.errors— convenience filter: onlyERROR-severity issues.r.warnings— convenience filter: onlyWARNING-severity issues.
Errors correspond to MUST violations — the store fails validation. Warnings correspond to SHOULD violations — informational only, the store still passes.
from dca_helpers.validation.result import Severity
for r in run:
print(f"{r.node_path}: {len(r.errors)} error(s), {len(r.warnings)} warning(s)")
What v0.2 Validates#
The v0.2 validator applies all v0.1 array-level checks and adds new checks for multiresolution rules and channel/normalization metadata.
Array-level checks (unchanged from v0.1)#
These rules apply to every scale level (Zarr array) in both image and label stores:
Level |
Rule |
Spec section |
|---|---|---|
MUST |
Axes must be |
array-standard.rst § Dimensions |
MUST |
Uncompressed chunk size ≥ 512 KB (or equal to array size if smaller) |
array-standard.rst § Chunk Size |
MUST |
Compression codec must be |
array-standard.rst § Compression |
MUST |
Shard size < 5 TB |
array-standard.rst § Shard Size |
MUST |
If array ≥ 1 GB, shard size must be ≥ 1 GB |
array-standard.rst § Shard Size |
MUST |
Downsampling factor for |
array-standard.rst § Multiresolution |
SHOULD |
Uncompressed chunk size ≥ 1 MB |
array-standard.rst § Chunk Size |
SHOULD |
Spatial chunk sizes should be 128 × 128 × 128 (z × y × x) |
array-standard.rst § Chunk Size |
SHOULD |
Time and channel chunk sizes should be 1 |
array-standard.rst § Chunk Size |
SHOULD |
Shard size < 5 GB |
array-standard.rst § Shard Size |
SHOULD |
Spatial shard dimensions should be ≤ 2048 |
array-standard.rst § Shard Size |
SHOULD |
Time shard dimension should be ≥ 16 |
array-standard.rst § Shard Size |
SHOULD |
Integer data should use zstd (not blosc), compression level ≤ 3 |
array-standard.rst § Compression |
SHOULD |
Float data should use byte shuffle + zstd (not blosc), level ≤ 3 |
array-standard.rst § Compression |
SHOULD |
Raw image arrays should use |
array-standard.rst § Data Types |
SHOULD |
Label image arrays should use |
array-standard.rst § Data Types |
Multiresolution rule (changed from v0.1)#
v0.1 required at least 3 scale levels. v0.2 replaces this with a size-based rule:
Level |
Rule |
Spec section |
|---|---|---|
MUST |
At least one scale level must have every spatial dimension ≤ 2048 |
array-standard.rst § Multiresolution |
SHOULD |
Downsampling factor for spatial dimensions should be ≈ 2 |
array-standard.rst § Multiresolution |
DCA metadata (new in v0.2)#
v0.2 requires a dca object in the zarr.json of each image group.
Label images are exempt from dca metadata requirements.
Level |
Rule |
Spec section |
|---|---|---|
MUST |
|
channel-metadata.rst § Location in Zarr Store |
MUST |
|
channel-metadata.rst, schema.json |
MUST |
|
channel-metadata.rst, schema.json |
SHOULD |
|
channel-metadata.rst § Location in Zarr Store |
Channel metadata (new in v0.2)#
Each entry in dca.channels is validated as follows:
Level |
Rule |
Spec section |
|---|---|---|
MUST |
Each channel must have |
channel-metadata.rst § Required Fields |
MUST |
Each channel must have |
channel-metadata.rst § Required Fields |
MUST |
Each channel must have |
channel-metadata.rst § Required Fields |
MUST |
Channel |
channel-metadata.rst § Required Fields |
SHOULD |
|
channel-metadata.rst § Guidance on Channel Type |
SHOULD |
|
channel-metadata.rst § Optional Fields |
SHOULD |
|
channel-metadata.rst § Guidance on Marker Type Vocabulary |
SHOULD |
|
channel-metadata.rst § Biological Annotation Entries |
SHOULD |
|
channel-metadata.rst § Biological Annotation Entries |
Normalization statistics (new in v0.2)#
dca.normalization_statistics is validated as follows:
Level |
Rule |
Spec section |
|---|---|---|
MUST |
Keys must be channel index strings (e.g. |
normalization-statistics.rst, schema.json |
MUST |
Each channel entry must have |
normalization-statistics.rst § Channel Normalization Fields |
MUST |
|
normalization-statistics.rst § Required Fields |
Experimental metadata (new in v0.2)#
The v0.2 dca block also carries the experimental-metadata record, mirrored from the
curated Parquet (the source of truth). Its required floor (study,
biosample, specimen, study_component, provenance) is the validated
part; recommended and optional fields are carried in the same block and are not
enforced. The validator extracts the floor
blocks from the dca object and validates them against the generated Pydantic
models (helpers/src/dca_helpers/validation/versions/v0_2/_generated_models.py), which are
produced from the
LinkML schema — the single
source of truth for the experimental metadata. The generated base enforces the required
floor, types, enums, and CURIE-shape patterns; the conditional if/then rules below
(gen-pydantic records these only as inert metadata) are re-stated as model_validator
subclasses in validation/versions/v0_2/experimental_metadata.py. Most violations are
MUST (ERROR), reported under a dca location prefix; the one exception is
provenance-coverage (see below). Label images are exempt.
Note
The committed JSON Schema (…/dca_experimental_metadata.schema.json) carries the
identical rules and is exercised by the test suite as a cross-check, but it is not
on the runtime path — the validator enforces the Pydantic models. Editing the JSON
Schema by hand has no effect on validation; change the LinkML source and run
make schema.
Level |
Rule |
Spec section |
|---|---|---|
MUST |
Structure — object nesting, field types, and numeric bounds |
experimental-metadata.rst § Conformance |
MUST |
Required floor present — |
experimental-metadata.rst § Conformance |
MUST |
|
experimental-metadata.rst § Conformance |
MUST |
|
experimental-metadata.rst § Conformance |
MUST |
Cross-placement agreement — every field of view under one well must carry an identical |
experimental-metadata.rst § Where it is written |
MUST |
Controlled-vocabulary enums ( |
experimental-metadata.rst § Conformance |
MUST |
CURIE shape on ontology-id fields (e.g. |
experimental-metadata.rst § Conformance |
MUST |
|
experimental-metadata.rst § Conformance |
MUST |
|
experimental-metadata.rst § Conformance |
MUST |
a JUMP-CP |
experimental-metadata.rst § Conformance |
MUST |
|
experimental-metadata.rst § Conformance |
MUST |
a non- |
experimental-metadata.rst § Conformance |
MUST |
|
experimental-metadata.rst § Conformance |
MUST |
Provenance coverage — an orphan |
experimental-metadata.rst § Conformance |
SHOULD |
Provenance coverage — every populated field should carry a |
experimental-metadata.rst § Conformance |
SHOULD |
A sentinel |
experimental-metadata.rst § What is and is not machine-checked |
SHOULD |
|
experimental-metadata.rst § What is and is not machine-checked |
SHOULD |
For |
experimental-metadata.rst § What is and is not machine-checked |
Provenance coverage is a cross-field check (the set of provenance keys vs. the
populated fields) that LinkML rules cannot express, so it is computed by the validator
alongside the model rules rather than by the schema. The two cross-slot SHOULDs above
are there for the same reason: a LinkML rule is a per-slot predicate and cannot compare
one slot against another. Both compare fields already present on the same Biosample,
so neither needs to resolve a term.
time_increment_s is also conditionally required (for time-lapse data), but its
condition is the zarr.json array shape rather than a metadata field, so it is checked
by the validator directly rather than by the generated schema — the one conditional
requirement outside the JSON Schema.
What the validator does not check
Term existence — the CURIE patterns check shape (correct prefix, well-formed id), not that the term is real; confirming a term against pinned ontology versions is a deferred layer.
Mappings — the
exact_mappingsare cross-references for interop / projection, not constraints.Free-text fields — unconstrained.
Grounding re-verification —
source_kind: agentrequires acitation(enforced above), but re-checking that the quote actually appears in the source needs the raw source pages (not shipped with the store), so it is deferred; downstream trusts the carriedmatch_status.Whether a ``perturbation`` or ``well_perturbation`` block should be there at all — “this dataset has perturbations” is a fact about the experiment, not about the store, so no check can distinguish an unperturbed dataset from a screen whose curator forgot the block. The validator enforces each block’s contents whenever it is present and never requires its presence; an absent block is always accepted at either placement (a possible future opt-in requirement).
Regenerating the schema artifacts#
The JSON Schema and Pydantic models are generated from the LinkML source and committed.
After editing schema/v0.2/dca_experimental_metadata.yaml, regenerate both:
make schema
This runs gen-json-schema --no-metadata (→
schema/v0.2/generated/dca_experimental_metadata.schema.json, the test-suite cross-check)
and gen-pydantic (→
helpers/src/dca_helpers/validation/versions/v0_2/_generated_models.py, the models the
validator enforces at runtime, can also be used by an agent and downstream consumers;
committed inside the package so it ships with any install). CI fails if the committed
artifacts drift from the schema, so commit the regenerated files alongside any schema
change.
S3 Access#
Set AWS credentials before calling validate:
AWS_PROFILE=my-profile dca-validate s3://my-bucket/data/plate.ome.zarr --spec-version 0.2
Or in Python:
import os
os.environ["AWS_PROFILE"] = "my-profile"
from dca_helpers.validation import validate
run = validate("s3://my-bucket/data/plate.ome.zarr", spec_version="0.2")
Note
For large HCS plates with thousands of wells, point validate at the plate root
directly and use strategy="walk" rather than a parent directory.
Recursive S3 discovery over thousands of zarr.json files can exhaust session
credentials before completing. Validating the plate root also runs structural checks
(e.g. verifying all declared wells exist as valid Zarr groups).