Skip to content

Data Module

PyTorch Dataset / IterableDataset classes over FITS images and tables, plus make_loader (a DataLoader factory with torchfits cache defaults).

For a single file, use Core I/O or Tables. Use Datasets when you need workers, shuffling (map-style via make_loader), or many samples.

Typical stack: read_tensor / table.read → optional transforms → Fits*Dataset → make_loader.


Choosing a Dataset

Your data Access mode Use
General N-D IMAGE (any rank) Map-style (indexed) FitsTensorDataset
General N-D IMAGE Streaming / multi-worker FitsTensorIterableDataset
2D images / multi-band Map-style (indexed) FitsImageDataset
2D images / multi-band Streaming / multi-worker FitsImageIterableDataset
3D+ cubes Map-style (indexed) FitsCubeDataset
3D+ cubes Streaming / channel slicing FitsCubeIterableDataset
1D / multi-arm spectra Map-style (indexed) FitsSpectrumDataset
1D / multi-arm spectra Streaming / multi-arm FitsSpectrumIterableDataset
One table HDU In-memory catalog FitsTableDataset
One table HDU Streaming catalog FitsTableIterableDataset
Fixed cutouts (path, hdu, x, y, size) Map-style FitsCutoutDataset
Survey mosaics (remote/local) Async staged cutouts FitsStagedCutoutIterableDataset

Channels: flux bands / arms / CCDs stack as tensor channels. IVAR and mask are companion tensors (or dict fields) — pass ivar_hdu= / mask_hdu= (or table ivar_column=).

transform=: each dataset calls transform(payload) with one argument (the tensor, or a {"flux", "ivar"?, "mask"?} dict when companions are set). There is no separate mask= kwarg at the Dataset boundary. Custom transforms should branch on dict vs tensor if they need mask-aware wiring — see custom transforms.

When to mmap

Good for large local IMAGE HDUs and repeated cutouts. Prefer mmap=False when many workers open the same files, on cold network filesystems, and for VLA / scaled tables.

Disk cache (remotes + samples)

HTTP(S)/vos prefetch and example samples write under TORCHFITS_CACHE_DIR (default $XDG_CACHE_HOME/torchfits or ~/.cache/torchfits), with remote/ and samples/ subdirs. Override with TORCHFITS_REMOTE_CACHE / TORCHFITS_SAMPLE_CACHE, or pass cache_dir= on a remote-capable dataset (image / tensor / cube / spectrum). HTTP auth: TORCHFITS_HTTP_AUTHORIZATION or TORCHFITS_HTTP_TOKEN. Uncompressed 2D HTTP cutouts use Range GETs when possible; compressed and vos paths cache the full file first. An HTTP body that does not start like a FITS file (or a gzip/bzip2/compress/zip wrapper) raises OSError naming the URL and is never cached.


Cache: how and when

Cache What it stores How to clear / size
Disk Remote downloads, example samples TORCHFITS_*_CACHE env / cache_dir=
I/O metadata In-process LRUs cache.clear_cache(), clear_file_cache(...)
Everything (incl. disk) Same + the configured cache root (only torchfits download files in a remote override elsewhere) clear_all_caches() / cache.clear_cache(disk=True)

A single-file read_tensor does not need warm-up.

from torchfits import cache

cache.get_cache_stats()
cache.clear_cache()

Warm-up helps when a loader iterates many files (remote prefetch, multi-worker reads, or repeated mosaic cutouts via open_subset_reader).


FitsTensorDataset

Map-style general N-D IMAGE reader (any rank). Multi-HDU hdu=[…] stacks flux on dim 0; optional ivar_hdu / mask_hdu return companion tensors.

from torchfits.data import FitsTensorDataset, make_loader

