Layer 0 — Datasets¶
Four operators. The base + three concrete subclasses. Each yields
(input, target) pairs and provides a stable content hash that the
training artifact pins.
TrainingDataset¶
class TrainingDataset(Operator):
"""Base class — yields (input, target) pairs.
Subclasses implement __iter__ + content_hash. The base provides
pipekit Operator semantics: get_config / from_state / dumps,
composition with Sequential, and split derivation.
Attributes:
seed: PRNG seed used by sampling subclasses.
split: One of "train", "val", "test". Subclasses use this
to partition deterministically by seed.
"""
seed: int = 0
split: Literal["train", "val", "test"] = "train"
def __iter__(self) -> Iterator[tuple[Any, Any]]:
raise NotImplementedError
def content_hash(self) -> str:
"""Stable identifier for this dataset's contents.
Default: sha256 over (type, get_config()).
Subclasses override to fold in dataset-specific state
(catalog URI, forward-model state_signature, etc.).
"""
h = hashlib.sha256()
h.update(type(self).__qualname__.encode())
h.update(b"\x00")
h.update(json.dumps(self.get_config(), sort_keys=True).encode())
return h.hexdigest()
def with_split(self, split: Literal["train", "val", "test"]) -> TrainingDataset:
"""Return a clone with a different split, same seed."""
return dataclasses.replace(self, split=split)
The __iter__ discipline. A TrainingDataset is a stream of
(x, y) pairs. It does not have a meaningful _apply — calling the
operator like a function raises TypeError (the carrier model
doesn't fit). Datasets compose into TrainingLoop via the
dataset=... argument, not via Sequential.
CatalogDataset¶
For direct-supervised training over labeled scenes. Bridges to
geocatalog and geopatcher.
class CatalogDataset(TrainingDataset):
"""Yields (preprocessed_carrier, label) pairs from a catalog.
Args:
catalog: A geocatalog.GeoCatalog (or StateCatalog) reference.
Stored as a URI string for YAML round-trip.
preprocess: Operator chain applied per-row before yielding.
Typically the inference-time preprocessing pipeline.
target_op: Operator that extracts the label from a catalog row.
sampler: Optional geopatcher.SpatialSampler that tiles each
row into patches. None means one yield per row.
seed: PRNG seed.
split: "train" / "val" / "test". Maps deterministically onto
the catalog via split_op (default 80/10/10 by row hash).
split_op: Operator that takes (row, seed, split) and returns
True iff the row belongs to `split`. None uses the
default row-hash splitter.
"""
catalog: str # URI; loaded lazily
preprocess: Operator
target_op: Operator
sampler: Operator | None = None
split_op: Operator | None = None
def content_hash(self) -> str:
# Includes catalog URI + a catalog-side checksum if available
# (e.g. parquet schema hash + row count) so cache invalidation
# is reliable.
...
The catalog argument is a URI string, not an open catalog handle,
so the dataset round-trips through YAML. The handle is opened
lazily on first __iter__.
SimulationDataset¶
The bridge to pipekit-cycle. For emulator training and amortized
inference.
class SimulationDataset(TrainingDataset):
"""Yields (parameters, simulator_output) pairs.
Used for both emulator training (network learns forward map) and
amortized inference (network learns inverse map — the loss
differs, not the data).
Args:
forward_model: Anything satisfying pipekit_cycle.ForwardModel.
prior: Operator that samples parameter realizations.
Called per __iter__ tick.
n_samples: Total samples per epoch. With infinite
iteration (Grain `num_epochs=None`), this controls
steps-per-epoch.
cycle: Optional pipekit_cycle.Cycle wrapping forward_model
to roll out trajectories rather than single steps.
obs_op: Optional pipekit_cycle.ObservationOperator applied
after the forward model. Used for SBI training where the
target is observed-space outputs, not model-space states.
seed: PRNG seed.
split: "train" / "val" / "test". Splits realised by
partitioning the seed space (e.g. train uses seeds in
[0, 0.8 * 2^32); val uses [0.8 * 2^32, 0.9 * 2^32); …).
"""
forward_model: Operator # also satisfies ForwardModel
prior: Operator
n_samples: int
cycle: Operator | None = None
obs_op: Operator | None = None
def content_hash(self) -> str:
# Includes forward_model.state_signature, prior config,
# n_samples, seed, split — anything that affects the data.
...
The split-by-seed-partition discipline is what makes
SimulationDataset.with_split("val") deterministic without
materializing the train set first.
CachedDataset¶
A disk-backed cache around any TrainingDataset. Identical interface
to its source.
class CachedDataset(TrainingDataset):
"""Disk-backed cache around any TrainingDataset.
First epoch hits the source and writes to `cache_dir`. Subsequent
epochs read from disk. Cache is keyed on `source.content_hash()`.
Args:
source: The wrapped TrainingDataset.
cache_dir: Directory or fsspec URI (s3://, gcs://, …).
format: Storage format. "zarr" for array-shaped pairs;
"parquet" for record-shaped; "tfrecord" for very large
heterogeneous batches.
"""
source: TrainingDataset
cache_dir: str
format: Literal["zarr", "parquet", "tfrecord"] = "zarr"
def content_hash(self) -> str:
# Inherits source.content_hash() — the cache is just a
# materialisation of the same logical contents.
return self.source.content_hash()
Cache invariants. The cache assumes the source is deterministic
given (get_config(), seed). If the underlying catalog mutates,
the cache stays stale until explicitly invalidated. The cache
exposes an invalidate() method that deletes the cache directory
keyed on the current content hash; full re-cache happens on the
next __iter__.
XarrayWindowDataset (moved to geopatcher)¶
The lazy, indexable windowed-Zarr dataset now lives in geotoolz-patcher as
geopatcher.integrations.train.XarrayWindowDataset (the [train] extra). It
needs geopatcher's temporal stencils, and keeping it here made pipekit import
geopatcher while geotoolz depends on pipekit. It still rides the Equinox
adapter's Grain path and its build_batch_iter hook.
pipekit_train.XarrayWindowDataset raises an ImportError naming the new
home.
Backend handoff¶
Each backend adapter translates TrainingDataset into its native
loader idiom:
| Backend | Translation |
|---|---|
| Equinox | Wrap as a grain.MapDataset source, then |
grain.DataLoader(operations=[..., grain.Batch(N)]) |
|
| Lightning | Wrap as a torch.utils.data.IterableDataset, then |
LightningDataModule with train/val/test loaders |
|
| Keras | tf.data.Dataset.from_generator(dataset.__iter__) |
The user does not normally see this — they pass the
TrainingDataset to TrainingLoop(dataset=...) and the adapter
does the translation. Power users who want backend-native loaders
can construct them directly and bypass Layer 0 entirely.