canvod.store API reference¶
Versioned data storage using Icechunk.
Package¶
canvod-store: Icechunk storage for GNSS VOD data.
This package provides versioned storage for GNSS VOD data using Icechunk. Manages both RINEX observation storage and VOD analysis results, and stores hemisphere grids and the analysis results computed on them.
MyIcechunkStore
¶
Core Icechunk store manager for GNSS data.
This class encapsulates all operations on a single Icechunk repository, providing a clean interface for GNSS data storage and retrieval with integrated logging and proper resource management.
Features: - Automatic repository creation/connection - Group management with validation - Session management with context managers - Integrated logging with file contexts - Configurable compression and chunking
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
StoreLocation | Path | str
|
The store: a local directory or |
required |
store_type
|
str
|
Type of store ("gnss_store" or "vod_store"). |
"gnss_store"
|
compression_level
|
int | None
|
Override default compression level. |
None
|
compression_algorithm
|
str | None
|
Override default compression algorithm. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
location |
StoreLocation
|
Where the store lives. |
store_path |
Path | str
|
The store's local directory, or its |
store_type |
str
|
Type of store ("gnss_store" or "vod_store"). |
compression_level |
int
|
Compression level (1-9). |
compression_algorithm |
CompressionAlgorithm
|
Compression algorithm enum. |
repo |
Repository
|
The Icechunk repository instance. |
settings
property
¶
The settings the store writes with.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store was opened without settings. Reading needs none; every write does. |
repo
property
¶
Get the repository instance.
site_name
property
¶
The site this store was written for, from its repository metadata.
Never the name of the directory the store sits in, so moving or renaming the directory doesn't change it.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the store has no repository metadata. |
source_format
property
¶
Return the source_format root attribute, or None.
tree
property
¶
Print hierarchical tree of all branches and groups. See print_tree.
__init__(store_path, store_type='gnss_store', compression_level=None, compression_algorithm=None, *, config=None)
¶
Initialize the Icechunk store manager.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
StoreLocation | Path | str
|
The store: a local directory, |
required |
store_type
|
str
|
Type of store ("gnss_store" or "vod_store"). |
"gnss_store"
|
compression_level
|
int | None
|
Override default compression level. |
None
|
compression_algorithm
|
str | None
|
Override default compression algorithm. |
None
|
config
|
CanvodConfig | None
|
The settings the store writes with; every commit records them.
|
None
|
chunk_encoding_for(ds)
¶
Build {var: {"chunks": (...)}} matching self.chunk_strategy.
Only meaningful at group creation -- Zarr fixes an array's chunk
shape the moment it's first written, so this must be passed as
encoding= on the mode="w" call, never on an append (which
must match whatever shape creation already fixed).
Without this, chunk_strategies config only ever affected
read-side chunking (the chunks= hint passed to
xr.open_zarr elsewhere in this class) -- nothing applied it at
write time, so the physical on-disk chunk shape was left to Zarr's
own default, silently disconnected from config. Confirmed by
grepping this file and processor.py for any encoding["chunks"]
assignment: there wasn't one anywhere (2026-07-18).
A dim with a configured chunk size (e.g. epoch: 17280) gets
exactly that size regardless of how much data is in this
particular write -- Zarr supports writing less than one full chunk
in the first call and filling the rest on later appends, which is
the whole point of fixing chunk shape once at creation. A dim
configured -1 (or unconfigured) gets one chunk spanning
whatever this write's current size is (e.g. sid: -1 -> the
full SID universe already present at creation).
readonly_session(branch='main')
¶
Thin wrapper over repo.readonly_session() for this CM.
writable_session(branch='main')
¶
Thin wrapper over repo.writable_session() for this CM.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store was opened without settings. |
set_root_attrs(attrs, branch='main')
¶
Set root-level Zarr attributes on the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attrs
|
dict[str, Any]
|
Key-value pairs to merge into root attrs. |
required |
branch
|
str
|
Branch to write to. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID from the commit. |
get_root_attrs(branch='main')
¶
Read root-level Zarr attributes from the store.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Root attributes (empty dict if none set). |
get_branch_names()
¶
List all branches in the store.
Returns:
| Type | Description |
|---|---|
list[str]
|
List of branch names. |
get_group_names(branch=None)
¶
List all groups in the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str | None
|
Repository branch to examine. Defaults to listing groups from all branches. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, list[str]]
|
Dictionary mapping branch names to lists of group names. |
list_groups(branch='main')
¶
List all groups in a branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Branch name (default: "main") |
'main'
|
Returns:
| Type | Description |
|---|---|
list[str]
|
List of group names in the branch |
list_data_groups(branch='main')
¶
List the groups that hold data, at any depth.
In a GNSS store these are the receiver groups, as
:meth:list_groups gives them; in a VOD store each analysis sits
below its calculator (tau_omega/canopy_01_vs_reference_01).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Branch name (default: "main") |
'main'
|
Returns:
| Type | Description |
|---|---|
list[str]
|
The paths of the groups, sorted. |
print_tree(max_depth=None)
¶
Display hierarchical tree of all branches, groups, and subgroups.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_depth
|
int | None
|
Maximum depth to display. None for unlimited depth. - 0: Only show branches - 1: Show branches and top-level groups - 2: Show branches, groups, and first level of subgroups/arrays - etc. |
None
|
group_exists(group_name, branch='main')
¶
Check if a group exists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the group to check. May be a nested path (e.g. VOD's
|
required |
branch
|
str
|
Repository branch to examine. |
"main"
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the group exists, False otherwise. |
Notes
Checks membership directly against the opened root group rather
than via :meth:get_group_names/:meth:list_groups -- those only
enumerate top-level Zarr group keys (root.group_keys()), so
for a nested group_name like a VOD analysis group, membership
in that flat list is always False even when the group exists.
Confirmed 2026-07-21: this previously caused every VOD write after
a group's first to take the mode="w" (full overwrite) branch in
:meth:write_or_append_vod_group instead of appending, silently
destroying all previously-written VOD data on each subsequent
write. Zarr's Group.__contains__ supports slash-separated
nested paths directly, so this works for both flat GNSS receiver
names (no behavior change there) and nested VOD group names (the
actual fix).
preprocessing_records(group_name, start=None, end=None, branch='main')
¶
Preprocessing records in group_name's log book, without repeats.
In ingest order; rows written before the record existed give "".
Each file row's record includes the steps of the group-wide rows
written after it (:meth:add_group_variables): it describes the
data as they are now.
With start and end, only rows whose data overlap that range,
without rows of files skipped as already ingested (as
:meth:source_file_hashes). A group without a log book gives none.
check_preprocessing_matches(group_name, dataset, branch='main')
¶
Refuse data preprocessed differently from the data in group_name.
Compares the operations and settings of the record dataset
carries (:data:~canvod.config.models.PREPROCESSING_ATTR; none
counts as no preprocessing) with every record in the group's log
book. The results of the operations (e.g. the input sampling) may
differ. In a VOD store, the records are those of the GNSS data
behind each result (vod_preprocessing_record).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Store group about to receive |
required |
dataset
|
Dataset
|
Data to write. |
required |
branch
|
str
|
Repository branch to examine. |
"main"
|
Raises:
| Type | Description |
|---|---|
PreprocessingMismatchError
|
If the group holds data preprocessed differently. |
check_settings_match(group_name, settings, source='this run has', branch='main')
¶
Refuse preprocessing settings unlike those of the data in group_name.
Lets a run compare its configured preprocessing (see
canvod.ops.planned_settings) with a group before it processes
any data; :meth:check_preprocessing_matches applies the same
comparison to a dataset at write time. A group that does not exist
yet accepts any settings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Store group the data will be written to. |
required |
settings
|
list[dict]
|
Operations and settings, as
|
required |
source
|
str
|
Names the new data in the error message. |
"this run has"
|
branch
|
str
|
Repository branch to examine. |
"main"
|
Raises:
| Type | Description |
|---|---|
PreprocessingMismatchError
|
If the group holds data preprocessed differently. |
add_group_variables(group_name, variables, step, software, branch='main')
¶
Add variables computed from the data already in a group.
For example the grid cells of every observation
(canvod.ops.assign_grid_cells_in_store). Only the new variables are
written; the group's other variables, its attributes, its log book and
its other subgroups stay as they are. The write is staged on a
temporary branch and moved to branch with :meth:promote_branch.
The log book receives one group-wide row (:data:GROUP_WIDE_ACTION)
whose preprocessing record holds step. From then on, the
preprocessing of the data already in the group counts as their own
plus step (:meth:preprocessing_records), so a later write is
accepted only if it was preprocessed with step as well, and
thus carries the new variables too.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Store group, e.g. |
required |
variables
|
Dataset
|
The new variables, with the group's dimensions and coordinate
values as :meth: |
required |
step
|
dict
|
The operation that computed them, as a preprocessing step
|
required |
software
|
dict[str, str]
|
Version of each package whose code computed them. |
required |
branch
|
str
|
Branch to change. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
The new tip of |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the group does not exist, already has one of the variables,
or |
BranchMovedError
|
If |
read_group(group_name, branch='main', time_slice=None, date=None, chunks=None, drop_empty=False)
¶
Read data from a group, its epochs in time order.
A group made of day blocks (ADR 0016) has a slot for every epoch of
a day at the day's cadence; slots without data hold missing values
(NaN, or the _FillValue of integer variables) at their grid
times.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the group to read. |
required |
branch
|
str
|
Repository branch. |
"main"
|
time_slice
|
slice | None
|
Optional label-based time slice for filtering (passed to |
None
|
date
|
str | None
|
YYYYDOY string (e.g. |
None
|
chunks
|
dict[str, Any] | None
|
Dask chunks for reading. If None, the chunks stored in the group. |
None
|
drop_empty
|
bool
|
Drop the empty slots of the epochs read, after |
False
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset from the group. |
write_dataset(dataset, group_name, session, mode='a', chunks=None)
¶
Write a dataset to Icechunk with proper chunking.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset
|
Dataset
|
Dataset to write |
required |
group_name
|
str
|
Group path in store |
required |
session
|
Any
|
Active writable session or store handle. |
required |
mode
|
str
|
Write mode: 'w' (overwrite) or 'a' (append) |
'a'
|
chunks
|
dict[str, int] | None
|
Chunking spec. If None, uses store's chunk_strategy. Example: {'epoch': 17280, 'sid': -1} |
None
|
grow_day_blocks(days, *, message, branch='main')
¶
Give days a block in existing day-block groups, in one commit.
A run calls this once per batch, before reading files, for the days whose Phase 1 succeeded (ADR 0016). A day that has a block keeps it, so a block grown for a day that failed later is reused on the next run. When another commit changed the branch meanwhile and the two conflict, the growth starts again on the new tip: it rereads the epoch axis, reuses the blocks added meanwhile and grows the rest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
Mapping[str, Mapping[datetime64, float]]
|
For each group, the cadence in seconds of each day to grow. |
required |
message
|
str
|
Commit message. |
required |
branch
|
str
|
Branch to commit to. |
"main"
|
Returns:
| Type | Description |
|---|---|
str or None
|
Snapshot ID of the growth commit; |
Raises:
| Type | Description |
|---|---|
LayoutError
|
If a group does not exist (its first write creates it) or is not made of day blocks. |
CadenceError
|
If a cadence does not fit the chunks, or a day has a block at a coarser cadence. |
RuntimeError
|
If the growth still conflicts after |
backup_metadata_table(group_name, session)
¶
Backup the metadata table to a Polars DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group name. |
required |
session
|
Any
|
Active session for reading. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame | None
|
DataFrame with metadata rows, or None if missing. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the table exists but cannot be read. |
restore_metadata_table(group_name, df, session)
¶
Restore the metadata table from a Polars DataFrame.
This recreates the full Zarr structure for the metadata table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group name. |
required |
df
|
DataFrame
|
Metadata table to restore. |
required |
session
|
Any
|
Active session for writing. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
rewrite_group(session, group_name, dataset, drop_ranges, metadata_parts=None)
¶
Rewrite group_name with dataset and keep its metadata/.
A mode="w" write deletes the group's subgroups. The log book
comes back unchanged; every other metadata dataset (e.g. sbf_obs)
comes back from the session's base snapshot without its epochs
inside drop_ranges (the replaced data), merged in epoch order with
the datasets in metadata_parts (name -> per-file datasets). A
metadata dataset left without epochs is not written back. Writes
into session without committing.
Raises:
| Type | Description |
|---|---|
LayoutError
|
If the group is made of day blocks: its files are written into their day's block instead, and a whole-group write would drop its layout and block index. |
get_group_info(group_name, branch='main')
¶
Get metadata about a group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the group. |
required |
branch
|
str
|
Repository branch to examine. |
"main"
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Group metadata. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the group does not exist. |
metadata_dataset_exists(group_name, name, branch='main')
¶
Return True if a metadata dataset name exists for group_name.
write_metadata_dataset(meta_ds, group_name, name, branch='main')
¶
Write a pre-concatenated metadata dataset to {group_name}/metadata/{name}.
Always writes with mode="w" (full overwrite for the day).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
meta_ds
|
Dataset
|
Pre-concatenated |
required |
group_name
|
str
|
Target group (receiver name). |
required |
name
|
str
|
Dataset name under |
required |
branch
|
str
|
Repository branch to write to. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
write_metadata_parts(parts, group_name, name, session)
¶
Add per-file metadata datasets to {group_name}/metadata/{name}.
Writes into session without committing, so the caller commits the
metadata together with the observations it belongs to. Creates the
dataset if it does not exist yet, otherwise appends along epoch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parts
|
list[Dataset]
|
Per-file metadata datasets with an |
required |
group_name
|
str
|
Target group (the observations' store group). |
required |
name
|
str
|
Dataset name under |
required |
session
|
Session or ForkSession
|
Open writable session of the observations' write. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of epochs written. |
append_metadata_datasets(parts, group_name, name, branch='main')
¶
Append metadata datasets in their own commit: no in-memory concat.
Creates {group_name}/metadata/{name} if it does not exist yet,
otherwise appends along epoch. Prefer
:meth:write_metadata_parts inside the session that writes the
observations, so both land in one commit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parts
|
list[Dataset]
|
Individual per-file metadata datasets with an |
required |
group_name
|
str
|
Target group (receiver name). |
required |
name
|
str
|
Dataset name under |
required |
branch
|
str
|
Repository branch to write to. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
read_metadata_dataset(group_name, name, branch='main', chunks=None)
¶
Read a metadata dataset name for group_name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group (receiver) name. |
required |
name
|
str
|
Dataset name under |
required |
branch
|
str
|
Repository branch. |
"main"
|
chunks
|
dict | None
|
Dask chunks for reading. If None, the chunks stored in the dataset. |
None
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Lazy |
get_metadata_dataset_info(group_name, name, branch='main')
¶
Get info about metadata dataset name for group_name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group (receiver) name. |
required |
name
|
str
|
Dataset name under |
required |
branch
|
str
|
Repository branch. |
"main"
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Info dict with the same structure as |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the metadata dataset does not exist. |
sbf_metadata_exists(group_name, branch='main')
¶
Return True if an SBF metadata dataset exists for group_name.
write_sbf_metadata(meta_ds, group_name, branch='main')
¶
Write SBF metadata dataset.
read_sbf_metadata(group_name, branch='main', chunks=None)
¶
Read SBF metadata dataset.
get_sbf_metadata_info(group_name, branch='main')
¶
Get SBF metadata info.
rel_path_for_commit(file_path)
¶
Generate relative path for commit messages.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path
|
Path
|
Full file path. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Relative path string with log_path_depth parts. |
get_store_stats()
¶
Get statistics about the store.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Store statistics. |
append_metadata(group_name, rinex_hash, start, end, snapshot_id, action, commit_msg, dataset_attrs, branch='main', canonical_name=None, physical_path=None)
¶
Standalone metadata-row append: commits on its own.
For ingest paths (write_initial_group, append_to_group) use _append_metadata_row directly inside the data-write session to avoid an extra commit per file.
append_metadata_bulk(group_name, rows, session=None)
¶
Append multiple metadata rows in one commit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group name (e.g. "canopy", "reference") |
required |
rows
|
list[dict[str, Any]]
|
List of metadata records matching the schema used in append_metadata(). |
required |
session
|
Session or ForkSession
|
If provided, rows are written into this session (caller commits later). If None, this method opens its own writable session and commits once. |
None
|
should_skip_vod_write(group_name, source_file_hashes, time_start, time_end, branch='main')
¶
Check whether a VOD write should be skipped as a duplicate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
|
required |
source_file_hashes
|
dict[str, str]
|
|
required |
time_start
|
datetime64
|
Epoch range of the incoming VOD dataset. |
required |
time_end
|
datetime64
|
Epoch range of the incoming VOD dataset. |
required |
branch
|
str
|
|
"main"
|
Returns:
| Type | Description |
|---|---|
tuple[bool, str]
|
|
commit_vod_days(session, forks, days, branch='main')
¶
Merge the forks of a unit of VOD days and commit them once (ADR 0021).
forks come from session, which had no changes of its own
when they were made; they hold the days the worker pool wrote
(:mod:canvodpy.orchestrator.vod_phase). Under the store's commit
lock: merge them, rebase onto commits made since the session was
opened, write the days not in a fork (VodDayWrite.in_fork), add
a log-book row per day and commit.
Returns:
| Type | Description |
|---|---|
str or None
|
Snapshot ID of the commit; |
Raises:
| Type | Description |
|---|---|
PreprocessingMismatchError
|
If a group holds results of data preprocessed differently. |
ValueError
|
If a group holds results of another calculator, or other calculator parameters. |
LayoutError
|
If a day's result covers more than its one day. |
(RebaseFailedError, ConflictError)
|
If another writer changed the same chunks first; nothing is committed. |
write_or_append_vod_groups_batch(items, branch='main', dedup=True)
¶
Write/append multiple VOD analysis groups in one fork/merge batch.
The same cross-group parallelization RinexDataProcessor uses for
GNSS receiver groups (_write_receiver_batch_forked), applied to VOD
analysis groups: any brand-new analysis group is created directly on
its own clean session that commits alone (pre-pass) -- verified
empirically (see _write_receiver_batch_forked's docstring) that
Session.merge() silently drops a fork's data if the base session
it merges into had its own pending uncommitted writes before
.fork() was called, so new-group creation and forked appends can
never safely share one session. Only groups that already exist go
through the fork/merge phase; a brand-new group's pre-pass write is
its entire contribution to this batch (a VOD write is exactly one
dataset per group per call, unlike GNSS's many-files-per-group-per-
day case, so there is never leftover data for a newly-created group
to append afterward).
Fail-fast: if any group's forked write raises, no merge or commit happens for the fork/merge phase -- a partial merge would silently drop a group's data from an apparently-successful commit. A pre-pass commit that already landed before a later fork failure is safe to leave in place: the next retry's dedup check sees that group's data as already-committed and won't re-attempt it.
load_metadata(store, group_name)
¶
Load metadata directly from Zarr into a Polars DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
Any
|
Zarr store or session store handle. |
required |
group_name
|
str
|
Group name. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
Metadata table. |
read_metadata_table(session, group_name)
¶
Read the metadata table from a session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
Any
|
Active session for reading. |
required |
group_name
|
str
|
Group name. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
Metadata table. |
metadata_row_exists(group_name, rinex_hash, start, end, branch='main')
¶
Check whether a file already exists or temporally overlaps existing data.
Performs two checks in order:
- Hash match — if
rinex_hashalready appears in the metadata table, the file was previously ingested (exact duplicate). - Temporal overlap — if the incoming
[start, end]interval overlaps any existing metadata interval, the file covers a time range that is already (partially) present in the store. This catches cases like a daily concatenation file coexisting with the sub-daily files it was built from.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Icechunk group name. |
required |
rinex_hash
|
str
|
Hash of the current GNSS dataset. |
required |
start
|
datetime64
|
Start epoch of the incoming file. |
required |
end
|
datetime64
|
End epoch of the incoming file. |
required |
branch
|
str
|
Branch name in the Icechunk repository. |
"main"
|
Returns:
| Type | Description |
|---|---|
tuple[bool, DataFrame]
|
|
should_skip_file(group_name, file_hash, time_start, time_end, branch='main')
¶
Check whether a file should be skipped before processing and writing.
Thin public wrapper around :meth:metadata_row_exists that returns a
simple (skip, reason) pair instead of a DataFrame, suitable for use
in Airflow task functions as an early-exit optimization.
This method is an early-exit optimization: it avoids opening a write
session for files that are already present. Pass dedup=True to
:meth:write_or_append_group to make the store the authoritative
final gate (runs the same check again before writing).
Checks performed (in order):
- Hash match — file was previously ingested (exact duplicate).
- Temporal overlap — incoming time range overlaps existing epochs.
Layer 3 (intra-batch overlap) is not applicable here because task functions process files one at a time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Icechunk group name. |
required |
file_hash
|
str or None
|
Hash from |
required |
time_start
|
datetime64
|
First epoch of the incoming dataset. |
required |
time_end
|
datetime64
|
Last epoch of the incoming dataset. |
required |
branch
|
str
|
Branch name in the Icechunk repository. |
"main"
|
Returns:
| Type | Description |
|---|---|
tuple[bool, str]
|
|
dir_entry_counts()
¶
Cheap on-disk entry counts for manifests/snapshots/transactions.
A directional proxy for Icechunk-internal growth (manifest count,
snapshot count) without walking the full store. Deliberately
excludes chunks/ (can reach millions of entries -- not cheap)
and only applies to local-filesystem-backed stores; returns {}
for anything else (e.g. S3/GCS) since os.scandir doesn't apply.
metadata_row_count(group_name, branch='main')
¶
Row count of {group_name}/metadata/table, or 0 if absent.
Reads only the index array's shape (no full table load) -- cheap
enough to log alongside every dedup check, so growth in
batch_check/check_temporal_overlaps duration can be directly
correlated against store size instead of assumed.
source_file_hashes(group_name, start, end, branch='main')
¶
Hashes of the ingested files whose data overlaps [start, end].
Read from group_name's log book, in ingest order and without
duplicates. Rows recording a file that was skipped as already
ingested (exists is "True" and nothing was written) are
ignored, so each file counts once however often it was re-run.
Used as the provenance of a dataset read back from the store for that range, e.g. the daily inputs of a VOD computation.
load_metadata_for_dedup(group_name, branch='main')
¶
{group_name}'s metadata table at the tip of branch.
Read whole the first time, afterwards only the rows committed since
(:class:~canvod.store_metadata.log_book.LogBooks): a run asks for it every
day, and a whole read grows with the store (ADR 0021).
batch_check_existing/check_temporal_overlaps each used to call
load_metadata() independently -- two full-table reads per batch,
growing with store size (confirmed
against a real backfill: batch_check duration tracked
metadata_rows almost exactly, ~5x over 41,597 rows). Callers
needing both checks should load once here and pass the result to
both via their metadata_df parameter. Returns None (not an
empty DataFrame) when the group/metadata doesn't exist yet, so
callers can distinguish "nothing to check against" from "failed to
load" without re-attempting the load.
batch_check_existing(group_name, file_hashes, metadata_df=None)
¶
Check which file hashes already exist in metadata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metadata_df
|
DataFrame
|
Pre-loaded metadata table (see |
None
|
check_temporal_overlaps(group_name, file_intervals, branch='main', metadata_df=None)
¶
Check which files temporally overlap existing metadata intervals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Icechunk group name. |
required |
file_intervals
|
list[tuple[str, datetime64, datetime64]]
|
List of |
required |
branch
|
str
|
Branch name in the Icechunk repository. |
"main"
|
metadata_df
|
DataFrame
|
Pre-loaded metadata table (see |
None
|
Returns:
| Type | Description |
|---|---|
set[str]
|
Hashes of files whose |
expire_old_snapshots(days=None, branch='main', delete_expired_branches=False, delete_expired_tags=False)
¶
Expire snapshots older than the given retention period.
Metadata-only "soft delete": rewrites ancestry so expired snapshots
become unreachable from any surviving branch/tag. No objects
(manifests, chunks, snapshot files) are deleted at this stage —
call garbage_collect() separately to reclaim storage.
Keeping
this expire-only (previously it also ran GC internally) is what
lets a caller run GC exactly once via maintenance() instead of
twice per call.
Never invoke this automatically inside the pipeline's write path:
it's an administrative operation, not safe to run casually
alongside active writes. Use a generous days margin (weeks to
months, not hours) so it can never touch anything from an
in-flight write session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
int | None
|
Number of days to retain snapshots. Defaults to 90 (weeks-to- months cadence). |
None
|
branch
|
str
|
Branch to apply expiration on. |
"main"
|
delete_expired_branches
|
bool
|
Whether to delete branches pointing to expired snapshots. Matches Icechunk's own safe default: flip only deliberately. |
False
|
delete_expired_tags
|
bool
|
Whether to delete tags pointing to expired snapshots.
Do not set True if using keeper tags ( |
False
|
Returns:
| Type | Description |
|---|---|
set[str]
|
Expired snapshot IDs. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store was opened without settings. |
garbage_collect(days=90, *, dry_run=False)
¶
Physically delete objects no longer reachable from any branch/tag.
Only removes what a prior expire_old_snapshots() (or manual ref
deletion) already made unreachable — this does not itself expire
anything. Always run with dry_run=True first on a store that
hasn't been GC'd before, especially one with a large accumulated
backlog: there's no documented cost model for GC time/memory at
scale, so a first-ever run's duration is unknown until measured.
Not safe to run casually alongside active writes — the cutoff must predate the start of any concurrently-running write session, and only one expire/GC operation should run at a time. Icechunk does have self-protection against races with new commits arriving mid-run (it restarts, bounded retries, rather than deleting something a concurrent writer needs), but that's not a substitute for a generous cutoff margin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
int
|
Objects older than this (by the same write-time semantics as
|
90
|
dry_run
|
bool
|
Report what would be deleted without deleting anything. |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
Summary of (would-be-)deleted objects. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store was opened without settings, also for a dry run, so maintenance behaves one way. |
get_history(branch='main', limit=None)
¶
Return commit ancestry (history) for a branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Branch name. |
"main"
|
limit
|
int | None
|
Maximum number of commits to return. |
None
|
Returns:
| Type | Description |
|---|---|
list[dict]
|
Commit info dictionaries (id, message, written_at, parent_ids). |
print_history(branch='main', limit=100)
¶
Pretty-print the ancestry for quick inspection.
get_ops_log(limit=None)
¶
Return the repository operations log (newest first).
The ops log is an immutable audit trail of every structural mutation:
commits, branch and tag operations, GC runs, config changes, etc.
Unlike :meth:get_history, this is repo-wide (not per-branch) and
covers all event types, not only commits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
Maximum number of entries to return. |
None
|
Returns:
| Type | Description |
|---|---|
list[dict]
|
Each entry contains |
print_ops_log(limit=50)
¶
Pretty-print the repository operations log for quick inspection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
Maximum number of entries to display (default: 50). |
50
|
maintenance_due(expire_interval_days, gc_delay_days, manifest_count_threshold=None)
¶
Determine whether scheduled expiration/GC/manifest compaction are due.
Used by canvodpy store maintain-due (a cron-safe, unattended
entry point -- see processing.storage.maintenance config). The
repo's own ops log (:meth:get_ops_log) is the source of truth for
"when did maintenance last actually run": it is written atomically
by Icechunk itself as part of the same operation, so it cannot
drift the way a separate marker file could (deleted, stale-copied
across hosts on a shared mount, or never written if a prior run
crashed after the real operation completed but before a marker
write). Confirmed empirically (2026-07-20, throwaway store): a real
expire_old_snapshots() call always writes an ExpirationRan
entry (even when zero snapshots were actually expired -- root/tip
snapshots are protected); garbage_collect(dry_run=True) does
not write a GCRan entry, only a real (non-dry-run) GC does
-- so a dry run correctly never counts as "GC happened" here.
GC is only ever considered due after a qualifying expiration, and
never again for the same expiration -- physical deletion should
lag the (reversible-in-effect) soft-delete by gc_delay_days,
per the skill doc's "expire every 1-2 months, GC every 15-30 days"
guidance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expire_interval_days
|
int
|
Re-run expiration if this many days have passed since the most
recent |
required |
gc_delay_days
|
int
|
Run GC this many days after the most recent expiration, once per expiration (skipped if GC already ran since that expiration). |
required |
manifest_count_threshold
|
int | None
|
Trigger |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
compact_manifests(branch='main')
¶
Consolidate all of a branch's fragmented manifests in one commit.
Thin wrapper over repo.rewrite_manifests(): canvodpy's write
pattern (one to_icechunk() call per file within a batch, e.g.
96 fifteen-minute files/receiver-day) creates one new manifest
per flush, not per commit -- a single day's ingest for one
receiver can create ~96 manifests. This rewrites them back to
the currently-configured splitting boundaries.
Uses commit_method="new_commit" deliberately, not "amend":
amend replaces the immediately-preceding commit's own message/
metadata rather than merging with it, and _append_to_icechunk
already attaches structured provenance (receiver, date, file
hashes) to every ingest commit -- amending could silently clobber
that. An explicit, separate, auditable commit matches the
precedent expire/GC already set. Like every commit, it records
software, environment and settings as commit metadata
(:func:canvod.store_metadata.rewrite_manifests).
Like expire_old_snapshots()/garbage_collect(), this is an
administrative operation -- never call it while a pipeline run is
active against this branch (ordinary commit conflict-retry
semantics apply, but a still-draining worker pool committing
per-file would just make every retry lose, burning cycles for
nothing).
Does not shrink on-disk manifest count by itself -- confirmed
empirically (2026-07-20, throwaway store, 300 commits): compaction
adds a new, consolidated manifest set but leaves the old
fragmented ones on disk (399 -> 401 manifests immediately after
compacting); only a subsequent expire_old_snapshots() +
garbage_collect() actually reclaims them (399 -> 2 in the same
test). This mirrors GC's own relationship to expiration -- don't
expect dir_entry_counts()['manifests'] to drop right after
calling this; it drops after the next expire/GC cycle instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Branch to compact. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID of the compaction commit. |
__repr__()
¶
Return the developer-facing representation.
Returns:
| Type | Description |
|---|---|
str
|
Representation string. |
__str__()
¶
Return a human-readable summary.
Returns:
| Type | Description |
|---|---|
str
|
Summary string. |
rechunk_group(group_name, chunks, source_branch='main', temp_branch=None, promote_to_main=True, delete_temp_branch=True)
¶
Rechunk a group with optimal chunk sizes.
Zarr arrays have a fixed chunk shape once created — writing with
mode="r+" onto an existing array cannot change chunk boundaries,
it can only update values within the existing grid (verified
2026-07-08: a previous version of this method used mode="r+",
which silently failed to change on-disk chunking whenever the
request didn't align with the current chunks). Actually changing
chunk size requires a destructive mode="w" rewrite of the
group, which also wipes the group's nested subgroups (metadata/
table, metadata/sbf_obs) — so this method copies that
subtree back afterward via a raw recursive Zarr copy (see
_copy_zarr_subtree), all on a temporary branch, before main
is ever touched. The copy handles canvod-store's two-level-deep
metadata/{table,sbf_obs} nesting.
Both the data read and the subtree copy are pinned to the exact
snapshot captured at the start of this call. If source_branch
moves before the promotion, the promotion is refused with
BranchMovedError (the new commits would be dropped) and the
rechunked data stays on temp_branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the group to rechunk |
required |
chunks
|
dict[str, int]
|
Chunking specification, e.g. {'epoch': 180, 'sid': -1}. A group stored in day blocks needs an epoch chunk that divides every day into whole chunks. |
required |
source_branch
|
str
|
Branch to read original data from (default: "main") |
'main'
|
temp_branch
|
str | None
|
Temporary branch name for rechunked data. If None, uses "{group_name}_rechunked". |
None
|
promote_to_main
|
bool
|
If True, move |
True
|
delete_temp_branch
|
bool
|
If True, delete temporary branch after promotion (only if promote_to_main=True). |
True
|
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID of the rechunked data |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CadenceError
|
If the epoch chunks would split a day of a day-block group. |
BranchMovedError
|
If |
promote_branch(temp_branch, target_branch, expected_tip)
¶
Move target_branch to the tip of temp_branch.
Only if target_branch still points at expected_tip, the
snapshot temp_branch was created from. Otherwise other commits
landed on target_branch meanwhile and moving it would drop them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
temp_branch
|
str
|
Branch holding the staged work. |
required |
target_branch
|
str
|
Branch to move (usually |
required |
expected_tip
|
str
|
Snapshot ID |
required |
Returns:
| Type | Description |
|---|---|
str
|
The new tip of |
Raises:
| Type | Description |
|---|---|
BranchMovedError
|
If |
create_branch(branch_name, snapshot_id)
¶
Create a branch pointing at a specific snapshot.
Accepts the full snapshot ID or a prefix of it, such as the
8 characters plot_commit_graph() shows (case-insensitive). The
snapshot can be on any branch or tag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch_name
|
str
|
Name of the new branch (e.g. |
required |
snapshot_id
|
str
|
Full snapshot ID or a prefix of it. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the ID is empty, no snapshot on a branch or tag has this ID or prefix, or the prefix fits several. |
create_release_tag(tag_name, snapshot_id=None)
¶
Create an immutable tag for an important version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag_name
|
str
|
Name for the tag (e.g., "v2024_complete", "before_reprocess") |
required |
snapshot_id
|
str | None
|
Snapshot to tag. If None, uses current tip of main branch. |
None
|
list_tags()
¶
List all tags in the repository.
delete_tag(tag_name)
¶
Delete a tag (use with caution - tags are meant to be permanent).
create_keeper_tag(receiver, yyyydoy, snapshot_id)
¶
Tag a per-(receiver, date) commit as a permanent retention keeper.
Part of the tag-based retention scheme: tagging
keep/{receiver}/{yyyydoy} after each commit means a later
expire_old_snapshots(delete_expired_tags=False) run can safely
prune everything else while these specific snapshots stay
time-travelable forever. Additive and inert on its own — creating a
tag doesn't expire or delete anything.
Never raises: a tag name collision (already exists, or was previously deleted — Icechunk tag names are never reusable) is logged and skipped rather than failing the pipeline write that called this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
receiver
|
str
|
Receiver/group name (e.g. |
required |
yyyydoy
|
str
|
Date key (e.g. |
required |
snapshot_id
|
str
|
Snapshot to tag — typically the just-committed snapshot. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the tag was created, False if skipped. |
plot_commit_graph(branch=None, *, plain=False)
¶
Visualize commit history as a graph.
Delegates to icechunk's native repo.ancestry_graph(). Renders as
an SVG diagram in Jupyter/marimo notebooks, or as colored text in a
terminal (print(store.plot_commit_graph())).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Restrict the graph to this branch's linear history. When omitted, all branches are shown as a tree. |
None
|
plain
|
bool
|
Render without ANSI colors or SVG fill colors (useful for CI logs or piping to a file). |
False
|
Returns:
| Type | Description |
|---|---|
AncestryGraph
|
Displayable object with |
cleanup_stale_branches(keep_patterns=None)
¶
Delete stale temporary branches (e.g., from failed rechunking).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keep_patterns
|
list[str] | None
|
Patterns to preserve. Default: ["main", "dev"] |
None
|
Returns:
| Type | Description |
|---|---|
list[str]
|
Names of deleted branches |
delete_branch(branch_name)
¶
Delete a branch.
get_snapshot_info(snapshot_id)
¶
Get detailed information about a specific snapshot.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snapshot_id
|
str
|
Snapshot ID to inspect |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Snapshot metadata and statistics |
compare_snapshots(snapshot_id_1, snapshot_id_2)
¶
Compare two snapshots to see what changed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snapshot_id_1
|
str
|
First snapshot (older) |
required |
snapshot_id_2
|
str
|
Second snapshot (newer) |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Comparison results showing added/removed/modified groups |
maintenance(expire_days=90, cleanup_branches=True, run_gc=True, *, dry_run_gc=False, delete_expired_branches=False, delete_expired_tags=False)
¶
Run full maintenance on the store: expire, then (optionally) GC.
Must be invoked manually or from a separately scheduled maintenance
job: never from the pipeline's own run path, and never on an
automatic schedule until garbage_collect(dry_run=True) has been
checked against the real store and, if using keeper tags,
create_tag() cost at scale has been confirmed.
Not safe during active writes — see expire_old_snapshots/
garbage_collect docstrings for the concurrency caveats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expire_days
|
int
|
Days of snapshot history to keep. Default 90 (weeks-to-months cadence, not the previous 7-day default — 7 days is far too aggressive for a store under sustained daily writes). |
90
|
cleanup_branches
|
bool
|
Remove stale temporary branches. |
True
|
run_gc
|
bool
|
Run garbage collection after expiration. |
True
|
dry_run_gc
|
bool
|
Pass through to |
False
|
delete_expired_branches
|
bool
|
Pass through to |
False
|
delete_expired_tags
|
bool
|
Pass through to |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
Summary of maintenance actions. |
GnssResearchSite
¶
High-level manager for a GNSS research site with dual Icechunk stores.
This class coordinates between RINEX data storage (Level 1) and VOD analysis storage (Level 2), providing a unified interface for site-wide operations.
Architecture: - GNSS Store: Raw/standardized observations per receiver - VOD Store: Analysis products comparing receiver pairs
Features: - Automatic store initialization from config - Receiver management and validation - Analysis workflow coordination - Unified logging and error handling
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
site_name
|
str
|
Name of the research site (must exist in config). |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
gnss_store
cached
property
¶
The site's GNSS store, opened on first use.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store doesn't exist yet; a run creates it. |
vod_store
cached
property
¶
The site's VOD store, opened on first use.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store doesn't exist yet; a run creates it. |
site_config
property
¶
Get the site configuration as a dictionary.
receivers
property
¶
Get all configured receivers for this site.
active_receivers
property
¶
Get only active receivers for this site.
vod_analyses
property
¶
Get all configured VOD analyses for this site.
Returns auto-derived analyses from scs_from if no explicit vod_analyses are configured.
active_vod_analyses
property
¶
Get only active VOD analyses for this site.
__init__(site_name)
¶
Initialize the site manager.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
site_name
|
str
|
Name of the research site. |
required |
add_receiver_metadata(dataset, group)
¶
dataset with the receiver's metadata setting in its attributes.
Attributes the dataset already has are kept; a key of the setting
that clashes with one is left out, with a warning. dataset itself
is not changed: receivers can share the datasets of one file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset
|
Dataset
|
Data of the receiver. |
required |
group
|
str
|
The GNSS store group the data go to: a canopy receiver's name,
or a reference receiver's group per canopy
( |
required |
get_receiver_metadata(receiver_name)
¶
Get freeform metadata for a receiver.
Returns the metadata dict of the receiver in the sites:
section of canvod-settings.yaml, or None if not configured.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
receiver_name
|
str
|
Name of the receiver. |
required |
Returns:
| Type | Description |
|---|---|
dict or None
|
Receiver metadata dict, or None. |
from_gnss_store_path(gnss_store_path)
classmethod
¶
Create a GnssResearchSite instance from a GNSS store path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gnss_store_path
|
Path
|
Path to the RINEX Icechunk store. |
required |
Returns:
| Type | Description |
|---|---|
GnssResearchSite
|
Initialized research site manager. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no matching site is found for the given path. |
get_reference_canopy_pairs()
¶
Expand scs_from into (reference_name, canopy_name) pairs.
Returns:
| Type | Description |
|---|---|
list[tuple[str, str]]
|
List of (reference_name, canopy_name) pairs. |
get_auto_vod_analyses()
¶
Derive VOD analysis pairs from scs_from configuration.
Creates one VOD pair per (canopy, reference_for_that_canopy) combination.
Returns:
| Type | Description |
|---|---|
dict[str, VodAnalysisConfig]
|
Auto-derived VOD analyses keyed by analysis name. |
validate_site_config()
¶
Validate that the site configuration is consistent.
Returns:
| Type | Description |
|---|---|
bool
|
True if configuration is valid. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If configuration is invalid. |
get_receiver_groups()
¶
Get list of receiver groups that exist in the GNSS store.
Returns:
| Type | Description |
|---|---|
list[str]
|
Existing receiver group names. |
get_vod_analysis_groups()
¶
Get list of VOD result groups in the VOD store.
The VOD store nests results as {model}/{analysis_name} (one
model per registered VODFactory calculator, disambiguating
results from different calculators over the same analysis pair).
Top-level list_groups() only
returns model names, so this walks one level deeper.
Returns:
| Type | Description |
|---|---|
list[str]
|
|
ensure_store_metadata(store_type, names, source_format)
¶
Make a store of the site ready for data: the step every run takes first.
Gives a new store its repository metadata from the settings and refuses groups the metadata doesn't list (ADR 0014).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_type
|
str
|
|
required |
names
|
Sequence[str]
|
The groups about to be written: receivers and their pair groups, or VOD analyses. |
required |
source_format
|
str | None
|
The reader format (GNSS store) or VOD calculator (VOD store) a
new store records; |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The store's repository metadata. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the store is new and |
source_file_hashes_for(group_name, ds)
¶
Comma-joined hashes of the GNSS files behind ds.
ds is data read from GNSS store group group_name. The hashes
come from that group's log book, for every ingested file overlapping
ds's epoch range (see MyIcechunkStore.source_file_hashes), so a
daily dataset built from many files lists all of them. Falls back to
ds.attrs["File Hash"] when the log book has no matching rows.
preprocessing_records_for(group_name, ds)
¶
Preprocessing records of the GNSS data behind ds.
ds is data read from GNSS store group group_name; the records
come from that group's log book, for the files overlapping ds's
epoch range (as :meth:source_file_hashes_for).
read_receiver_data(receiver_name, time_range=None, drop_empty=True)
¶
Read data from a specific receiver, its epochs in time order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
receiver_name
|
str
|
Name of the receiver. |
required |
time_range
|
tuple of datetime
|
(start_time, end_time) for filtering the data. |
None
|
drop_empty
|
bool
|
Drop the empty slots of the epochs read: the receiver's epochs
with data. |
True
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset containing receiver observations. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the receiver group does not exist. |
read_vod_analysis(analysis_name, calculator_name, time_range=None, drop_empty=True)
¶
Read VOD analysis results for one calculator model.
VOD written as day blocks (ADR 0016) has a slot for every epoch of a day at the cadence of its GNSS data; slots without a VOD value hold NaN at their grid times.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
analysis_name
|
str
|
Name of the analysis. |
required |
calculator_name
|
str
|
Registered |
required |
time_range
|
tuple of datetime
|
(start_time, end_time) for filtering the results. |
None
|
drop_empty
|
bool
|
Drop the empty slots of the epochs read, after |
True
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset containing VOD analysis results. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the analysis group does not exist. |
prepare_vod_input_data(analysis_name, time_range=None)
¶
Prepare aligned input data for VOD analysis.
Reads data from both receivers specified in the analysis configuration and returns them aligned for VOD processing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
analysis_name
|
str
|
Name of the VOD analysis configuration. |
required |
time_range
|
tuple of datetime
|
(start_time, end_time) for filtering the data. |
None
|
Returns:
| Type | Description |
|---|---|
tuple of (xr.Dataset, xr.Dataset)
|
Tuple of (canopy_dataset, reference_dataset). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the analysis is not configured or data is missing. |
get_site_summary()
¶
Get a comprehensive summary of the research site.
Returns:
| Type | Description |
|---|---|
dict
|
Dictionary with site statistics, data availability, and store paths. |
is_day_complete(yyyydoy, receiver_types=None, completeness_threshold=0.95)
¶
Check if a day has complete data coverage for all receiver types.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
yyyydoy
|
str
|
Date in YYYYDOY format (e.g., "2024256") |
required |
receiver_types
|
List[str]
|
Receiver types to check. Defaults to ['canopy', 'reference'] |
None
|
completeness_threshold
|
float
|
Fraction of expected epochs that must exist (default 0.95 = 95%) Allows for small gaps due to receiver issues |
0.95
|
Returns:
| Type | Description |
|---|---|
bool
|
True if all receiver types have complete data for this day |
__repr__()
¶
Return the developer-facing representation.
Returns:
| Type | Description |
|---|---|
str
|
Representation string. |
__str__()
¶
Return a human-readable summary.
Returns:
| Type | Description |
|---|---|
str
|
Summary string. |
AnalysisStorage
¶
Manage persistent storage of analysis results for dataset+grid pairs.
Storage layout inside the Icechunk repository::
metadata/{dataset_name}/{grid_name}/
├── weights/ # (ncells,)
│ ├── observation_count
│ ├── solid_angle
│ └── combined
├── filter_masks/ # (epoch, sid)
│ ├── mask_iqr
│ └── mask_zscore
├── spatial_masks/ # (ncells,)
│ ├── mask_north
│ └── mask_high_elevation
└── statistics/ # (ncells,)
├── obs_count
├── mean_vod
└── std_vod
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
Path or str
|
Path to the VOD Icechunk store directory. |
required |
__init__(store_path)
¶
Initialize the storage manager.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
Path | str
|
Path to the VOD Icechunk store directory. |
required |
__repr__()
¶
Return the developer-facing representation.
Returns:
| Type | Description |
|---|---|
str
|
Representation string. |
store_weights(dataset_name, grid_name, weights, weight_params=None, overwrite=False)
¶
Store per-cell weight arrays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier (e.g. |
required |
grid_name
|
str
|
Grid identifier (e.g. |
required |
weights
|
dict
|
|
required |
weight_params
|
dict
|
Parameters used to compute each weight type. |
None
|
overwrite
|
bool
|
Overwrite existing weights. |
False
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_weights(dataset_name, grid_name, weight_type=None)
¶
Load stored weight arrays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
weight_type
|
str
|
Load only this weight. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
has_weights(dataset_name, grid_name)
¶
Return True if weights exist for the dataset+grid pair.
store_filter_masks(dataset_name, grid_name, masks, filter_params=None, overwrite=False)
¶
Store per-observation filter masks at native resolution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
masks
|
dict
|
|
required |
filter_params
|
dict
|
Parameters used for each filter. |
None
|
overwrite
|
bool
|
Overwrite existing masks. |
False
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_filter_mask(dataset_name, grid_name, filter_type, attach_coords=True)
¶
Load a single filter mask.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
filter_type
|
str
|
Filter name (e.g. |
required |
attach_coords
|
bool
|
Re-attach |
True
|
Returns:
| Type | Description |
|---|---|
DataArray
|
Boolean mask with shape |
load_all_filter_masks(dataset_name, grid_name)
¶
Load all stored filter masks.
Returns:
| Type | Description |
|---|---|
dict
|
|
has_filter_masks(dataset_name, grid_name)
¶
Return True if filter masks exist for the dataset+grid pair.
store_spatial_masks(dataset_name, grid_name, masks, mask_descriptions=None, overwrite=False)
¶
Store per-cell geometric selection masks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
masks
|
dict
|
|
required |
mask_descriptions
|
dict
|
Human-readable description for each mask (stored as variable attrs). |
None
|
overwrite
|
bool
|
Overwrite existing masks. |
False
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_spatial_mask(dataset_name, grid_name, mask_name)
¶
Load a single spatial mask.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
mask_name
|
str
|
Mask name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Boolean array with shape |
load_all_spatial_masks(dataset_name, grid_name)
¶
Load all spatial masks.
Returns:
| Type | Description |
|---|---|
dict
|
|
has_spatial_masks(dataset_name, grid_name)
¶
Return True if spatial masks exist for the dataset+grid pair.
store_statistics(dataset_name, grid_name, stats, overwrite=False)
¶
Store pre-computed per-cell statistics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
stats
|
dict
|
|
required |
overwrite
|
bool
|
Overwrite existing statistics. |
False
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_statistics(dataset_name, grid_name, stat_name=None)
¶
Load pre-computed per-cell statistics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
stat_name
|
str
|
Load only this statistic. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
has_statistics(dataset_name, grid_name)
¶
Return True if statistics exist for the dataset+grid pair.
list_available_metadata(dataset_name, grid_name)
¶
Check which metadata categories are stored.
Returns:
| Type | Description |
|---|---|
dict
|
|
get_metadata_summary(dataset_name, grid_name)
¶
Detailed summary of all stored metadata for a dataset+grid pair.
Returns:
| Type | Description |
|---|---|
dict
|
Nested summary with availability flags and per-category details. |
delete_weights(dataset_name, grid_name)
¶
Delete all weights for a dataset+grid pair.
delete_filter_masks(dataset_name, grid_name)
¶
Delete all filter masks for a dataset+grid pair.
delete_spatial_masks(dataset_name, grid_name)
¶
Delete all spatial masks for a dataset+grid pair.
delete_statistics(dataset_name, grid_name)
¶
Delete all statistics for a dataset+grid pair.
store_percell_timeseries(percell_ds, system, band, code, branch='per_cell', metadata=None)
¶
Store a per-cell timeseries dataset for a system/band/code combination.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
percell_ds
|
Dataset
|
Per-cell timeseries dataset with dims |
required |
system
|
str
|
GNSS system prefix (e.g. |
required |
band
|
str
|
Frequency band (e.g. |
required |
code
|
str
|
Tracking code (e.g. |
required |
branch
|
str
|
Icechunk branch name (default: |
'per_cell'
|
metadata
|
dict
|
Additional metadata to attach to the dataset. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_percell_timeseries(system, band, code, branch='per_cell')
¶
Load a per-cell timeseries dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
system
|
str
|
GNSS system prefix. |
required |
band
|
str
|
Frequency band. |
required |
code
|
str
|
Tracking code. |
required |
branch
|
str
|
Icechunk branch name (default: |
'per_cell'
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Per-cell timeseries dataset. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the requested group does not exist. |
list_percell_datasets(branch='per_cell')
¶
List all per-cell timeseries datasets in the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Icechunk branch name (default: |
'per_cell'
|
Returns:
| Type | Description |
|---|---|
list of str
|
Sorted list of group names in |
has_percell_timeseries(system, band, code, branch='per_cell')
¶
Return True if a per-cell timeseries exists for the given combination.
delete_all_metadata(dataset_name, grid_name)
¶
Delete the entire metadata subtree for a dataset+grid pair.
delete_specific_weight(dataset_name, grid_name, weight_name)
¶
Delete a single weight variable from an existing weights group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
weight_name
|
str
|
Weight variable name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID. |
delete_specific_filter_mask(dataset_name, grid_name, filter_type)
¶
Delete a single filter mask variable from an existing filter_masks group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
filter_type
|
str
|
Filter type name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID. |
open_gnss_store(store_path)
¶
Open the GNSS store at store_path; never create it.
A run creates a site's stores when it first writes to them. Reading,
branches and tags need no settings file; commits, expiring snapshots
and garbage collection do. A store opened without one refuses them
with a FileNotFoundError that names canvodpy config init.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If no store is at |
open_vod_store(store_path)
¶
Open the VOD store at store_path; never create it.
A run creates a site's stores when it first writes to them. Reading,
branches and tags need no settings file; commits, expiring snapshots
and garbage collection do. A store opened without one refuses them
with a FileNotFoundError that names canvodpy config init.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If no store is at |
store_grid(grid, store, grid_name)
¶
Store grid in unified xarray format to Icechunk store.
Converts the grid to an xarray Dataset with vertex information
and writes it to the grids/ group in the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
GridData
|
Hemisphere grid instance to store. |
required |
store
|
MyIcechunkStore
|
Icechunk store to write to. |
required |
grid_name
|
str
|
Grid identifier for storage path (e.g., 'equal_area_4deg'). |
required |
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID from the commit. |
Examples:
>>> from canvod.grids import create_hemigrid
>>> from canvod.store import store_grid
>>> grid = create_hemigrid(angular_resolution=4, grid_type='equal_area')
>>> snapshot_id = store_grid(grid, my_store, 'equal_area_4deg')
load_grid(store, grid_name)
¶
Load a grid from Icechunk store.
Loads the grid structure from grids/{grid_name} and reconstructs
a GridData object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
MyIcechunkStore
|
Icechunk store to read from. |
required |
grid_name
|
str
|
Grid identifier (e.g., 'equal_area_4deg'). |
required |
Returns:
| Type | Description |
|---|---|
GridData
|
Reconstructed grid instance. |
Examples:
>>> from canvod.store import load_grid
>>> grid = load_grid(my_store, 'equal_area_4deg')
>>> print(f"Loaded {grid.ncells} cells")
Store manager¶
Research site manager that coordinates RINEX and VOD Icechunk stores.
This module provides the GnssResearchSite class that manages both stores for a research site and provides high-level operations across them.
GnssResearchSite
¶
High-level manager for a GNSS research site with dual Icechunk stores.
This class coordinates between RINEX data storage (Level 1) and VOD analysis storage (Level 2), providing a unified interface for site-wide operations.
Architecture: - GNSS Store: Raw/standardized observations per receiver - VOD Store: Analysis products comparing receiver pairs
Features: - Automatic store initialization from config - Receiver management and validation - Analysis workflow coordination - Unified logging and error handling
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
site_name
|
str
|
Name of the research site (must exist in config). |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
gnss_store
cached
property
¶
The site's GNSS store, opened on first use.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store doesn't exist yet; a run creates it. |
vod_store
cached
property
¶
The site's VOD store, opened on first use.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store doesn't exist yet; a run creates it. |
site_config
property
¶
Get the site configuration as a dictionary.
receivers
property
¶
Get all configured receivers for this site.
active_receivers
property
¶
Get only active receivers for this site.
vod_analyses
property
¶
Get all configured VOD analyses for this site.
Returns auto-derived analyses from scs_from if no explicit vod_analyses are configured.
active_vod_analyses
property
¶
Get only active VOD analyses for this site.
__init__(site_name)
¶
Initialize the site manager.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
site_name
|
str
|
Name of the research site. |
required |
add_receiver_metadata(dataset, group)
¶
dataset with the receiver's metadata setting in its attributes.
Attributes the dataset already has are kept; a key of the setting
that clashes with one is left out, with a warning. dataset itself
is not changed: receivers can share the datasets of one file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset
|
Dataset
|
Data of the receiver. |
required |
group
|
str
|
The GNSS store group the data go to: a canopy receiver's name,
or a reference receiver's group per canopy
( |
required |
get_receiver_metadata(receiver_name)
¶
Get freeform metadata for a receiver.
Returns the metadata dict of the receiver in the sites:
section of canvod-settings.yaml, or None if not configured.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
receiver_name
|
str
|
Name of the receiver. |
required |
Returns:
| Type | Description |
|---|---|
dict or None
|
Receiver metadata dict, or None. |
from_gnss_store_path(gnss_store_path)
classmethod
¶
Create a GnssResearchSite instance from a GNSS store path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gnss_store_path
|
Path
|
Path to the RINEX Icechunk store. |
required |
Returns:
| Type | Description |
|---|---|
GnssResearchSite
|
Initialized research site manager. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no matching site is found for the given path. |
get_reference_canopy_pairs()
¶
Expand scs_from into (reference_name, canopy_name) pairs.
Returns:
| Type | Description |
|---|---|
list[tuple[str, str]]
|
List of (reference_name, canopy_name) pairs. |
get_auto_vod_analyses()
¶
Derive VOD analysis pairs from scs_from configuration.
Creates one VOD pair per (canopy, reference_for_that_canopy) combination.
Returns:
| Type | Description |
|---|---|
dict[str, VodAnalysisConfig]
|
Auto-derived VOD analyses keyed by analysis name. |
validate_site_config()
¶
Validate that the site configuration is consistent.
Returns:
| Type | Description |
|---|---|
bool
|
True if configuration is valid. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If configuration is invalid. |
get_receiver_groups()
¶
Get list of receiver groups that exist in the GNSS store.
Returns:
| Type | Description |
|---|---|
list[str]
|
Existing receiver group names. |
get_vod_analysis_groups()
¶
Get list of VOD result groups in the VOD store.
The VOD store nests results as {model}/{analysis_name} (one
model per registered VODFactory calculator, disambiguating
results from different calculators over the same analysis pair).
Top-level list_groups() only
returns model names, so this walks one level deeper.
Returns:
| Type | Description |
|---|---|
list[str]
|
|
ensure_store_metadata(store_type, names, source_format)
¶
Make a store of the site ready for data: the step every run takes first.
Gives a new store its repository metadata from the settings and refuses groups the metadata doesn't list (ADR 0014).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_type
|
str
|
|
required |
names
|
Sequence[str]
|
The groups about to be written: receivers and their pair groups, or VOD analyses. |
required |
source_format
|
str | None
|
The reader format (GNSS store) or VOD calculator (VOD store) a
new store records; |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The store's repository metadata. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the store is new and |
source_file_hashes_for(group_name, ds)
¶
Comma-joined hashes of the GNSS files behind ds.
ds is data read from GNSS store group group_name. The hashes
come from that group's log book, for every ingested file overlapping
ds's epoch range (see MyIcechunkStore.source_file_hashes), so a
daily dataset built from many files lists all of them. Falls back to
ds.attrs["File Hash"] when the log book has no matching rows.
preprocessing_records_for(group_name, ds)
¶
Preprocessing records of the GNSS data behind ds.
ds is data read from GNSS store group group_name; the records
come from that group's log book, for the files overlapping ds's
epoch range (as :meth:source_file_hashes_for).
read_receiver_data(receiver_name, time_range=None, drop_empty=True)
¶
Read data from a specific receiver, its epochs in time order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
receiver_name
|
str
|
Name of the receiver. |
required |
time_range
|
tuple of datetime
|
(start_time, end_time) for filtering the data. |
None
|
drop_empty
|
bool
|
Drop the empty slots of the epochs read: the receiver's epochs
with data. |
True
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset containing receiver observations. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the receiver group does not exist. |
read_vod_analysis(analysis_name, calculator_name, time_range=None, drop_empty=True)
¶
Read VOD analysis results for one calculator model.
VOD written as day blocks (ADR 0016) has a slot for every epoch of a day at the cadence of its GNSS data; slots without a VOD value hold NaN at their grid times.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
analysis_name
|
str
|
Name of the analysis. |
required |
calculator_name
|
str
|
Registered |
required |
time_range
|
tuple of datetime
|
(start_time, end_time) for filtering the results. |
None
|
drop_empty
|
bool
|
Drop the empty slots of the epochs read, after |
True
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset containing VOD analysis results. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the analysis group does not exist. |
prepare_vod_input_data(analysis_name, time_range=None)
¶
Prepare aligned input data for VOD analysis.
Reads data from both receivers specified in the analysis configuration and returns them aligned for VOD processing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
analysis_name
|
str
|
Name of the VOD analysis configuration. |
required |
time_range
|
tuple of datetime
|
(start_time, end_time) for filtering the data. |
None
|
Returns:
| Type | Description |
|---|---|
tuple of (xr.Dataset, xr.Dataset)
|
Tuple of (canopy_dataset, reference_dataset). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the analysis is not configured or data is missing. |
get_site_summary()
¶
Get a comprehensive summary of the research site.
Returns:
| Type | Description |
|---|---|
dict
|
Dictionary with site statistics, data availability, and store paths. |
is_day_complete(yyyydoy, receiver_types=None, completeness_threshold=0.95)
¶
Check if a day has complete data coverage for all receiver types.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
yyyydoy
|
str
|
Date in YYYYDOY format (e.g., "2024256") |
required |
receiver_types
|
List[str]
|
Receiver types to check. Defaults to ['canopy', 'reference'] |
None
|
completeness_threshold
|
float
|
Fraction of expected epochs that must exist (default 0.95 = 95%) Allows for small gaps due to receiver issues |
0.95
|
Returns:
| Type | Description |
|---|---|
bool
|
True if all receiver types have complete data for this day |
__repr__()
¶
Return the developer-facing representation.
Returns:
| Type | Description |
|---|---|
str
|
Representation string. |
__str__()
¶
Return a human-readable summary.
Returns:
| Type | Description |
|---|---|
str
|
Summary string. |
create_default_site()
¶
Create a GnssResearchSite instance for the default site.
Returns:
| Type | Description |
|---|---|
GnssResearchSite
|
Instance for the |
Data store¶
StoredOverlapError
¶
Bases: Exception
A file's epochs fall on epochs a file with another hash already filled.
Deliberately not a ValueError: the run stops on it instead of
skipping the file (grilling on two writers, 2026-10-15, Q11). Neither
file is preferred silently; gnss_store_strategy: overwrite replaces
the stored data on purpose.
BranchMovedError
¶
Bases: RuntimeError
A branch moved while work for it was staged on a temporary branch.
Promoting the temporary branch would drop the commits made in the meantime, so canVODpy refuses and keeps the temporary branch.
MaintenanceOnS3Error
¶
Bases: RuntimeError
Maintenance on a store on S3 is not supported yet (#357).
Deliberately not a ValueError: nothing records it as a failed file
and carries on. A store on S3 may be written by several machines, and
maintenance only knows the runs of its own machine
(is_pipeline_running), so a maintenance action that changes the
store is refused. A garbage-collection dry run and maintenance_due
still work: they change nothing.
VodWriteItem
dataclass
¶
One VOD analysis pair's write request, as passed into a batch write.
VodWritePlan
dataclass
¶
A VodWriteItem after dedup-check + normalization, ready to write.
VodDayWrite
dataclass
¶
One analysis pair's VOD of one day, for :meth:MyIcechunkStore.commit_vod_days.
item.cadences holds the one day and its cadence. With in_fork,
the day was written into a fork of the commit's session and
item.dataset holds only the result's attributes; otherwise it holds
the result, which the commit writes (a group's first days: the forks
were made before the group existed).
MyIcechunkStore
¶
Core Icechunk store manager for GNSS data.
This class encapsulates all operations on a single Icechunk repository, providing a clean interface for GNSS data storage and retrieval with integrated logging and proper resource management.
Features: - Automatic repository creation/connection - Group management with validation - Session management with context managers - Integrated logging with file contexts - Configurable compression and chunking
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
StoreLocation | Path | str
|
The store: a local directory or |
required |
store_type
|
str
|
Type of store ("gnss_store" or "vod_store"). |
"gnss_store"
|
compression_level
|
int | None
|
Override default compression level. |
None
|
compression_algorithm
|
str | None
|
Override default compression algorithm. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
location |
StoreLocation
|
Where the store lives. |
store_path |
Path | str
|
The store's local directory, or its |
store_type |
str
|
Type of store ("gnss_store" or "vod_store"). |
compression_level |
int
|
Compression level (1-9). |
compression_algorithm |
CompressionAlgorithm
|
Compression algorithm enum. |
repo |
Repository
|
The Icechunk repository instance. |
settings
property
¶
The settings the store writes with.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store was opened without settings. Reading needs none; every write does. |
repo
property
¶
Get the repository instance.
site_name
property
¶
The site this store was written for, from its repository metadata.
Never the name of the directory the store sits in, so moving or renaming the directory doesn't change it.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the store has no repository metadata. |
source_format
property
¶
Return the source_format root attribute, or None.
tree
property
¶
Print hierarchical tree of all branches and groups. See print_tree.
__init__(store_path, store_type='gnss_store', compression_level=None, compression_algorithm=None, *, config=None)
¶
Initialize the Icechunk store manager.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
StoreLocation | Path | str
|
The store: a local directory, |
required |
store_type
|
str
|
Type of store ("gnss_store" or "vod_store"). |
"gnss_store"
|
compression_level
|
int | None
|
Override default compression level. |
None
|
compression_algorithm
|
str | None
|
Override default compression algorithm. |
None
|
config
|
CanvodConfig | None
|
The settings the store writes with; every commit records them.
|
None
|
chunk_encoding_for(ds)
¶
Build {var: {"chunks": (...)}} matching self.chunk_strategy.
Only meaningful at group creation -- Zarr fixes an array's chunk
shape the moment it's first written, so this must be passed as
encoding= on the mode="w" call, never on an append (which
must match whatever shape creation already fixed).
Without this, chunk_strategies config only ever affected
read-side chunking (the chunks= hint passed to
xr.open_zarr elsewhere in this class) -- nothing applied it at
write time, so the physical on-disk chunk shape was left to Zarr's
own default, silently disconnected from config. Confirmed by
grepping this file and processor.py for any encoding["chunks"]
assignment: there wasn't one anywhere (2026-07-18).
A dim with a configured chunk size (e.g. epoch: 17280) gets
exactly that size regardless of how much data is in this
particular write -- Zarr supports writing less than one full chunk
in the first call and filling the rest on later appends, which is
the whole point of fixing chunk shape once at creation. A dim
configured -1 (or unconfigured) gets one chunk spanning
whatever this write's current size is (e.g. sid: -1 -> the
full SID universe already present at creation).
readonly_session(branch='main')
¶
Thin wrapper over repo.readonly_session() for this CM.
writable_session(branch='main')
¶
Thin wrapper over repo.writable_session() for this CM.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store was opened without settings. |
set_root_attrs(attrs, branch='main')
¶
Set root-level Zarr attributes on the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
attrs
|
dict[str, Any]
|
Key-value pairs to merge into root attrs. |
required |
branch
|
str
|
Branch to write to. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID from the commit. |
get_root_attrs(branch='main')
¶
Read root-level Zarr attributes from the store.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Root attributes (empty dict if none set). |
get_branch_names()
¶
List all branches in the store.
Returns:
| Type | Description |
|---|---|
list[str]
|
List of branch names. |
get_group_names(branch=None)
¶
List all groups in the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str | None
|
Repository branch to examine. Defaults to listing groups from all branches. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, list[str]]
|
Dictionary mapping branch names to lists of group names. |
list_groups(branch='main')
¶
List all groups in a branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Branch name (default: "main") |
'main'
|
Returns:
| Type | Description |
|---|---|
list[str]
|
List of group names in the branch |
list_data_groups(branch='main')
¶
List the groups that hold data, at any depth.
In a GNSS store these are the receiver groups, as
:meth:list_groups gives them; in a VOD store each analysis sits
below its calculator (tau_omega/canopy_01_vs_reference_01).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Branch name (default: "main") |
'main'
|
Returns:
| Type | Description |
|---|---|
list[str]
|
The paths of the groups, sorted. |
print_tree(max_depth=None)
¶
Display hierarchical tree of all branches, groups, and subgroups.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_depth
|
int | None
|
Maximum depth to display. None for unlimited depth. - 0: Only show branches - 1: Show branches and top-level groups - 2: Show branches, groups, and first level of subgroups/arrays - etc. |
None
|
group_exists(group_name, branch='main')
¶
Check if a group exists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the group to check. May be a nested path (e.g. VOD's
|
required |
branch
|
str
|
Repository branch to examine. |
"main"
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the group exists, False otherwise. |
Notes
Checks membership directly against the opened root group rather
than via :meth:get_group_names/:meth:list_groups -- those only
enumerate top-level Zarr group keys (root.group_keys()), so
for a nested group_name like a VOD analysis group, membership
in that flat list is always False even when the group exists.
Confirmed 2026-07-21: this previously caused every VOD write after
a group's first to take the mode="w" (full overwrite) branch in
:meth:write_or_append_vod_group instead of appending, silently
destroying all previously-written VOD data on each subsequent
write. Zarr's Group.__contains__ supports slash-separated
nested paths directly, so this works for both flat GNSS receiver
names (no behavior change there) and nested VOD group names (the
actual fix).
preprocessing_records(group_name, start=None, end=None, branch='main')
¶
Preprocessing records in group_name's log book, without repeats.
In ingest order; rows written before the record existed give "".
Each file row's record includes the steps of the group-wide rows
written after it (:meth:add_group_variables): it describes the
data as they are now.
With start and end, only rows whose data overlap that range,
without rows of files skipped as already ingested (as
:meth:source_file_hashes). A group without a log book gives none.
check_preprocessing_matches(group_name, dataset, branch='main')
¶
Refuse data preprocessed differently from the data in group_name.
Compares the operations and settings of the record dataset
carries (:data:~canvod.config.models.PREPROCESSING_ATTR; none
counts as no preprocessing) with every record in the group's log
book. The results of the operations (e.g. the input sampling) may
differ. In a VOD store, the records are those of the GNSS data
behind each result (vod_preprocessing_record).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Store group about to receive |
required |
dataset
|
Dataset
|
Data to write. |
required |
branch
|
str
|
Repository branch to examine. |
"main"
|
Raises:
| Type | Description |
|---|---|
PreprocessingMismatchError
|
If the group holds data preprocessed differently. |
check_settings_match(group_name, settings, source='this run has', branch='main')
¶
Refuse preprocessing settings unlike those of the data in group_name.
Lets a run compare its configured preprocessing (see
canvod.ops.planned_settings) with a group before it processes
any data; :meth:check_preprocessing_matches applies the same
comparison to a dataset at write time. A group that does not exist
yet accepts any settings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Store group the data will be written to. |
required |
settings
|
list[dict]
|
Operations and settings, as
|
required |
source
|
str
|
Names the new data in the error message. |
"this run has"
|
branch
|
str
|
Repository branch to examine. |
"main"
|
Raises:
| Type | Description |
|---|---|
PreprocessingMismatchError
|
If the group holds data preprocessed differently. |
add_group_variables(group_name, variables, step, software, branch='main')
¶
Add variables computed from the data already in a group.
For example the grid cells of every observation
(canvod.ops.assign_grid_cells_in_store). Only the new variables are
written; the group's other variables, its attributes, its log book and
its other subgroups stay as they are. The write is staged on a
temporary branch and moved to branch with :meth:promote_branch.
The log book receives one group-wide row (:data:GROUP_WIDE_ACTION)
whose preprocessing record holds step. From then on, the
preprocessing of the data already in the group counts as their own
plus step (:meth:preprocessing_records), so a later write is
accepted only if it was preprocessed with step as well, and
thus carries the new variables too.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Store group, e.g. |
required |
variables
|
Dataset
|
The new variables, with the group's dimensions and coordinate
values as :meth: |
required |
step
|
dict
|
The operation that computed them, as a preprocessing step
|
required |
software
|
dict[str, str]
|
Version of each package whose code computed them. |
required |
branch
|
str
|
Branch to change. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
The new tip of |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the group does not exist, already has one of the variables,
or |
BranchMovedError
|
If |
read_group(group_name, branch='main', time_slice=None, date=None, chunks=None, drop_empty=False)
¶
Read data from a group, its epochs in time order.
A group made of day blocks (ADR 0016) has a slot for every epoch of
a day at the day's cadence; slots without data hold missing values
(NaN, or the _FillValue of integer variables) at their grid
times.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the group to read. |
required |
branch
|
str
|
Repository branch. |
"main"
|
time_slice
|
slice | None
|
Optional label-based time slice for filtering (passed to |
None
|
date
|
str | None
|
YYYYDOY string (e.g. |
None
|
chunks
|
dict[str, Any] | None
|
Dask chunks for reading. If None, the chunks stored in the group. |
None
|
drop_empty
|
bool
|
Drop the empty slots of the epochs read, after |
False
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset from the group. |
write_dataset(dataset, group_name, session, mode='a', chunks=None)
¶
Write a dataset to Icechunk with proper chunking.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset
|
Dataset
|
Dataset to write |
required |
group_name
|
str
|
Group path in store |
required |
session
|
Any
|
Active writable session or store handle. |
required |
mode
|
str
|
Write mode: 'w' (overwrite) or 'a' (append) |
'a'
|
chunks
|
dict[str, int] | None
|
Chunking spec. If None, uses store's chunk_strategy. Example: {'epoch': 17280, 'sid': -1} |
None
|
grow_day_blocks(days, *, message, branch='main')
¶
Give days a block in existing day-block groups, in one commit.
A run calls this once per batch, before reading files, for the days whose Phase 1 succeeded (ADR 0016). A day that has a block keeps it, so a block grown for a day that failed later is reused on the next run. When another commit changed the branch meanwhile and the two conflict, the growth starts again on the new tip: it rereads the epoch axis, reuses the blocks added meanwhile and grows the rest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
Mapping[str, Mapping[datetime64, float]]
|
For each group, the cadence in seconds of each day to grow. |
required |
message
|
str
|
Commit message. |
required |
branch
|
str
|
Branch to commit to. |
"main"
|
Returns:
| Type | Description |
|---|---|
str or None
|
Snapshot ID of the growth commit; |
Raises:
| Type | Description |
|---|---|
LayoutError
|
If a group does not exist (its first write creates it) or is not made of day blocks. |
CadenceError
|
If a cadence does not fit the chunks, or a day has a block at a coarser cadence. |
RuntimeError
|
If the growth still conflicts after |
backup_metadata_table(group_name, session)
¶
Backup the metadata table to a Polars DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group name. |
required |
session
|
Any
|
Active session for reading. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame | None
|
DataFrame with metadata rows, or None if missing. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the table exists but cannot be read. |
restore_metadata_table(group_name, df, session)
¶
Restore the metadata table from a Polars DataFrame.
This recreates the full Zarr structure for the metadata table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group name. |
required |
df
|
DataFrame
|
Metadata table to restore. |
required |
session
|
Any
|
Active session for writing. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
rewrite_group(session, group_name, dataset, drop_ranges, metadata_parts=None)
¶
Rewrite group_name with dataset and keep its metadata/.
A mode="w" write deletes the group's subgroups. The log book
comes back unchanged; every other metadata dataset (e.g. sbf_obs)
comes back from the session's base snapshot without its epochs
inside drop_ranges (the replaced data), merged in epoch order with
the datasets in metadata_parts (name -> per-file datasets). A
metadata dataset left without epochs is not written back. Writes
into session without committing.
Raises:
| Type | Description |
|---|---|
LayoutError
|
If the group is made of day blocks: its files are written into their day's block instead, and a whole-group write would drop its layout and block index. |
get_group_info(group_name, branch='main')
¶
Get metadata about a group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the group. |
required |
branch
|
str
|
Repository branch to examine. |
"main"
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Group metadata. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the group does not exist. |
metadata_dataset_exists(group_name, name, branch='main')
¶
Return True if a metadata dataset name exists for group_name.
write_metadata_dataset(meta_ds, group_name, name, branch='main')
¶
Write a pre-concatenated metadata dataset to {group_name}/metadata/{name}.
Always writes with mode="w" (full overwrite for the day).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
meta_ds
|
Dataset
|
Pre-concatenated |
required |
group_name
|
str
|
Target group (receiver name). |
required |
name
|
str
|
Dataset name under |
required |
branch
|
str
|
Repository branch to write to. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
write_metadata_parts(parts, group_name, name, session)
¶
Add per-file metadata datasets to {group_name}/metadata/{name}.
Writes into session without committing, so the caller commits the
metadata together with the observations it belongs to. Creates the
dataset if it does not exist yet, otherwise appends along epoch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parts
|
list[Dataset]
|
Per-file metadata datasets with an |
required |
group_name
|
str
|
Target group (the observations' store group). |
required |
name
|
str
|
Dataset name under |
required |
session
|
Session or ForkSession
|
Open writable session of the observations' write. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of epochs written. |
append_metadata_datasets(parts, group_name, name, branch='main')
¶
Append metadata datasets in their own commit: no in-memory concat.
Creates {group_name}/metadata/{name} if it does not exist yet,
otherwise appends along epoch. Prefer
:meth:write_metadata_parts inside the session that writes the
observations, so both land in one commit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parts
|
list[Dataset]
|
Individual per-file metadata datasets with an |
required |
group_name
|
str
|
Target group (receiver name). |
required |
name
|
str
|
Dataset name under |
required |
branch
|
str
|
Repository branch to write to. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
read_metadata_dataset(group_name, name, branch='main', chunks=None)
¶
Read a metadata dataset name for group_name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group (receiver) name. |
required |
name
|
str
|
Dataset name under |
required |
branch
|
str
|
Repository branch. |
"main"
|
chunks
|
dict | None
|
Dask chunks for reading. If None, the chunks stored in the dataset. |
None
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Lazy |
get_metadata_dataset_info(group_name, name, branch='main')
¶
Get info about metadata dataset name for group_name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group (receiver) name. |
required |
name
|
str
|
Dataset name under |
required |
branch
|
str
|
Repository branch. |
"main"
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Info dict with the same structure as |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the metadata dataset does not exist. |
sbf_metadata_exists(group_name, branch='main')
¶
Return True if an SBF metadata dataset exists for group_name.
write_sbf_metadata(meta_ds, group_name, branch='main')
¶
Write SBF metadata dataset.
read_sbf_metadata(group_name, branch='main', chunks=None)
¶
Read SBF metadata dataset.
get_sbf_metadata_info(group_name, branch='main')
¶
Get SBF metadata info.
rel_path_for_commit(file_path)
¶
Generate relative path for commit messages.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path
|
Path
|
Full file path. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Relative path string with log_path_depth parts. |
get_store_stats()
¶
Get statistics about the store.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Store statistics. |
append_metadata(group_name, rinex_hash, start, end, snapshot_id, action, commit_msg, dataset_attrs, branch='main', canonical_name=None, physical_path=None)
¶
Standalone metadata-row append: commits on its own.
For ingest paths (write_initial_group, append_to_group) use _append_metadata_row directly inside the data-write session to avoid an extra commit per file.
append_metadata_bulk(group_name, rows, session=None)
¶
Append multiple metadata rows in one commit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Group name (e.g. "canopy", "reference") |
required |
rows
|
list[dict[str, Any]]
|
List of metadata records matching the schema used in append_metadata(). |
required |
session
|
Session or ForkSession
|
If provided, rows are written into this session (caller commits later). If None, this method opens its own writable session and commits once. |
None
|
should_skip_vod_write(group_name, source_file_hashes, time_start, time_end, branch='main')
¶
Check whether a VOD write should be skipped as a duplicate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
|
required |
source_file_hashes
|
dict[str, str]
|
|
required |
time_start
|
datetime64
|
Epoch range of the incoming VOD dataset. |
required |
time_end
|
datetime64
|
Epoch range of the incoming VOD dataset. |
required |
branch
|
str
|
|
"main"
|
Returns:
| Type | Description |
|---|---|
tuple[bool, str]
|
|
commit_vod_days(session, forks, days, branch='main')
¶
Merge the forks of a unit of VOD days and commit them once (ADR 0021).
forks come from session, which had no changes of its own
when they were made; they hold the days the worker pool wrote
(:mod:canvodpy.orchestrator.vod_phase). Under the store's commit
lock: merge them, rebase onto commits made since the session was
opened, write the days not in a fork (VodDayWrite.in_fork), add
a log-book row per day and commit.
Returns:
| Type | Description |
|---|---|
str or None
|
Snapshot ID of the commit; |
Raises:
| Type | Description |
|---|---|
PreprocessingMismatchError
|
If a group holds results of data preprocessed differently. |
ValueError
|
If a group holds results of another calculator, or other calculator parameters. |
LayoutError
|
If a day's result covers more than its one day. |
(RebaseFailedError, ConflictError)
|
If another writer changed the same chunks first; nothing is committed. |
write_or_append_vod_groups_batch(items, branch='main', dedup=True)
¶
Write/append multiple VOD analysis groups in one fork/merge batch.
The same cross-group parallelization RinexDataProcessor uses for
GNSS receiver groups (_write_receiver_batch_forked), applied to VOD
analysis groups: any brand-new analysis group is created directly on
its own clean session that commits alone (pre-pass) -- verified
empirically (see _write_receiver_batch_forked's docstring) that
Session.merge() silently drops a fork's data if the base session
it merges into had its own pending uncommitted writes before
.fork() was called, so new-group creation and forked appends can
never safely share one session. Only groups that already exist go
through the fork/merge phase; a brand-new group's pre-pass write is
its entire contribution to this batch (a VOD write is exactly one
dataset per group per call, unlike GNSS's many-files-per-group-per-
day case, so there is never leftover data for a newly-created group
to append afterward).
Fail-fast: if any group's forked write raises, no merge or commit happens for the fork/merge phase -- a partial merge would silently drop a group's data from an apparently-successful commit. A pre-pass commit that already landed before a later fork failure is safe to leave in place: the next retry's dedup check sees that group's data as already-committed and won't re-attempt it.
load_metadata(store, group_name)
¶
Load metadata directly from Zarr into a Polars DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
Any
|
Zarr store or session store handle. |
required |
group_name
|
str
|
Group name. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
Metadata table. |
read_metadata_table(session, group_name)
¶
Read the metadata table from a session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
Any
|
Active session for reading. |
required |
group_name
|
str
|
Group name. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
Metadata table. |
metadata_row_exists(group_name, rinex_hash, start, end, branch='main')
¶
Check whether a file already exists or temporally overlaps existing data.
Performs two checks in order:
- Hash match — if
rinex_hashalready appears in the metadata table, the file was previously ingested (exact duplicate). - Temporal overlap — if the incoming
[start, end]interval overlaps any existing metadata interval, the file covers a time range that is already (partially) present in the store. This catches cases like a daily concatenation file coexisting with the sub-daily files it was built from.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Icechunk group name. |
required |
rinex_hash
|
str
|
Hash of the current GNSS dataset. |
required |
start
|
datetime64
|
Start epoch of the incoming file. |
required |
end
|
datetime64
|
End epoch of the incoming file. |
required |
branch
|
str
|
Branch name in the Icechunk repository. |
"main"
|
Returns:
| Type | Description |
|---|---|
tuple[bool, DataFrame]
|
|
should_skip_file(group_name, file_hash, time_start, time_end, branch='main')
¶
Check whether a file should be skipped before processing and writing.
Thin public wrapper around :meth:metadata_row_exists that returns a
simple (skip, reason) pair instead of a DataFrame, suitable for use
in Airflow task functions as an early-exit optimization.
This method is an early-exit optimization: it avoids opening a write
session for files that are already present. Pass dedup=True to
:meth:write_or_append_group to make the store the authoritative
final gate (runs the same check again before writing).
Checks performed (in order):
- Hash match — file was previously ingested (exact duplicate).
- Temporal overlap — incoming time range overlaps existing epochs.
Layer 3 (intra-batch overlap) is not applicable here because task functions process files one at a time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Icechunk group name. |
required |
file_hash
|
str or None
|
Hash from |
required |
time_start
|
datetime64
|
First epoch of the incoming dataset. |
required |
time_end
|
datetime64
|
Last epoch of the incoming dataset. |
required |
branch
|
str
|
Branch name in the Icechunk repository. |
"main"
|
Returns:
| Type | Description |
|---|---|
tuple[bool, str]
|
|
dir_entry_counts()
¶
Cheap on-disk entry counts for manifests/snapshots/transactions.
A directional proxy for Icechunk-internal growth (manifest count,
snapshot count) without walking the full store. Deliberately
excludes chunks/ (can reach millions of entries -- not cheap)
and only applies to local-filesystem-backed stores; returns {}
for anything else (e.g. S3/GCS) since os.scandir doesn't apply.
metadata_row_count(group_name, branch='main')
¶
Row count of {group_name}/metadata/table, or 0 if absent.
Reads only the index array's shape (no full table load) -- cheap
enough to log alongside every dedup check, so growth in
batch_check/check_temporal_overlaps duration can be directly
correlated against store size instead of assumed.
source_file_hashes(group_name, start, end, branch='main')
¶
Hashes of the ingested files whose data overlaps [start, end].
Read from group_name's log book, in ingest order and without
duplicates. Rows recording a file that was skipped as already
ingested (exists is "True" and nothing was written) are
ignored, so each file counts once however often it was re-run.
Used as the provenance of a dataset read back from the store for that range, e.g. the daily inputs of a VOD computation.
load_metadata_for_dedup(group_name, branch='main')
¶
{group_name}'s metadata table at the tip of branch.
Read whole the first time, afterwards only the rows committed since
(:class:~canvod.store_metadata.log_book.LogBooks): a run asks for it every
day, and a whole read grows with the store (ADR 0021).
batch_check_existing/check_temporal_overlaps each used to call
load_metadata() independently -- two full-table reads per batch,
growing with store size (confirmed
against a real backfill: batch_check duration tracked
metadata_rows almost exactly, ~5x over 41,597 rows). Callers
needing both checks should load once here and pass the result to
both via their metadata_df parameter. Returns None (not an
empty DataFrame) when the group/metadata doesn't exist yet, so
callers can distinguish "nothing to check against" from "failed to
load" without re-attempting the load.
batch_check_existing(group_name, file_hashes, metadata_df=None)
¶
Check which file hashes already exist in metadata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metadata_df
|
DataFrame
|
Pre-loaded metadata table (see |
None
|
check_temporal_overlaps(group_name, file_intervals, branch='main', metadata_df=None)
¶
Check which files temporally overlap existing metadata intervals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Icechunk group name. |
required |
file_intervals
|
list[tuple[str, datetime64, datetime64]]
|
List of |
required |
branch
|
str
|
Branch name in the Icechunk repository. |
"main"
|
metadata_df
|
DataFrame
|
Pre-loaded metadata table (see |
None
|
Returns:
| Type | Description |
|---|---|
set[str]
|
Hashes of files whose |
expire_old_snapshots(days=None, branch='main', delete_expired_branches=False, delete_expired_tags=False)
¶
Expire snapshots older than the given retention period.
Metadata-only "soft delete": rewrites ancestry so expired snapshots
become unreachable from any surviving branch/tag. No objects
(manifests, chunks, snapshot files) are deleted at this stage —
call garbage_collect() separately to reclaim storage.
Keeping
this expire-only (previously it also ran GC internally) is what
lets a caller run GC exactly once via maintenance() instead of
twice per call.
Never invoke this automatically inside the pipeline's write path:
it's an administrative operation, not safe to run casually
alongside active writes. Use a generous days margin (weeks to
months, not hours) so it can never touch anything from an
in-flight write session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
int | None
|
Number of days to retain snapshots. Defaults to 90 (weeks-to- months cadence). |
None
|
branch
|
str
|
Branch to apply expiration on. |
"main"
|
delete_expired_branches
|
bool
|
Whether to delete branches pointing to expired snapshots. Matches Icechunk's own safe default: flip only deliberately. |
False
|
delete_expired_tags
|
bool
|
Whether to delete tags pointing to expired snapshots.
Do not set True if using keeper tags ( |
False
|
Returns:
| Type | Description |
|---|---|
set[str]
|
Expired snapshot IDs. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store was opened without settings. |
garbage_collect(days=90, *, dry_run=False)
¶
Physically delete objects no longer reachable from any branch/tag.
Only removes what a prior expire_old_snapshots() (or manual ref
deletion) already made unreachable — this does not itself expire
anything. Always run with dry_run=True first on a store that
hasn't been GC'd before, especially one with a large accumulated
backlog: there's no documented cost model for GC time/memory at
scale, so a first-ever run's duration is unknown until measured.
Not safe to run casually alongside active writes — the cutoff must predate the start of any concurrently-running write session, and only one expire/GC operation should run at a time. Icechunk does have self-protection against races with new commits arriving mid-run (it restarts, bounded retries, rather than deleting something a concurrent writer needs), but that's not a substitute for a generous cutoff margin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
int
|
Objects older than this (by the same write-time semantics as
|
90
|
dry_run
|
bool
|
Report what would be deleted without deleting anything. |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
Summary of (would-be-)deleted objects. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the store was opened without settings, also for a dry run, so maintenance behaves one way. |
get_history(branch='main', limit=None)
¶
Return commit ancestry (history) for a branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Branch name. |
"main"
|
limit
|
int | None
|
Maximum number of commits to return. |
None
|
Returns:
| Type | Description |
|---|---|
list[dict]
|
Commit info dictionaries (id, message, written_at, parent_ids). |
print_history(branch='main', limit=100)
¶
Pretty-print the ancestry for quick inspection.
get_ops_log(limit=None)
¶
Return the repository operations log (newest first).
The ops log is an immutable audit trail of every structural mutation:
commits, branch and tag operations, GC runs, config changes, etc.
Unlike :meth:get_history, this is repo-wide (not per-branch) and
covers all event types, not only commits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
Maximum number of entries to return. |
None
|
Returns:
| Type | Description |
|---|---|
list[dict]
|
Each entry contains |
print_ops_log(limit=50)
¶
Pretty-print the repository operations log for quick inspection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
Maximum number of entries to display (default: 50). |
50
|
maintenance_due(expire_interval_days, gc_delay_days, manifest_count_threshold=None)
¶
Determine whether scheduled expiration/GC/manifest compaction are due.
Used by canvodpy store maintain-due (a cron-safe, unattended
entry point -- see processing.storage.maintenance config). The
repo's own ops log (:meth:get_ops_log) is the source of truth for
"when did maintenance last actually run": it is written atomically
by Icechunk itself as part of the same operation, so it cannot
drift the way a separate marker file could (deleted, stale-copied
across hosts on a shared mount, or never written if a prior run
crashed after the real operation completed but before a marker
write). Confirmed empirically (2026-07-20, throwaway store): a real
expire_old_snapshots() call always writes an ExpirationRan
entry (even when zero snapshots were actually expired -- root/tip
snapshots are protected); garbage_collect(dry_run=True) does
not write a GCRan entry, only a real (non-dry-run) GC does
-- so a dry run correctly never counts as "GC happened" here.
GC is only ever considered due after a qualifying expiration, and
never again for the same expiration -- physical deletion should
lag the (reversible-in-effect) soft-delete by gc_delay_days,
per the skill doc's "expire every 1-2 months, GC every 15-30 days"
guidance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expire_interval_days
|
int
|
Re-run expiration if this many days have passed since the most
recent |
required |
gc_delay_days
|
int
|
Run GC this many days after the most recent expiration, once per expiration (skipped if GC already ran since that expiration). |
required |
manifest_count_threshold
|
int | None
|
Trigger |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
compact_manifests(branch='main')
¶
Consolidate all of a branch's fragmented manifests in one commit.
Thin wrapper over repo.rewrite_manifests(): canvodpy's write
pattern (one to_icechunk() call per file within a batch, e.g.
96 fifteen-minute files/receiver-day) creates one new manifest
per flush, not per commit -- a single day's ingest for one
receiver can create ~96 manifests. This rewrites them back to
the currently-configured splitting boundaries.
Uses commit_method="new_commit" deliberately, not "amend":
amend replaces the immediately-preceding commit's own message/
metadata rather than merging with it, and _append_to_icechunk
already attaches structured provenance (receiver, date, file
hashes) to every ingest commit -- amending could silently clobber
that. An explicit, separate, auditable commit matches the
precedent expire/GC already set. Like every commit, it records
software, environment and settings as commit metadata
(:func:canvod.store_metadata.rewrite_manifests).
Like expire_old_snapshots()/garbage_collect(), this is an
administrative operation -- never call it while a pipeline run is
active against this branch (ordinary commit conflict-retry
semantics apply, but a still-draining worker pool committing
per-file would just make every retry lose, burning cycles for
nothing).
Does not shrink on-disk manifest count by itself -- confirmed
empirically (2026-07-20, throwaway store, 300 commits): compaction
adds a new, consolidated manifest set but leaves the old
fragmented ones on disk (399 -> 401 manifests immediately after
compacting); only a subsequent expire_old_snapshots() +
garbage_collect() actually reclaims them (399 -> 2 in the same
test). This mirrors GC's own relationship to expiration -- don't
expect dir_entry_counts()['manifests'] to drop right after
calling this; it drops after the next expire/GC cycle instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Branch to compact. |
"main"
|
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID of the compaction commit. |
__repr__()
¶
Return the developer-facing representation.
Returns:
| Type | Description |
|---|---|
str
|
Representation string. |
__str__()
¶
Return a human-readable summary.
Returns:
| Type | Description |
|---|---|
str
|
Summary string. |
rechunk_group(group_name, chunks, source_branch='main', temp_branch=None, promote_to_main=True, delete_temp_branch=True)
¶
Rechunk a group with optimal chunk sizes.
Zarr arrays have a fixed chunk shape once created — writing with
mode="r+" onto an existing array cannot change chunk boundaries,
it can only update values within the existing grid (verified
2026-07-08: a previous version of this method used mode="r+",
which silently failed to change on-disk chunking whenever the
request didn't align with the current chunks). Actually changing
chunk size requires a destructive mode="w" rewrite of the
group, which also wipes the group's nested subgroups (metadata/
table, metadata/sbf_obs) — so this method copies that
subtree back afterward via a raw recursive Zarr copy (see
_copy_zarr_subtree), all on a temporary branch, before main
is ever touched. The copy handles canvod-store's two-level-deep
metadata/{table,sbf_obs} nesting.
Both the data read and the subtree copy are pinned to the exact
snapshot captured at the start of this call. If source_branch
moves before the promotion, the promotion is refused with
BranchMovedError (the new commits would be dropped) and the
rechunked data stays on temp_branch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
group_name
|
str
|
Name of the group to rechunk |
required |
chunks
|
dict[str, int]
|
Chunking specification, e.g. {'epoch': 180, 'sid': -1}. A group stored in day blocks needs an epoch chunk that divides every day into whole chunks. |
required |
source_branch
|
str
|
Branch to read original data from (default: "main") |
'main'
|
temp_branch
|
str | None
|
Temporary branch name for rechunked data. If None, uses "{group_name}_rechunked". |
None
|
promote_to_main
|
bool
|
If True, move |
True
|
delete_temp_branch
|
bool
|
If True, delete temporary branch after promotion (only if promote_to_main=True). |
True
|
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID of the rechunked data |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
CadenceError
|
If the epoch chunks would split a day of a day-block group. |
BranchMovedError
|
If |
promote_branch(temp_branch, target_branch, expected_tip)
¶
Move target_branch to the tip of temp_branch.
Only if target_branch still points at expected_tip, the
snapshot temp_branch was created from. Otherwise other commits
landed on target_branch meanwhile and moving it would drop them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
temp_branch
|
str
|
Branch holding the staged work. |
required |
target_branch
|
str
|
Branch to move (usually |
required |
expected_tip
|
str
|
Snapshot ID |
required |
Returns:
| Type | Description |
|---|---|
str
|
The new tip of |
Raises:
| Type | Description |
|---|---|
BranchMovedError
|
If |
create_branch(branch_name, snapshot_id)
¶
Create a branch pointing at a specific snapshot.
Accepts the full snapshot ID or a prefix of it, such as the
8 characters plot_commit_graph() shows (case-insensitive). The
snapshot can be on any branch or tag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch_name
|
str
|
Name of the new branch (e.g. |
required |
snapshot_id
|
str
|
Full snapshot ID or a prefix of it. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the ID is empty, no snapshot on a branch or tag has this ID or prefix, or the prefix fits several. |
create_release_tag(tag_name, snapshot_id=None)
¶
Create an immutable tag for an important version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag_name
|
str
|
Name for the tag (e.g., "v2024_complete", "before_reprocess") |
required |
snapshot_id
|
str | None
|
Snapshot to tag. If None, uses current tip of main branch. |
None
|
list_tags()
¶
List all tags in the repository.
delete_tag(tag_name)
¶
Delete a tag (use with caution - tags are meant to be permanent).
create_keeper_tag(receiver, yyyydoy, snapshot_id)
¶
Tag a per-(receiver, date) commit as a permanent retention keeper.
Part of the tag-based retention scheme: tagging
keep/{receiver}/{yyyydoy} after each commit means a later
expire_old_snapshots(delete_expired_tags=False) run can safely
prune everything else while these specific snapshots stay
time-travelable forever. Additive and inert on its own — creating a
tag doesn't expire or delete anything.
Never raises: a tag name collision (already exists, or was previously deleted — Icechunk tag names are never reusable) is logged and skipped rather than failing the pipeline write that called this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
receiver
|
str
|
Receiver/group name (e.g. |
required |
yyyydoy
|
str
|
Date key (e.g. |
required |
snapshot_id
|
str
|
Snapshot to tag — typically the just-committed snapshot. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the tag was created, False if skipped. |
plot_commit_graph(branch=None, *, plain=False)
¶
Visualize commit history as a graph.
Delegates to icechunk's native repo.ancestry_graph(). Renders as
an SVG diagram in Jupyter/marimo notebooks, or as colored text in a
terminal (print(store.plot_commit_graph())).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Restrict the graph to this branch's linear history. When omitted, all branches are shown as a tree. |
None
|
plain
|
bool
|
Render without ANSI colors or SVG fill colors (useful for CI logs or piping to a file). |
False
|
Returns:
| Type | Description |
|---|---|
AncestryGraph
|
Displayable object with |
cleanup_stale_branches(keep_patterns=None)
¶
Delete stale temporary branches (e.g., from failed rechunking).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keep_patterns
|
list[str] | None
|
Patterns to preserve. Default: ["main", "dev"] |
None
|
Returns:
| Type | Description |
|---|---|
list[str]
|
Names of deleted branches |
delete_branch(branch_name)
¶
Delete a branch.
get_snapshot_info(snapshot_id)
¶
Get detailed information about a specific snapshot.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snapshot_id
|
str
|
Snapshot ID to inspect |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Snapshot metadata and statistics |
compare_snapshots(snapshot_id_1, snapshot_id_2)
¶
Compare two snapshots to see what changed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snapshot_id_1
|
str
|
First snapshot (older) |
required |
snapshot_id_2
|
str
|
Second snapshot (newer) |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Comparison results showing added/removed/modified groups |
maintenance(expire_days=90, cleanup_branches=True, run_gc=True, *, dry_run_gc=False, delete_expired_branches=False, delete_expired_tags=False)
¶
Run full maintenance on the store: expire, then (optionally) GC.
Must be invoked manually or from a separately scheduled maintenance
job: never from the pipeline's own run path, and never on an
automatic schedule until garbage_collect(dry_run=True) has been
checked against the real store and, if using keeper tags,
create_tag() cost at scale has been confirmed.
Not safe during active writes — see expire_old_snapshots/
garbage_collect docstrings for the concurrency caveats.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expire_days
|
int
|
Days of snapshot history to keep. Default 90 (weeks-to-months cadence, not the previous 7-day default — 7 days is far too aggressive for a store under sustained daily writes). |
90
|
cleanup_branches
|
bool
|
Remove stale temporary branches. |
True
|
run_gc
|
bool
|
Run garbage collection after expiration. |
True
|
dry_run_gc
|
bool
|
Pass through to |
False
|
delete_expired_branches
|
bool
|
Pass through to |
False
|
delete_expired_tags
|
bool
|
Pass through to |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
Summary of maintenance actions. |
normalize_encodings(ds)
¶
Normalize dataset encodings for Icechunk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
Dataset to normalize. |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset with normalized encodings. |
cleanse_dataset_attrs(dataset)
¶
Remove the attributes that must not be stored (in place; returns dataset).
settings_file()
¶
The settings file a store loads when it is given no settings.
open_gnss_store(store_path)
¶
Open the GNSS store at store_path; never create it.
A run creates a site's stores when it first writes to them. Reading,
branches and tags need no settings file; commits, expiring snapshots
and garbage collection do. A store opened without one refuses them
with a FileNotFoundError that names canvodpy config init.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If no store is at |
open_vod_store(store_path)
¶
Open the VOD store at store_path; never create it.
A run creates a site's stores when it first writes to them. Reading,
branches and tags need no settings file; commits, expiring snapshots
and garbage collection do. A store opened without one refuses them
with a FileNotFoundError that names canvodpy config init.
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If no store is at |
Grid storage¶
Store hemisphere grids in an Icechunk store and read them back.
A grid is written to the grids/{grid_name} group of the store as an
xarray Dataset with the cell centers, vertices and solid angles
(canvod.grids.grid_to_dataset).
store_grid(grid, store, grid_name)
¶
Store grid in unified xarray format to Icechunk store.
Converts the grid to an xarray Dataset with vertex information
and writes it to the grids/ group in the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
GridData
|
Hemisphere grid instance to store. |
required |
store
|
MyIcechunkStore
|
Icechunk store to write to. |
required |
grid_name
|
str
|
Grid identifier for storage path (e.g., 'equal_area_4deg'). |
required |
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID from the commit. |
Examples:
>>> from canvod.grids import create_hemigrid
>>> from canvod.store import store_grid
>>> grid = create_hemigrid(angular_resolution=4, grid_type='equal_area')
>>> snapshot_id = store_grid(grid, my_store, 'equal_area_4deg')
load_grid(store, grid_name)
¶
Load a grid from Icechunk store.
Loads the grid structure from grids/{grid_name} and reconstructs
a GridData object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
MyIcechunkStore
|
Icechunk store to read from. |
required |
grid_name
|
str
|
Grid identifier (e.g., 'equal_area_4deg'). |
required |
Returns:
| Type | Description |
|---|---|
GridData
|
Reconstructed grid instance. |
Examples:
>>> from canvod.store import load_grid
>>> grid = load_grid(my_store, 'equal_area_4deg')
>>> print(f"Loaded {grid.ncells} cells")
Analysis storage¶
Persistent storage for precomputed analysis results via Icechunk.
Stores dataset+grid-specific analysis outputs in an Icechunk repository
under metadata/{dataset_name}/{grid_name}/:
- weights – per-cell weight arrays (
ncells,). - filter_masks – per-observation statistical masks (
epoch × sid). - spatial_masks – per-cell geometric selection masks (
ncells,). - statistics – per-cell aggregated statistics (
ncells,).
Classes:
| Name | Description |
|---|---|
``AnalysisStorage`` |
Read / write / delete analysis metadata for a single Icechunk store. |
AnalysisStorage
¶
Manage persistent storage of analysis results for dataset+grid pairs.
Storage layout inside the Icechunk repository::
metadata/{dataset_name}/{grid_name}/
├── weights/ # (ncells,)
│ ├── observation_count
│ ├── solid_angle
│ └── combined
├── filter_masks/ # (epoch, sid)
│ ├── mask_iqr
│ └── mask_zscore
├── spatial_masks/ # (ncells,)
│ ├── mask_north
│ └── mask_high_elevation
└── statistics/ # (ncells,)
├── obs_count
├── mean_vod
└── std_vod
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
Path or str
|
Path to the VOD Icechunk store directory. |
required |
__init__(store_path)
¶
Initialize the storage manager.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store_path
|
Path | str
|
Path to the VOD Icechunk store directory. |
required |
__repr__()
¶
Return the developer-facing representation.
Returns:
| Type | Description |
|---|---|
str
|
Representation string. |
store_weights(dataset_name, grid_name, weights, weight_params=None, overwrite=False)
¶
Store per-cell weight arrays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier (e.g. |
required |
grid_name
|
str
|
Grid identifier (e.g. |
required |
weights
|
dict
|
|
required |
weight_params
|
dict
|
Parameters used to compute each weight type. |
None
|
overwrite
|
bool
|
Overwrite existing weights. |
False
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_weights(dataset_name, grid_name, weight_type=None)
¶
Load stored weight arrays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
weight_type
|
str
|
Load only this weight. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
has_weights(dataset_name, grid_name)
¶
Return True if weights exist for the dataset+grid pair.
store_filter_masks(dataset_name, grid_name, masks, filter_params=None, overwrite=False)
¶
Store per-observation filter masks at native resolution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
masks
|
dict
|
|
required |
filter_params
|
dict
|
Parameters used for each filter. |
None
|
overwrite
|
bool
|
Overwrite existing masks. |
False
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_filter_mask(dataset_name, grid_name, filter_type, attach_coords=True)
¶
Load a single filter mask.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
filter_type
|
str
|
Filter name (e.g. |
required |
attach_coords
|
bool
|
Re-attach |
True
|
Returns:
| Type | Description |
|---|---|
DataArray
|
Boolean mask with shape |
load_all_filter_masks(dataset_name, grid_name)
¶
Load all stored filter masks.
Returns:
| Type | Description |
|---|---|
dict
|
|
has_filter_masks(dataset_name, grid_name)
¶
Return True if filter masks exist for the dataset+grid pair.
store_spatial_masks(dataset_name, grid_name, masks, mask_descriptions=None, overwrite=False)
¶
Store per-cell geometric selection masks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
masks
|
dict
|
|
required |
mask_descriptions
|
dict
|
Human-readable description for each mask (stored as variable attrs). |
None
|
overwrite
|
bool
|
Overwrite existing masks. |
False
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_spatial_mask(dataset_name, grid_name, mask_name)
¶
Load a single spatial mask.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
mask_name
|
str
|
Mask name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Boolean array with shape |
load_all_spatial_masks(dataset_name, grid_name)
¶
Load all spatial masks.
Returns:
| Type | Description |
|---|---|
dict
|
|
has_spatial_masks(dataset_name, grid_name)
¶
Return True if spatial masks exist for the dataset+grid pair.
store_statistics(dataset_name, grid_name, stats, overwrite=False)
¶
Store pre-computed per-cell statistics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
stats
|
dict
|
|
required |
overwrite
|
bool
|
Overwrite existing statistics. |
False
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_statistics(dataset_name, grid_name, stat_name=None)
¶
Load pre-computed per-cell statistics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
stat_name
|
str
|
Load only this statistic. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
has_statistics(dataset_name, grid_name)
¶
Return True if statistics exist for the dataset+grid pair.
list_available_metadata(dataset_name, grid_name)
¶
Check which metadata categories are stored.
Returns:
| Type | Description |
|---|---|
dict
|
|
get_metadata_summary(dataset_name, grid_name)
¶
Detailed summary of all stored metadata for a dataset+grid pair.
Returns:
| Type | Description |
|---|---|
dict
|
Nested summary with availability flags and per-category details. |
delete_weights(dataset_name, grid_name)
¶
Delete all weights for a dataset+grid pair.
delete_filter_masks(dataset_name, grid_name)
¶
Delete all filter masks for a dataset+grid pair.
delete_spatial_masks(dataset_name, grid_name)
¶
Delete all spatial masks for a dataset+grid pair.
delete_statistics(dataset_name, grid_name)
¶
Delete all statistics for a dataset+grid pair.
store_percell_timeseries(percell_ds, system, band, code, branch='per_cell', metadata=None)
¶
Store a per-cell timeseries dataset for a system/band/code combination.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
percell_ds
|
Dataset
|
Per-cell timeseries dataset with dims |
required |
system
|
str
|
GNSS system prefix (e.g. |
required |
band
|
str
|
Frequency band (e.g. |
required |
code
|
str
|
Tracking code (e.g. |
required |
branch
|
str
|
Icechunk branch name (default: |
'per_cell'
|
metadata
|
dict
|
Additional metadata to attach to the dataset. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Icechunk snapshot ID. |
load_percell_timeseries(system, band, code, branch='per_cell')
¶
Load a per-cell timeseries dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
system
|
str
|
GNSS system prefix. |
required |
band
|
str
|
Frequency band. |
required |
code
|
str
|
Tracking code. |
required |
branch
|
str
|
Icechunk branch name (default: |
'per_cell'
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Per-cell timeseries dataset. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the requested group does not exist. |
list_percell_datasets(branch='per_cell')
¶
List all per-cell timeseries datasets in the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
branch
|
str
|
Icechunk branch name (default: |
'per_cell'
|
Returns:
| Type | Description |
|---|---|
list of str
|
Sorted list of group names in |
has_percell_timeseries(system, band, code, branch='per_cell')
¶
Return True if a per-cell timeseries exists for the given combination.
delete_all_metadata(dataset_name, grid_name)
¶
Delete the entire metadata subtree for a dataset+grid pair.
delete_specific_weight(dataset_name, grid_name, weight_name)
¶
Delete a single weight variable from an existing weights group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
weight_name
|
str
|
Weight variable name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID. |
delete_specific_filter_mask(dataset_name, grid_name, filter_type)
¶
Delete a single filter mask variable from an existing filter_masks group.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_name
|
str
|
Dataset identifier. |
required |
grid_name
|
str
|
Grid identifier. |
required |
filter_type
|
str
|
Filter type name (e.g. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Snapshot ID. |
Preprocessing¶
Preprocessing wrapper for Icechunk storage.
This module provides the IcechunkPreprocessor class which wraps preprocessing functions from canvod.readers.preprocessing and canvod.auxiliary.preprocessing.
IcechunkPreprocessor
¶
Handles preprocessing of RINEX-converted datasets before writing to Icechunk.
This class wraps functions from canvod.readers.preprocessing and canvod.auxiliary.preprocessing.
Note: All methods now delegate to canvod.readers.preprocessing / canvod.auxiliary.preprocessing functions. The aggregate_glonass_fdma parameter should be passed from configuration.
map_aux_sv_to_sid(aux_ds, fill_value=None, aggregate_glonass_fdma=True)
staticmethod
¶
Transform auxiliary dataset from sv → sid dimension.
Delegates to canvod.readers.preprocessing.map_aux_sv_to_sid()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aux_ds
|
Dataset
|
Dataset with 'sv' dimension |
required |
fill_value
|
float
|
Fill value for missing entries (default: np.nan) |
None
|
aggregate_glonass_fdma
|
bool
|
Whether to aggregate GLONASS FDMA bands |
True
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset with 'sid' dimension replacing 'sv' |
pad_to_global_sid(ds, keep_sids=None, aggregate_glonass_fdma=True)
staticmethod
¶
Pad dataset so it has all possible SIDs across all constellations.
Delegates to canvod.readers.preprocessing.pad_to_global_sid()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
Dataset with 'sid' dimension |
required |
keep_sids
|
list[str] | None
|
Optional list of specific SIDs to keep |
None
|
aggregate_glonass_fdma
|
bool
|
Whether to aggregate GLONASS FDMA bands |
True
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset padded with NaN for missing SIDs |
normalize_sid_dtype(ds)
staticmethod
¶
Ensure sid coordinate uses object dtype.
Delegates to canvod.readers.preprocessing.normalize_sid_dtype()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
Dataset with 'sid' coordinate |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset with sid as object dtype |
strip_fillvalue(ds)
staticmethod
¶
Remove _FillValue attrs/encodings.
Delegates to canvod.readers.preprocessing.strip_fillvalue()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
Dataset to clean |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset with _FillValue attributes removed |
add_future_datavars(ds, var_config)
staticmethod
¶
Add placeholder data variables from configuration.
Delegates to canvod.readers.preprocessing.add_future_datavars()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ds
|
Dataset
|
Dataset to add variables to |
required |
var_config
|
dict[str, dict[str, Any]]
|
Variable configuration dictionary |
required |
Returns:
| Type | Description |
|---|---|
Dataset
|
Dataset with new variables added |
prep_aux_ds(aux_ds, fill_value=None, aggregate_glonass_fdma=True, keep_sids=None)
staticmethod
¶
Preprocess auxiliary dataset before writing to Icechunk.
Delegates to canvod.auxiliary.preprocessing.prep_aux_ds()
Performs complete 4-step preprocessing: 1. Convert sv → sid dimension 2. Pad to global sid list or filter to keep_sids 3. Normalize sid dtype to object 4. Strip _FillValue attributes
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aux_ds
|
Dataset
|
Dataset with 'sv' dimension |
required |
fill_value
|
float
|
Fill value for missing entries (default: np.nan) |
None
|
aggregate_glonass_fdma
|
bool
|
Whether to aggregate GLONASS FDMA bands |
True
|
keep_sids
|
list[str] | None
|
List of specific SIDs to keep. If None, keeps all possible SIDs. |
None
|
Returns:
| Type | Description |
|---|---|
Dataset
|
Fully preprocessed dataset ready for Icechunk |