ds = FitsTensorDataset(
    "observations/*.fits",
    hdu=0,
    label_key="CLASS",  # header keyword → int label
    transform=None,  # optional callable
    device="cpu",
    mmap=True,
    add_channel_dim=False,  # Tensor default: leave rank alone
    cache_dir=None,  # optional remote materialization dir
)
loader = make_loader(ds, batch_size=32, num_workers=4)
Parameter Type Default Description
paths str or list[str] (required) File paths, glob, or HTTP(S) URLs
hdu int or str or sequence 0 Flux HDU(s); multi → channel stack
ivar_hdu / mask_hdu same arity as hdu None Companion HDUs (not flux channels)
label_key str or None None Header keyword for classification labels
labels list[int] or None None Explicit per-file labels (overrides label_key)
transform callable or None None Applied to each payload
device str "cpu" Torch device
mmap bool or str True Memory-mapped reads
add_channel_dim bool False Prepend channel dim for 2D
cache_dir str or Path or None None Override remote prefetch directory
mask_is_dq bool False Decode mask_hdu as a FITS DQ bitfield via mask_from_dq
bad_bits int or sequence or None None Fatal DQ bit positions; default treats any non-zero value as invalid
return_label bool or None None Keyword-only. None/True: items are (payload, label); False: the payload alone (cannot be combined with labels / label_key)

Returns per item: (payload, label) where payload is a Tensor or {"flux", "ivar"?, "mask"?} and label is a torch.long scalar. Without labels= or label_key= the label is still there and is always tensor(0) — kept so existing for x, y in loader loops keep working. FitsCutoutDataset, FitsSpectrumDataset and the iterable datasets return the payload alone instead when no labels are configured. Pass return_label=False to get the same payload-only items here:

from torchfits.data import FitsImageDataset

images = FitsImageDataset("obs/*.fits", return_label=False)
x = images[0]  # a Tensor, no (x, tensor(0)) pair

Masks are validity masks

A mask_hdu companion always comes back as a boolean tensor where True means valid — the one convention every transform and statistic in torchfits shares. FITS DQ extensions are bitfields, so pass mask_is_dq=True (with bad_bits= naming the fatal bits) to decode them. Read verbatim, a perfectly clean all-zero DQ frame would mark every pixel invalid and turn each mask-aware reduction into NaN.

Companions follow the flux rank: with add_channel_dim=True a one-band image yields flux/ivar/mask all of shape [1, H, W], so mask[0] is channel 0 rather than row 0.

When to use

Use FitsTensorDataset when rank/layout is unknown or non-2D. Prefer FitsImageDataset / FitsCubeDataset / FitsSpectrumDataset for those shapes. For 100k+ files, prefer FitsTensorIterableDataset.


FitsImageDataset / FitsCubeDataset

Subclasses of FitsTensorDataset — same constructor signature and returns, with two defaults changed:

  • Image — 2D; add_channel_dim=True by default; multi-band HDUs → [C,H,W].
  • Cube — 3D+; add_channel_dim=False by default; adds slice_index.
from torchfits.data import FitsImageDataset, FitsCubeDataset

images = FitsImageDataset("obs/*.fits", hdu=0, label_key="CLASS")
cubes = FitsCubeDataset("cubes/*.fits", hdu=0, slice_index=None)
Parameter Type Default Description
paths str or list[str] (required) File paths, glob, or HTTP(S) URLs; must select at least one file
hdu int or str or sequence 0 Flux HDU(s); multi → channel stack; must be non-empty
add_channel_dim bool True (Image) / False (Cube) Prepend channel dim for rank-2 payloads
slice_index int or None None (Cube only) Index the leading (spectral) axis after read; must be >= 0
spectral_slice (int, int) or None None (Cube only) Half-open spectral/channel window on the leading axis; mutually exclusive with slice_index
ivar_hdu / mask_hdu / label_key / labels / transform / device / mmap / cache_dir — see FitsTensorDataset Passed through unchanged

Empty selections are refused, not tolerated

Every dataset in torchfits.data rejects a selection with nothing in it — paths must be non-empty, hdu sequence must be non-empty, cutouts must be non-empty. The reason is that an empty dataset does not fail on its own: make_loader(..., shuffle=True) raises from torch's RandomSampler, which validates num_samples > 0, but shuffle=False uses SequentialSampler, which does not check — so without the check the same dataset under the same make_loader would be either an error or a silent zero-work training loop depending on one flag.

A glob that matches nothing is not this case. It keeps falling back to the literal pattern, so it fails at read time with the path you actually asked for in the message.

Returns per item: same as FitsTensorDataset — (payload, label), with label == tensor(0) when no labels are configured; return_label=False returns the payload alone.

FitsImageIterableDataset stays first-class for loaders (same knobs as the tensor iterable, channel dim default on, including labels= / label_key=).

