scigantic-headers
Read the metadata of a scientific file and return its fields (dimensions, data type, pixel size, columns, row count) as a dict, without reading the rest of the file. Decodes formats across cryo-EM, imaging, arrays, tabular data, flow cytometry, mass spec, sequencing, genome annotation, and structural biology (the full list is below). The decode functions have no dependencies; separate reader functions fetch the bytes from a file or a URL (the leading bytes for most formats, the trailing bytes for Parquet, whose schema is in a footer).
from scigantic_headers import decode_file
hdr = decode_file("session/frame.mrc")
hdr.summary # "MRC stack 4096x4096x16, float32"
hdr.fields["dtype"] # "float32"
hdr.fields["nz"] # 16 (frames)
Zero runtime dependencies and header-only reads, so it runs air-gapped, on amd64 or arm64, and installs into a slim image with nothing else to pull.
Install
pip install scigantic-headers
Published to PyPI on a v* tag by .github/workflows/publish.yml. Pin the
version in anything that consumes it (scigantic-headers==X.Y.Z). With no
package index at build time, install from a wheel or a checkout instead:
pip install ./scigantic-headers
Why it exists
You often want to know what a file is, its shape, dtype, and key acquisition fields, without reading the whole thing. A scientific file's first ~1 KiB is a structured header that already says so. This reads that, and nothing more.
Decoders today:
- MRC / MRCS: cryo-EM micrographs, movie stacks, tilt series, EMDB maps.
- NPY: NumPy arrays. Cross-checked against numpy.
- NIfTI-1: neuroimaging volumes,
.niiand.nii.gz. Cross-checked against nibabel. Big- and little-endian. - CryoSPARC
.cs: a structured-array dataset. Reports record count and the field schema. - Parquet: reads the footer, not a leading header, and returns the column names, physical types, and row count. Cross-checked against pyarrow.
- FCS: flow cytometry (Flow Cytometry Standard). Returns parameter and event counts, data type, and per-channel names. Cross-checked against flowio.
- mzML: mass spectrometry. Returns the spectrum count, instrument model, software, source file, and run start time from the XML preamble. Cross-checked against a compliant XML parse.
- FASTQ: sequencing reads. Parses the Illumina read-name convention in the first record for instrument, run, flowcell, and lane, plus the read length.
- Illumina run: RunInfo.xml / RunParameters.xml. Instrument, flowcell, run id, date, and read structure. Dispatched by file name, not extension.
- VCF: variant calls. Version, sample names, reference, and meta-line counts from the header.
- SAM: alignment header. Version, sort order, reference sequences, read groups, and programs.
- PDB: structure header. PDB id, classification, deposition date, title, experiment method, and resolution.
- mmCIF / CIF: structures (the modern PDB format). Entry id, title, keywords, experiment method, deposition date, and resolution (including the cryo-EM resolution item).
- GenBank: annotated sequence records. LOCUS (name, length, molecule type, topology), definition, accession, version, and organism.
- GFF3 / GTF: genome annotation. Version and a preview of the sources and feature types.
- BED: genome intervals. BED flavor (column count) and track-line presence.
- DICOM: medical images. Modality, dimensions, bit depth, manufacturer, and study/series description. Reads technical fields only, never patient identifiers.
.gz is transparent. The reader decompresses just the leading block, so a
gzipped .nii.gz or .mrc.gz decodes without inflating the whole file, and
dispatch sees the inner format.
Adding a format is a pure bytes -> DecodedHeader | None function plus one
register_decoder call. That call also declares how many bytes the decoder needs
and from which end (read=Read(leading=...), or read=Read(footer=...) for a
schema in the tail like Parquet), so the read strategy lives next to the decoder,
not in a table elsewhere. Nothing about the dispatch, the readers, or the batch
path is format-specific: the readers ask the registry how to read and never name
a format. NPY and NIfTI landed with zero plumbing changes.
Pixel size for raw movies
Cross-check the acquisition settings
Voltage, pixel size, dose and frame count usually arrive from several places typed
or exported by different people. A wrong one does not fail; it quietly caps the
resolution. compare_acquisition lines the sources up and reports disagreements:
from scigantic_headers import compare_acquisition, decode_epu_xml, read_star_optics
epu = decode_epu_xml(open("FoilHole_..._Data_....xml", "rb").read()).fields
for f in compare_acquisition({
"sheet": {"voltageKv": 300, "pixelSizeA": 0.84, "doseEA2": 42.4, "fractions": 40},
"epu": epu,
"relion": read_star_optics("Import/movies.star"),
}, target_resolution_a=3.3):
print(f.severity, f.field, f.message)
It does not decide which source is right. EPU records the pixel size of the binned
readout grid, so epu_pixel_size_candidates(epu) lists the likely values for the
saved movie; compare with the movie's own dimensions. decode_epu_xml is not
auto-dispatched (.xml is too generic), so call it on a file you know is EPU's.
A raw MRC movie header has no pixel size: CELLA is 0, so decode_* returns
pixelSizeA as None. The value is in the RELION STAR or CryoSPARC .cs file
the workflow writes in the session, and the library reads it. read_session_optics
finds that file next to (or above) the data file and returns the optics:
from scigantic_headers import decode_file, read_session_optics
hdr = decode_file("Movies/frame.mrc") # header fields; pixelSizeA is None
opt = read_session_optics("Movies/frame.mrc")
# {'pixelSizeA': 1.05, 'voltageKv': 300.0, 'source': 'relion-star:particles.star'}
read_star_optics(path) and read_cryosparc_optics(path) read a specific optics
file when you already know which one. The RELION 3.1 data_optics loop, the
older key-value form, optics columns in the main data loop, and the legacy
detector-pixel-size / magnification pair are all handled.
Layout
src/scigantic_headers/
decoders.py pure core: registry + MRC / NPY / NIfTI / .cs decoders
parquet.py Parquet footer decode (small Thrift-compact reader)
fcs.py FCS (flow cytometry) header + TEXT-segment decode
mzml.py mzML (mass spec) XML-preamble metadata decode
fastq.py FASTQ first-record / read-name decode
illumina.py Illumina RunInfo.xml / RunParameters.xml decode
vcf.py VCF header decode
sam.py SAM alignment-header decode
pdb.py PDB structure-header decode
mmcif.py mmCIF / CIF structure-header decode
genbank.py GenBank flat-file header decode
gff.py GFF3 / GTF annotation-header decode
bed.py BED interval-file decode
dicom.py DICOM technical-metadata decode (no patient data)
sources.py read leading bytes from file or URL; bounded parallel batch
star.py RELION STAR optics reader
cryosparc.py CryoSPARC .cs optics reader
optics.py read_session_optics: find a data file's optics file
cli.py scigantic-headers <file|url|--dir>
benchmark.py scigantic-headers-bench, measures the speed levers
tests/ pytest (208 tests, incl. fuzz + golden fixtures)
Robustness
A decoder is handed the leading bytes of arbitrary files, so it must be total: for any input it returns None or a valid, JSON-safe DecodedHeader. It never raises and never emits NaN or inf. This is enforced, not assumed.
- Fuzz (
test_fuzz.py): thousands of random and magic-seeded byte buffers through every decoder. Each result must serialize withallow_nan=False. - Golden fixtures (
fixtures/mrc-cases.json): hex input to exact decode. A change to a decoder that the fixture does not expect fails a test rather than passing silently. - Non-finite floats (a garbage NIfTI pixdim, an inf MRC cell) sanitize to null.
Speed
The decode is microseconds: a 1 KiB read and a few struct unpacks. Optimizing
that is a rounding error. All the speed is in I/O, and two things carry it. Both
are measured, not asserted (run scigantic-headers-bench).
Read the header, not the file. The read is bounded (HEADER_BYTES, 1 KiB, for
most formats; a few hundred KB for text formats with long headers, declared per
decoder), so its cost is independent of file size and a multi-GB movie decodes as
fast as a small one.
header read + decode (1 KiB): 15 us
full-file read + decode (200 MB): 22.7 ms (about 1,500x cheaper)
Parallelize the I/O, bounded. Header reads across files are independent and I/O-bound, so a thread pool overlaps their latency (the read releases the GIL). Measured over 8 headers pulled by HTTP Range from a remote archive:
serial (1 worker): 25.7 s
parallel (8 workers): 0.7 s (about 35x)
That 35x is large because the server's per-request latency is about 3 s, so overlapping eight requests recovers a lot of dead wait. On low-latency local storage the speedup is smaller. The win scales with per-read latency, which is why the pool is bounded and tunable (default 8; past a point more connections hit server throttling or disk-queue thrash and get slower).
Two more, by construction:
- Filter before I/O.
has_decoder_foris a cheap extension check. The batch and walk paths use it to skip a file before any syscall. - Zero deps, fast import. The dtype table is a plain dict, not numpy. There is nothing to install and nothing to import, and it runs air-gapped.
The biggest system-level lever lives in the caller, not here: decode once when a file lands, cache the small result, and answer every later query from it without re-decoding.
Use
from scigantic_headers import decode_file, decode_paths, decode_urls, iter_decodable_files
decode_file("s/frame.mrc") # one local file
decode_paths(iter_decodable_files("/data/sessions")) # a tree, parallel
decode_urls([...], workers=8) # remote, by Range
# CLI
scigantic-headers session/frame.mrc
scigantic-headers https://example.org/path/img.mrcs
scigantic-headers --dir /data/sessions --workers 8
pytest
scigantic-headers-bench # reproduce the numbers above
Limitations
- Pointer-based formats are not supported. HDF5 and TIFF store their layout behind internal pointers, so reading the header or footer alone is not enough. Parquet (a footer) is supported; formats that need to chase offsets through the file are not. Plain gzip is handled (the reader inflates the leading block), but BAM's BGZF framing and per-record structure are not.
- Output is not a standard. The fields are an ad-hoc dict, not an interchange format such as Allotrope ASM. Map the dict to a standard if you need one.
License
MIT. See LICENSE.
Metadata
Release files for scigantic-headers 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| scigantic_headers-0.5.0.tar.gz | 66.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| scigantic_headers-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 116.9 kB
Release files / scigantic_headers-0.5.0.tar.gz
| Download URL | scigantic_headers-0.5.0.tar.gz |
|---|---|
| Size | 66.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6c12c08a0d0e8b589457e9b291afa1a7cb18c31de09af1f619ac8f5d8a9d5aad
|
|
BLAKE2b-256 checksum How to use checksums |
7cd214673aed7819e14973d4b809cdd2a8ee51288dfefde61d3b613e6e60ec57
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.17
|
Release files / scigantic_headers-0.5.0-py3-none-any.whl
| Download URL | scigantic_headers-0.5.0-py3-none-any.whl |
|---|---|
| Size | 50.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f084c33ac26581aa7a923167b3745da2d7e1e0ba8e724a1f28c645f53e1f66a7
|
|
BLAKE2b-256 checksum How to use checksums |
37bd9d53edb8aaff8d7a139cb7734e2fef1140ae4324d5433090efc13b916c2c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.17
|