Cubes and IFU cubes

The leading axis of a cube is the spectral (or velocity / channel) axis. Most IFU pipelines want a range, not a single channel:

from torchfits.data import FitsCubeDataset

# A rest-wavelength window instead of the whole cube.
ifs = FitsCubeDataset("cubes/*.fits", spectral_slice=(1200, 2600), label_key="OBJID")

The same window is applied to companion ivar / mask extensions, so they stay aligned with flux. FitsCubeIterableDataset(..., spectral_slice=(a, b)) does the same while streaming.

Discovering bands

discover_bands(path) lists every 2D+ image extension with its name, role and photometric metadata, so datasets can be built from band names instead of hand-counted HDU indices:

from torchfits.data import FitsImageDataset, discover_bands

for band in discover_bands("mosaic.fits"):
    print(band.name, band.role, band.shape, band.zeropoint)
# G flux (4096, 4096) 25.3
# R flux (4096, 4096) 25.1
# Z flux (4096, 4096) 24.8
# G_IVAR ivar (4096, 4096) None

ds = FitsImageDataset.from_bands("mosaic.fits", bands=["G", "R", "Z"])

BandInfo.role is "flux", "ivar", "mask" or "wavelength", inferred from the name suffix, so from_bands(bands=None) selects the flux extensions and automatically attaches matching G_IVAR / G_DQ companions when every selected band has one. A *_DQ companion is a bitfield, so from_bands decodes it into a boolean validity mask by default (mask_is_dq=False to override, bad_bits= to name the fatal bits); mixing *_DQ and *_MASK companions across bands is rejected as ambiguous. Zeropoints come from PHOTZEROPOINT, PHOTZP, ZP, MAGZERO, MAGZP, ABMAGZERO or ZEROPOINT (first present wins; ZP and friends are read as HIERARCH where needed):

zp = discover_bands("mosaic.fits")[0].zeropoint
counts_to_flux = 10 ** (-0.4 * zp)

# or, straight from the dataset:
ds.band_zeropoints()  # {'G': 25.3, 'R': 25.1, 'Z': 24.8}

BandInfo.flux_scale(exptime_normalized=False) returns that factor divided by EXPTIME for flux per second. Feed it to AffineTransform to apply it inside a pipeline.


FitsSpectrumDataset

1D spectra (IMAGE NAXIS=1 or table column= + optional ivar_column=), plus DESI-style 2D [nspec, nwave] via row=. Multi-arm (MOS B/R/Z) uses layout=:

layout Behavior
"dict" (default) Per-arm {name: {flux, ivar?, mask?}} (flat keys if one arm)
"stack" Flux [C, nwave] only if all arms share nwave
"concat" One 1D flux + parallel ivar/mask along wavelength
from torchfits.data import FitsSpectrumDataset

ds = FitsSpectrumDataset(
    "spectra/*.fits",
    hdu=["B", "R", "Z"],
    layout="dict",
)
Parameter Type Default Description
paths str or list[str] (required) File paths, glob, or HTTP(S) URLs
hdu int or str or sequence 0 IMAGE flux HDU(s), one per arm
ivar_hdu / mask_hdu same arity as hdu, or one shared HDU None Companion IMAGE HDUs per arm
column str or None None Table flux column (mutually exclusive with hdu image path)
ivar_column str or None None Table ivar column
mask_column str or None None Table mask / DQ column
wavelength_hdu / wavelength_column same arity as hdu, or one shared HDU None Parallel wavelength array, attached as payload["wavelength"]
mask_is_dq bool False Read the mask extension/column as a FITS DQ bitfield and return a boolean validity mask
bad_bits int or sequence or None None Bit positions passed to mask_from_dq when mask_is_dq=True (default: any non-zero DQ value is invalid)
label_key / labels str / list[int] or None None Per-file classification labels
row int or None None Row index into a DESI-style [nspec, nwave] HDU/column; the HDU must have rank ≥ 2, otherwise this raises ValueError
layout str "dict" "dict", "stack", or "concat"
transform callable or None None Applied to the laid-out payload
device str "cpu" Torch device
mmap bool or str True Memory-mapped reads
cache_dir str or Path or None None Override remote prefetch directory

Returns per item: payload per layout (see table above). When labels= or label_key= is given the item becomes (payload, label); otherwise the payload is returned alone, so existing call sites are unchanged.

Synthetic DESI-shaped demo: examples/desi_shaped_spectrum.py.


FitsTensorIterableDataset

Iterable dataset for multi-worker sharded tensor loading. Each worker processes a disjoint, deterministic subset — every file is seen exactly once per epoch. Which order those files arrive in is a separate question, answered by shuffle and seed (see the note below).

from torchfits.data import FitsTensorIterableDataset

ds = FitsTensorIterableDataset(
    "observations/*.fits",
    hdu=0,
    shuffle=True,
    seed=42,
    add_channel_dim=False,
)
Parameter Type Default Description
paths str or list[str] (required) File paths, glob, or HTTP(S) URLs
hdu int or str or sequence 0 Flux HDU(s)
transform callable or None None Applied to each payload
device str "cpu" Torch device
mmap bool or str True Memory-mapped reads
shuffle bool False Shuffle file order; a different permutation each epoch
seed int 0 Base seed for shuffling — the same seed replays the same sequence of epochs
add_channel_dim bool False Prepend channel dimension
cache_dir str or Path or None None Override remote prefetch directory
label_key str or None None Keyword-only. Header keyword of the first flux HDU holding each file's integer label
labels list[int], Tensor or None None Keyword-only. Explicit per-file labels (overrides label_key); length must match the files

Returns per item: payload (Tensor or flux/ivar/mask dict) alone. With labels= or label_key= the item becomes (payload, label), label a torch.long scalar. Each label stays with its own file through rank and worker sharding, shuffle= and shuffle_buffer_size= (duplicate paths keep their own labels):

from torchfits.data import FitsImageIterableDataset, make_loader

files = ["a.fits", "b.fits", "c.fits"]
ds = FitsImageIterableDataset(files, labels=[0, 1, 0], shuffle=True, seed=0)
if __name__ == "__main__":
    for image, label in make_loader(ds, batch_size=2, num_workers=2):
        ...

When to use

Use when you have many files and want deterministic multi-worker loading without file duplication. Unlike map-style + num_workers, each worker gets a disjoint shard of files.

Sharding is deterministic; shuffling is per-epoch

Two different guarantees, and only the first one is "same every time":

  • The shard is a function of (rank, world_size, worker_id) alone, so every rank and worker sees its own disjoint slice on every epoch.
  • The order within the shard changes every epoch when shuffle=True or shuffle_buffer_size= is set: the shuffle mixes a per-dataset epoch counter with torch.initial_seed().

seed= makes a run reproducible: two datasets built with the same seed in the same process replay the same sequence of epochs. If you need one fixed order — a fixture, a golden-output test — leave shuffle=False and set no shuffle_buffer_size.


FitsTableDataset

Map-style dataset for row-indexable FITS catalogs. Loads the full filtered table at __init__ — use only when the catalog fits in RAM.

from torchfits.data import FitsTableDataset

ds = FitsTableDataset(
    "catalog.fits",
    hdu=1,
    columns=["RA", "DEC", "MAG_G"],
    where="MAG_G < 20",  # predicate pushdown at load time
    labels=[0, 1, 0, 1, 0, 1],  # optional per-row labels (default 0)
    transform=None,
)
Parameter Type Default Description
path str (required) FITS file path
hdu int 1 Table HDU index
columns list[str] or None None Column names (None = all)
where str or None None SQL-like predicate for row filtering
labels list[int] or None None Per-row integer labels (default 0 when omitted)
transform callable or None None Applied to each row dict
device str "cpu" Torch device
mmap bool or str "auto" Memory-mapped reads
return_label bool or None None Keyword-only. None/True: items are (row_dict, label); False: the row dict alone (cannot be combined with labels)

Returns per item: (dict[str, Tensor], torch.Tensor) — row dict and torch.long label, which is tensor(0) for every row when labels is omitted. return_label=False returns the row dict alone.

Rows hold the values as stored, whether or not where= is set: where= only selects rows. Integer columns keep their dtype and a TNULL null comes back as its sentinel value; character columns are uint8 byte tensors (space padded). Convert nulls to NaN with TNullToNan:

import torchfits
from torchfits.data import FitsTableDataset
from torchfits.transforms import TNullToNan

header = torchfits.read_header("catalog.fits", 1)
ds = FitsTableDataset(
    "catalog.fits", where="MAG_G < 20", transform=TNullToNan.from_header(header)
)

When to use

Use for catalogs up to a few million rows that fit in memory. For larger catalogs, use FitsTableIterableDataset.


FitsTableIterableDataset

Streams table rows in constant memory. Each iteration yields one dict[str, Tensor] row.

from torchfits.data import FitsTableIterableDataset

ds = FitsTableIterableDataset(
    "survey.fits",
    hdu=1,
    columns=["RA", "DEC"],
    where="DEC > 0",
    batch_size=65536,
    mmap="auto",
)
Parameter Type Default Description
path str (required) FITS file path
hdu int 1 Table HDU index
columns list[str] or None None Column projection
where str or None None SQL-like predicate
batch_size int 65536 Rows per internal scan batch
as_batches bool False Yield dict[str, Tensor] chunks of rows instead of one dict per row; character and bit columns are omitted
transform callable or None None Applied to each row dict (or each chunk when as_batches=True)
device str "cpu" Torch device
mmap bool or str "auto" Memory-mapped reads

Returns per item: dict[str, Tensor | Any] — one row, or one chunk of batch_size rows when as_batches=True.

Iterating the unfiltered dataset yields exactly as many rows as FitsTableDataset reports len for the same file and HDU, including when every column is variable-length. A variable-length column arrives from scan_torch as a Python list rather than a tensor, so the per-chunk row count is read from tensor or list columns.

Rows and chunks follow the same convention as FitsTableDataset: values as stored, with or without where=. A TNULL null is its sentinel value and character columns are uint8 byte tensors; use TNullToNan for NaN nulls. where= accepts the full table.read predicate dialect and is evaluated per chunk on the predicate columns only.

When to use

Use for large catalogs that don't fit in memory. Each worker (and each distributed rank) gets a contiguous, near-equal row range through the engine's row_slice, so every row is seen exactly once, no worker decodes rows it will discard, and the split stays even even when batch_size dwarfs the row count.

Per-row vs tensor-space

Per-row mode (as_batches=False, the default) is what you want for collate_fn-style row sampling. as_batches=True keeps the data as tensor chunks end to end and skips the per-row split — markedly faster for wide catalogs; where= works in both modes. Character (TFORM A) and bit (X) columns are omitted so both scanner paths agree; read them in per-row mode. The two modes are not mixable with shuffle_buffer_size=, which needs individual rows.


FitsCutoutDataset

Map-style dataset for fixed cutout windows from one or more FITS images.

from torchfits.data import FitsCutoutDataset

cutouts = [
    ("mosaic.fits", 0, 100, 200, 164),  # (path, hdu, x, y, size)
    ("mosaic.fits", 0, 300, 400, 164),
]
ds = FitsCutoutDataset(cutouts, transform=None, device="cpu")

Accepts (path, hdu, x, y, size) or (path, hdu, x1, y1, x2, y2) tuples. The public CutoutSpec type alias names the normalized six-item form: tuple[str, int, int, int, int, int], corresponding to (path, hdu, x1, y1, x2, y2) with half-open upper bounds.

Every spec must describe a window that actually selects pixels. The origin must be non-negative (x1 >= 0, y1 >= 0) and the half-open window must be non-empty (x1 < x2 and y1 < y2); otherwise the constructor raises ValueError. A degenerate window is rejected at construction rather than yielding a 0-pixel tensor, because fits_collate_fn stacks such shapes without complaint and make_loader would hand a model a batch whose numel() is 0. A window whose far edge runs off the image is still clamped to the overlap.

Parameter Type Default Description
cutouts Sequence (required) Non-empty list of cutout specs; each must select at least one pixel
transform callable or None None Applied to each cutout tensor
device str "cpu" Torch device
add_channel_dim bool True Prepend channel dimension

Returns per item: Tensor — raw cutout tensor without a label. This dataset is unsupervised by design (it has no labels parameter). For supervised patch classification, subclass FitsCutoutDataset and override __getitem__ to return (tensor, label), or create a custom Dataset that pairs read_subset calls with labels.

When to use

Use for patch training from a mosaic. Each cutout uses pixel coordinates supplied in the spec — torchfits has no WCS layer. If most cutouts share one file, open_subset_reader avoids repeated handle opens.


make_loader()

Builds a torch.utils.data.DataLoader with torchfits collation and shuffle defaults.

from torchfits.data import make_loader

loader = make_loader(
    ds,
    batch_size=32,
    num_workers=4,
    pin_memory=True,
    shuffle=True,  # default for map-style; False for iterable
)

Use a plain DataLoader when you already own collate_fn or samplers. See examples/example_make_loader_vs_dataloader.py.

Parameter Type Default Description
dataset Dataset or IterableDataset (required) A torchfits dataset
batch_size int 32 Batch size
shuffle bool or None None Auto: True for map-style, False for iterable
num_workers int 0 Worker processes
pin_memory bool False Pin for GPU transfers
prefetch_factor int 2 Prefetch per worker
drop_last bool False Drop incomplete batches
**loader_kwargs Passed to DataLoader (collate_fn defaults to fits_collate_fn)

Returns: torch.utils.data.DataLoader


fits_collate_fn

Default collation function for torchfits datasets:

  • list[Tensor] → stacked Tensor (all must have the same shape)
  • list[(Tensor, Tensor)] → (stacked_images, stacked_labels)
  • list[dict[str, Tensor]] → dict[str, stacked_Tensor]
  • list[(dict[str, Tensor], Tensor)] → (stacked_dict, stacked_labels)
  • Nested dicts, such as multi-arm spectra {arm: {"flux": Tensor, ...}}, are collated per leaf: batch["R"]["flux"] has shape (batch, n_pixels)

All tensors in a batch must have identical shapes — torch.stack is used under the hood. Raises ValueError on non-tensor columns (strings, VLA lists). For variable-size images, either pad externally before collation or supply a custom collate_fn.



FitsCubeIterableDataset

Streaming peer for 3D+ datacubes (e.g. IFUs, velocity cubes, radio cubes). Optionally slices along the leading spectral/velocity axis on the fly via slice_index=.

from torchfits.data import FitsCubeIterableDataset

ds = FitsCubeIterableDataset(
    "cubes/*.fits",
    hdu=0,
    slice_index=15,  # Extracts channel 15 on the fly
    shuffle=True,
    shuffle_buffer_size=100,  # In-flight reservoir shuffle
)
Parameter Type Default Description
paths str or list[str] (required) FITS file paths, glob, or URLs
hdu int or str or sequence 0 Primary datacube HDU(s)
slice_index int or None None Optional index along the leading channel/spectral axis; must be >= 0 (a negative index would silently pick a channel from the far end)
spectral_slice (int, int) or None None Half-open spectral window on the leading axis (mutually exclusive with slice_index)
transform callable or None None Applied to each payload
shuffle bool False Shuffle file order per epoch
shuffle_buffer_size int or None None Rolling reservoir buffer for in-flight random mixing
rank int or None None Distributed worker rank
world_size int or None None Distributed world size
label_key / labels str / list[int] or Tensor or None None Per-file labels; items become (payload, label)

FitsSpectrumIterableDataset

Streaming 1D multi-arm spectra from binary table columns (e.g. DESI, BOSS) or multi-HDU image extensions.

from torchfits.data import FitsSpectrumIterableDataset

ds = FitsSpectrumIterableDataset(
    "spectra/*.fits",
    hdu=["B_FLUX", "R_FLUX", "Z_FLUX"],
    ivar_hdu=["B_IVAR", "R_IVAR", "Z_IVAR"],
    layout="stack",  # 'dict', 'stack' [C, nwave], or 'concat'
)
Parameter Type Default Description
paths str or list[str] (required) File paths or URLs
hdu int or str or sequence 0 Primary spectrum HDU(s)
ivar_hdu int or str or sequence or None None Companion inverse variance HDU(s)
mask_hdu int or str or sequence or None None Companion mask HDU(s)
column str or None None Table column name for binary table spectra
ivar_column str or None None Table column for companion IVAR
row int or None None Optional row index within multi-spectrum HDUs
layout str "dict" Layout format: "dict", "stack", or "concat"
mask_column / wavelength_column / wavelength_hdu str / str / HDU spec or None None Same companions as FitsSpectrumDataset
mask_is_dq / bad_bits bool / sequence or None False / None Interpret the mask source as a DQ bitfield
label_key / labels str / list[int] or None None Per-file labels; items become (payload, label)
shuffle_buffer_size int or None None Reservoir shuffle buffer size

FitsStagedCutoutIterableDataset

Asynchronous staged cutout extraction for massive astronomical survey mosaics (CFHT MegaCam, HSC, Rubin LSST, MegaPipe). Downloads remote mosaics to fast ephemeral scratch (e.g. $SLURM_TMPDIR), samples \(K\) random stamps rapidly using open_subset_reader, and automatically deletes the staged file upon completion.

from torchfits.data import FitsStagedCutoutIterableDataset

ds = FitsStagedCutoutIterableDataset(
    paths=["https://archive.org/mosaic1.fits", "https://archive.org/mosaic2.fits"],
    cutouts_per_file=500,  # 500 cutouts sampled per staged mosaic
    cutout_size=(128, 128),
    staging_dir=None,  # Auto-detects $SLURM_TMPDIR, $TMPDIR, or scratch
    cleanup=True,  # Delete staged mosaic when finished sampling
    shuffle_buffer_size=1000,  # Mix stamps across mosaics in-flight
)
Parameter Type Default Description
paths str or list[str] (required) Mosaic file paths or remote URLs
cutouts_per_file int 100 Number of cutouts to sample per mosaic; must be >= 1
cutout_size int or tuple[int, int] 128 Output cutout dimensions (H, W)
hdu int or str or sequence 0 Primary/flux HDU(s); sequence stacks channels
ivar_hdu int or str or sequence or None None Companion inverse variance HDU(s)
mask_hdu int or str or sequence or None None Companion mask HDU(s)
mask_is_dq / bad_bits bool / sequence or None False / None Interpret the mask source as a DQ bitfield
staging_dir str or Path or None None Ephemeral scratch directory (defaults to $SLURM_TMPDIR / $TMPDIR)
cleanup bool True Automatically delete downloaded mosaic after sampling
cutout_generator callable or None None Custom spatial coordinate sampler (height, width, ch, cw) -> (x1, y1, x2, y2)
transform callable or None None Applied to each cutout tensor or companion dict
shuffle_files bool False Shuffle mosaic order per epoch
shuffle_buffer_size int or None None In-flight reservoir shuffle across mosaic stamps

Distributed & Worker Sharding

torchfits iterable datasets natively partition across both multi-process DataLoader workers and multi-GPU / multi-node distributed training (rank and world_size).

Automatic Coordinate Resolution

When rank and world_size are omitted, torchfits automatically checks: 1. Cluster environment variables, first present wins. Rank: RANK (torchrun, DeepSpeed, FSDP), SLURM_PROCID (SLURM), then LOCAL_RANK. World size: WORLD_SIZE, SLURM_NTASKS, then SLURM_NPROCS. 2. torch.distributed process group (if initialized). 3. Single-worker fallback (rank=0, world_size=1).

2-Level Strided Sharding

  1. Rank Sharding: Datasets first partition files across distributed ranks: files[rank::world_size]. Each GPU or node processes a disjoint subset of files.
  2. Worker Sharding: Within each rank, the rank's subset is partitioned across DataLoader workers:
    per_worker = total // num_workers
    remainder  = total %  num_workers
    start      = worker_id * per_worker + min(worker_id, remainder)
    size       = per_worker + (1 if worker_id < remainder else 0)
    

Every file is seen exactly once across the entire cluster without duplication.

Reservoir Shuffle Buffer

Because streaming datasets do not load all samples into memory at once, setting shuffle_buffer_size=N enables an \(O(N)\) streaming reservoir shuffle. Incoming items from the dataset stream are mixed with past items in a rolling buffer, yielding pseudo-random batches across files and mosaics.

The buffer is re-seeded every epoch, so each epoch mixes differently. It is a reservoir shuffle, not a uniform permutation: an item's chance of landing in a given output slot rises with its position in the stream, which is the standard streaming trade-off (measured here over 20,000 seeds with \(N=8\) and buffer 3: the last input item ended up in the final slot 33.6% of the time, the first 4.4%). The buffer size is therefore the shuffle window — raise it towards the dataset size when you need a stronger mix, at \(O(N)\) memory.