Skip to main content

Crate mrc

Crate mrc 

Source
Expand description

Read and write MRC-2014 files — the standard in cryo-EM and structural biology. Handles byte-order, type conversion, and compression so you can focus on your science. SIMD-accelerated, mmap-enabled.

See the README for installation. The companion mrc-cli crate provides a command-line tool for inspection, validation, conversion, and export.

§Quick example

use mrc::{read_as, write_as};

// Read an entire volume in one call
let (header, data): (_, Vec<f32>) = read_as("density.mrc")?;
println!("{}×{}×{} volume, {} voxels",
    header.nx, header.ny, header.nz, data.len());

// Write a volume in one call
write_as("output.mrc", &data, [512, 512, 256])?;

§Reading files

Open any MRC file with open() or Reader::open. Compression is detected from magic bytes — no need to hint gzip or bzip2.

use mrc::Reader;
let reader = Reader::open("tilt_series.mrc")?;
println!("{}×{}×{} voxels, mode {:?}",
    reader.shape().nx, reader.shape().ny, reader.shape().nz,
    reader.mode());

Then pick an iteration method, each returning DataBlock chunks whose DataView variant is determined by the file’s mode — no compile-time type guessing, no ModeMismatch errors at runtime:

  • slices — one Z-plane at a time
  • slabs — batches of k Z-planes
  • tiles — arbitrary 3D blocks
  • volumes — sub-volumes in a volume stack
  • subregion — a single block by coordinate
  • read_volume — the entire volume as one block

The file’s voxel type is known at runtime via reader.mode():

for slice in reader.slices() {
    let block = slice?;
    match block.data() {
        mrc::DataView::Float32(data) => { /* process f32 slice */ }
        mrc::DataView::Int16(data)   => { /* process i16 slice */ }
        mrc::DataView::Uint16(data)  => { /* process u16 slice */ }
        mrc::DataView::Int8(data)    => { /* process i8 slice */ }
        other                        => panic!("unhandled mode: {:?}", other),
    }
}

Or just use convert::<f32>() — the fire-and-forget option that reads any mode as f32:

for slice in reader.convert::<f32>().slices() {
    let block = slice?;
    println!("z={}: {} voxels", block.offset[2], block.data.len());
}
// Full volume in one call:
let block = reader.convert::<f32>().read_volume()?;

The same iteration methods — slabs, tiles, subregion — plus with_complex_strategy and with_m0_interpretation are all available on the returned converter.

§Special-mode reads (Packed4Bit and Mode 0)

These modes have no Voxel implementation — there is no single Rust type that maps to them safely at compile time — so dedicated methods read directly as u8 or f32:

§Reading from memory or streams

When your data is already in memory (e.g. from a camera readout, network stream, or embedded resource), use Reader::from_reader or Reader::from_bytes:

use mrc::Reader;
use std::io::Cursor;

let bytes = std::fs::read("density.mrc")?;
let reader = Reader::from_reader(Cursor::new(bytes))?;

Permissive variants Reader::from_reader_permissive and Reader::from_bytes_permissive accept non-critical header issues as warnings without failing, mirroring Reader::open_permissive.

§Large files

When the file does not fit in RAM, Reader::open automatically uses memory-mapped I/O (requires the mmap feature). Same iterator API, zero-copy DataBlock views, OS-managed paging.

For buffered readers (in-memory buffers, compressed files), the default reader methods also return zero-copy views when the requested block is a native-endian contiguous full-row slab.

§Quirky files

Common microscope quirks (NVERSION left at 0, "MAP\0" instead of "MAP ") are handled transparently by open() — no special flags needed.

For esoteric or severely non-standard files, use Reader::open_permissive which turns non-critical header issues into warnings instead of hard errors, and allows opening files that are shorter than the header declares. Use is_truncated to detect truncated data after a permissive open:

let (reader, warnings) = Reader::open_permissive("legacy.mrc")?;
if reader.is_truncated() {
    eprintln!("warning: file is incomplete");
}
for w in &warnings { eprintln!("note: {w}"); }

§Writing files

Use create() to get a WriterBuilder, set the shape and voxel type, then call finish.

For the simplest case — write an entire volume — use write_as() or Writer::set_data:

use mrc::write_as;
let data = vec![0.0f32; 256 * 256 * 128];
write_as("output.mrc", &data, [256, 256, 128])?;

For streaming writes (one slice at a time), use the builder API:

use mrc::{create, VoxelBlock};
let mut writer = create("output.mrc")
    .shape([256, 256, 128])
    .mode::<f32>()
    .finish()?;

// Write some data
writer.write_block(&VoxelBlock::new(
    [0, 0, 0], [256, 256, 1],
    vec![0.0f32; 256 * 256],
)?)?;

// Optionally compute and store density statistics
writer.update_header_stats()?;

// Finalize — required, rewrites header with final metadata
writer.finalize()?;

The lifecycle:

  1. Write blocks with write_block. The type T matches the file’s mode — a compile-time check that prevents accidentally treating bytes as the wrong kind of number. Use write_block_as for automatic conversion (e.g. write f32 data to an Int16 or Float16 file)

    For Uint16 files, write_u8_block auto-widens u8 data; for Packed4Bit files, write_u4_block packs u8 values (0–15) two-per-byte.

  1. Optionally call update_header_stats to fill in dmin/dmax/dmean/rms.
  2. Finalize with finalize to rewrite the header with final metadata. Required — without it the header is stale.

Four backends through the same builder, plus in-memory output:

BackendBuilder methodBest for
Writerfinish()General use, writes straight to disk
Writer (in-memory)finish_buffer()Memory buffer, e.g. testing or in-memory processing
Writer (mmap)finish_mmap()Very large files (mmap feature)
Writer (gzip)finish_gzip()Compressed output (gzip feature)
Writer (bzip2)finish_bzip2()Compressed output (bzip2 feature)

Note: The builder is the recommended path. The lower-level Writer::from_writer, Writer::from_writer_mmap, Writer::from_writer_gzip, and [Writer::from_writer_bzip2] constructors are also available for callers who already have a Header or a custom I/O target — see their respective docs for details.

Compressed writers support configurable CompressionLevel level via WriterBuilder::compression:

use mrc::{CompressionLevel, create};

let mut writer = create("output.mrc.gz")
    .shape([256, 256, 128])
    .mode::<f32>()
    .compression(CompressionLevel::Best)
    .finish_gzip()?;

§Data modes

MRC files encode voxels in one of several numeric modes. Mode represents them at runtime; Voxel ties each Rust type to its mode at compile time, catching mismatches before any data flows.

ModeRust typeTypical use
Int8 (0)i8Binary masks
Int16 (1)i16Raw cryo-EM density
Float32 (2)f32Processed / reconstructed density
Int16Complex (3)Int16ComplexComplex data (i16 real + i16 imag)
Float32Complex (4)Float32ComplexComplex data (f32 real + f32 imag)
Uint16 (6)u16Segmentation labels
Float16 (12)f16Half-precision storage (feature f16)
Packed4Bit (101)u8 via slices_u84-bit packed data; no Voxel impl

Packed 4-bit data is handled transparently by the unified API: convert::<f32>() unpacks nibbles to f32, slices_u8 / slabs_u8 unpack to u8 (0–15), and write_u4_block packs u8 values back.

When you don’t know the mode ahead of time, use convert::<f32>() which converts any mode to f32.

§Feature flags

FeatureDescriptionDefault
mmapMemory-mapped readers and writers
f16Half-precision float via the half crate
simdAVX2 / NEON acceleration for integer↔f32, f16↔f32, byte-swap, stats, and f32→integer clamping
parallelParallel decode/convert/encode via rayon — transparent for any block ≥512³
gzipGzip-compressed I/O
bzip2Bzip2-compressed I/O
ndarrayReturn volumes as ndarray::Array3<T> via to_ndarray()
serdeSerialize/Deserialize support via serde
use ndarray::Array3;
let arr: Array3<f32> = reader.convert::<f32>().to_ndarray()?;
println!("{}×{}×{} array", arr.shape()[0], arr.shape()[1], arr.shape()[2]);

§Headers

The Header struct mirrors the 1024-byte MRC-2014 fixed header. Every field is a typed public member — dimensions, cell parameters, axis mapping, density statistics, text labels, and more.

use mrc::Header;
let h = Header::new();
assert_eq!(h.map, *b"MAP ");

For fluent construction with validation, use HeaderBuilder:

use mrc::HeaderBuilder;
let header = HeaderBuilder::new()
    .shape([512, 512, 256])
    .mode::<f32>()
    .cell_lengths(1.0, 1.0, 1.0)
    .cell_angles(90.0, 90.0, 90.0)
    .origin([0.0, 0.0, 0.0])
    .nstart([0, 0, 0])
    .sampling([512, 512, 256])
    .axis_mapping([1, 2, 3])
    .add_label("reconstructed map")
    .build()?;

Three validation levels:

§Volume type helpers

The header distinguishes four volume types. Configure them explicitly when creating files that are not single 3D volumes:

let mut h = Header::new();
h.nx = 64; h.ny = 64; h.nz = 120;
h.mx = 64; h.my = 64; h.mz = 30;
h.set_volume_stack(30); // ispg = 401, mz = 30
assert!(h.is_volume_stack());
assert_eq!(h.logical_shape(), [4, 30, 64, 64]); // 4 sub-volumes

§Convenience API

The Header provides computed properties for common queries:

MethodReturnsDescription
validate()boolQuick validity check
validate_detailed()Result<(), HeaderValidationError>Full structural validation with diagnostics
validate_permissive()Result<Vec<String>>Lenient validation, returns warnings
is_single_image()boolnz == 1
is_image_stack()boolispg == 0
is_volume()boolNot a stack and not an image stack
is_volume_stack()boolispg in 401–630
set_image_stack()()Set as image stack (ispg = 0, mz = 1)
set_volume()()Set as single volume (ispg = 1, mz = nz)
set_volume_stack(mz)()Set as volume stack with sub-volume size mz
logical_shape()[usize; 4][nvolumes, mz, ny, nx]
exttyp()[u8; 4]Extended header type from extra[8..12]
exttyp_str()Result<&str>Extended header type as string (UTF-8 decoded)
nversion()i32NVERSION from extra[12..16]
get_labels()Vec<String>Read up to nlabl non-empty labels
label_at(i)Option<&str>Trimmed label at index i, or None if empty
add_label(text)()Append a text label (FIFO when full)
density_stats()(f32, f32, f32, f32)(dmin, dmax, dmean, rms)
sampling()[i32; 3][mx, my, mz]
voxel_size()[f32; 3]Å/pixel = cella / mxyz
cell_lengths()[f32; 3][xlen, ylen, zlen]
cell_angles()[f32; 3][alpha, beta, gamma]
cell_volume()f64Unit cell volume in ų (triclinic formula)
nstart()[i32; 3][nxstart, nystart, nzstart]
detect_endian()FileEndianDetect byte order from MACHST
set_file_endian(endian)()Set MACHST and re-encode NVERSION
is_standard_map()boolMAP field is exactly "MAP "
detect_imod()Option<ImodInfo>Detect IMOD stamp in extra bytes
is_y_inverted()booltrue when mapr == -2 (IMOD convention)
decode_from_bytes(bytes)HeaderParse from raw 1024 bytes (auto endian)
decode_from_bytes_with_info(bytes)(Header, Option<EndianFallbackWarning>)Parse with endian fallback diagnostics
encode_to_bytes(&mut [u8; 1024])()Encode to raw bytes

§Manual header parsing

Decode a raw 1024-byte header block with automatic endianness detection:

use mrc::Header;
let raw = [0u8; 1024];
// ... fill raw bytes from file ...
let header = Header::decode_from_bytes(&raw);

When the MACHST byte-order stamp is wrong (common in some EPU files), the decoder tries the opposite endianness automatically:

let (header, warning) = Header::decode_from_bytes_with_info(&raw);
if let Some(w) = warning {
    eprintln!("byte-order fallback used: {w}");
}

§Extended headers

Many MRC files carry additional metadata after the 1024-byte fixed header in an extended header region. The type is identified by the 4-byte exttyp field in the header’s extra[8..12].

The ExtHeaderType enum identifies the format without parsing:

use mrc::{Header, ExtHeaderType};
let header = Header::new();
match ExtHeaderType::from_header(&header) {
    ExtHeaderType::Fei1 => println!("FEI Type 1"),
    ExtHeaderType::Ccp4 => println!("CCP4"),
    ExtHeaderType::Unknown(id) => {
        println!("Unknown: {:?}", std::str::from_utf8(&id));
    }
    _ => {}
}

Instead of calling individual parser functions, use the auto-dispatch method on any open reader:

use mrc::ExtHeaderData;

match reader.parse_extended_header() {
    ExtHeaderData::Fei1(records) => {
        println!("FEI1 tilt series ({} records)", records.len());
        for r in &records {
            println!("  tilt {:.1}°, defocus {:.1} µm",
                r.alpha_tilt, r.defocus);
        }
    }
    ExtHeaderData::Ccp4(records) => {
        println!("CCP4 symmetry ({} records)", records.len());
    }
    ExtHeaderData::Seri(records) => {
        println!("  first tilt: {:.1}°", records[0].alpha_tilt);
    }
    ExtHeaderData::None => println!("No recognized extended header"),
    _ => {}
}

Typed convenience methods give direct access without pattern matching:

if let Some(records) = reader.fei1_metadata() {
    println!("{} FEI1 records", records.len());
}
if let Some(imod) = reader.imod_metadata() {
    println!("IMOD type {:?}, tilt increment {:.1}°",
        imod.image_type, imod.tilt_increment);
}

Available: fei1_metadata, fei2_metadata, ccp4_records, mrco_records, seri_records, agar_records, imod_metadata.

§Error handling

Fallible functions return Result<T, Error>. Match on specific variants to handle different failure modes:

use mrc::Error;

fn describe(err: &Error) -> &str {
    match err {
        Error::Io(_) => "I/O failure",
        Error::InvalidHeader => "not a valid MRC file",
        Error::ModeMismatch { .. } => "wrong voxel type for this file",
        Error::BoundsError { .. } => "block outside volume",
        Error::FileSizeMismatch { .. } => "file truncated or has trailing data",
        _ => "other",
    }
}

The errors you will actually hit in practice:

HeaderValidationError gives fine-grained diagnostics for header problems (bad dimensions, wrong MAP field, invalid NVERSION …).

§Endianness

MRC files encode byte order via a 4-byte MACHST stamp at file offset 212. FileEndian represents the detected byte order:

use mrc::FileEndian;
let stamp: [u8; 4] = [0x44, 0x44, 0x00, 0x00]; // little-endian
assert_eq!(FileEndian::from_machst(&stamp), FileEndian::LittleEndian);

Use FileEndian::native to query the host platform, and reader.endian() to get a file’s actual byte order. New files are always little-endian, matching modern hardware and the Python mrcfile library.

The crate has a fallback: if the MODE field is invalid under the detected endianness, the opposite byte order is tried. This handles files with a wrong MACHST stamp but correct data.

§Compression auto-detection

Reader::open reads the first two bytes of the file:

Magic bytesFormat
\x1f\x8bGzip
BZBzip2
anything elsePlain

Plain MRC files are memory-mapped or buffered directly. Compressed files are fully decompressed into memory on open, with a hard cap of DEFAULT_MAX_DECOMPRESSED_BYTES (256 GiB) to prevent bombs. Use Reader::open_gzip_with_limit or [Reader::open_bzip2_with_limit] for a custom limit.

Large compressed files: If the uncompressed data exceeds available RAM, decompress with gunzip or bunzip2 first, then use Reader::open for zero-copy access — the OS pages data on demand without loading the whole file into memory.

§File validation

validate_full runs comprehensive checks on a file — header, size, endianness, data statistics (1% tolerance), and NaN / Inf scanning. Returns a ValidationReport with categorized issues:

use mrc::validate::{validate_full, Severity};

let report = validate_full("protein.mrc", false)?;
if !report.is_valid() {
    for issue in &report.issues {
        if issue.severity == Severity::Error {
            eprintln!("[{}] {}", issue.category, issue.message);
        }
    }
}

If you already have an open Reader, use validate_reader to avoid re-opening the file.

§Real-world workflows

§1. Process a tilt series

A common cryo-EM workflow: open a tilt series, read the FEI metadata, then iterate over slices:

use mrc::open;

let reader = open("tiltseries.mrc")?;
println!("{}×{}×{} voxels, mode {:?}",
    reader.shape().nx, reader.shape().ny, reader.shape().nz,
    reader.mode());

// Read FEI extended header metadata via convenience method
if let Some(records) = reader.fei1_metadata() {
    for (i, r) in records.iter().enumerate() {
        println!("tilt {i}: α={:.1}°, defocus={:.1} µm",
            r.alpha_tilt, r.defocus);
    }
}

// Process each slice
for slice in reader.convert::<f32>().slices() {
    let block = slice?;
    // block.data: Vec<f32> — ready for filtering, CTF correction, etc.
}

If a file fails to open, try open_permissive for lenient header handling, or validate_full to diagnose the issue.

§2. Write a processed map

Always call finalize — without it the header is stale and density statistics will be wrong (tools display wrong contrast).

use mrc::create;

let mut writer = create("reconstructed.mrc")
    .shape([512, 512, 256])
    .mode::<f32>()
    .finish()?;

for z in 0..256 {
    let slice = vec![0.0f32; 512 * 512];
    writer.write_block(&mrc::VoxelBlock::new(
        [0, 0, z], [512, 512, 1], slice,
    )?)?;
}

writer.update_header_stats()?;
writer.finalize()?;

§3. Read subtomogram averages from a volume stack

Volume stacks (ISPG 401–630) pack multiple sub-volumes into one file, each mz slices thick. Use volumes to iterate:

for volume in reader.volumes()? {
    let vol = volume?;
    println!("sub-volume at z={} ({}×{}×{} voxels)",
        vol.offset()[2], vol.shape()[0], vol.shape()[1], vol.shape()[2]);
}

§Troubleshooting

ErrorLikely causeWhat to try
InvalidHeaderNot an MRC file, or header corruptionRun mrc validate file.mrc; try open_permissive
FileSizeMismatchFile truncated or has trailing garbageRe-download or check mrc validate output
ModeMismatchWriting a VoxelBlock<i16> to an Float32 fileUse write_block_as — auto-converts any mode
NotAVolumeStackCalling volumes() on a non-stack fileCheck reader.is_volume_stack() first
BoundsErrorBlock outside volumeCheck offset + shape against dimensions
BlockShapeMismatchData length doesn’t match block shapeVerify sx * sy * sz * sizeof(T) matches data length
UnsupportedModeUnrecognized mode, or mode needs the f16 featureEnable f16 feature or convert with another tool
Io errorFile permissions, filesystem issueCheck the file path and permissions
Values look wrongEndianness mismatchThe endianness fallback handles most cases; try mrc validate

§Philosophy

This crate does one thing — read and write MRC files. It does no array arithmetic, image processing, or type conversion beyond MRC-specific shortcuts (convert::<f32>(), slices_mode0, slices_u8). Leave those to crates like ndarray, or your own code.

Modules§

validate
MRC file validation infrastructure.

Structs§

AgarRecord
Agard extended header record.
Ccp4Record
A single CCP4 symmetry record — an 80-character text line containing space group symmetry operators.
ConvertReader
Auto-conversion wrapper returned by Reader::convert. A reader wrapper that auto-converts all voxel data to type T.
Fei1Metadata
Common FEI1 metadata fields.
Fei2Metadata
FEI2 metadata extends FEI1 with additional v2 fields.
Float32Complex
A complex number with 32-bit float real and imaginary components.
Header
Mirror of the 1024-byte MRC-2014 fixed header.
HeaderBuilder
Builder for constructing validated MRC headers.
ImodInfo
IMOD-specific metadata parsed from the extra block (bytes 56-63).
ImodMetadata
IMOD-specific metadata parsed from the main header’s extra bytes.
Int16Complex
A complex number with 16-bit signed integer real and imaginary components.
MrcoRecord
A legacy MRCO extended header record.
Reader
Consolidated MRC reader with automatic mmap/buffered backend selection. MRC file reader with automatic backend selection.
SeriRecord
SerialEM extended header record.
VolumeShape
Volume geometry in voxels.
VoxelBlock
A contiguous chunk of voxel data with a 3D offset and shape.
Writer
MRC file writer and its builder. MRC file writer using standard file I/O.
WriterBuilder
MRC file writer and its builder. Builder for configuring and creating a new MRC file writer.
f16
Half-precision floating point type (requires f16 feature). A 16-bit floating point type implementing the IEEE 754-2008 standard binary16 a.k.a “half” format.

Enums§

ComplexToRealStrategy
Strategy for converting complex numbers to real values.
CompressionLevel
Compression level for compressed MRC writers.
DataBlock
A block of voxel data with a 3D offset and shape, returned by the default (non-convert) reader methods.
DataView
Borrowed typed slice into an MRC volume’s raw data.
Error
The top-level error type for MRC I/O operations.
ExtHeaderData
Parsed extended header data, dispatched by ExtHeaderType.
ExtHeaderType
Known extended header types identified by the 4-byte EXTTYP field.
FileEndian
Endianness of MRC file data. Endianness of MRC file data.
HeaderValidationError
Errors that can occur during detailed header validation.
ImodImageType
IMOD image type classification from the idtype field.
M0Interpretation
Interpretation of Mode 0 (8-bit) data for legacy files.
Mode
MRC data mode defining the on-disk representation of voxel values.
OwnedData
Owned typed data — returned when a copy is unavoidable (sub-block scatter/gather, endian mismatch).

Constants§

AGAR_RECORD_SIZE
Size of a single AGAR record, in bytes.
CCP4_RECORD_SIZE
Size of a single CCP4 symmetry record, in bytes.
DEFAULT_MAX_DECOMPRESSED_BYTES
Default decompression safety limit for gzip/bzip2 files (256 GiB).
FEI1_RECORD_SIZE
Size of a single FEI1 metadata record, in bytes.
FEI2_RECORD_SIZE
Size of a single FEI2 metadata record, in bytes.
MRCO_RECORD_SIZE
Size of a single MRCO record, in bytes.
SERI_RECORD_SIZE
Size of a single SERI (SerialEM) record, in bytes.

Traits§

Voxel
Trait for MRC voxel types with compile-time mode tracking.

Functions§

convert_u8_slice_to_u16
Widen a u8 slice to u16 for writing as Mode 6 (Uint16).
convert_u16_slice_to_u8
Narrow a u16 slice to u8, returning Err if any value exceeds 255.
create
Create a new MRC file for writing.
open
Open an MRC file for reading, auto-detecting gzip or bzip2 compression.
parse_agar_records
Parse extended header bytes as typed records.
parse_ccp4_records
Parse extended header bytes as typed records.
parse_fei1_records
Parse a raw extended header byte slice as a vector of FEI1 records.
parse_fei2_records
Parse a raw extended header byte slice as a vector of FEI2 records.
parse_imod_metadata
Parse IMOD metadata from the main header’s extra bytes.
parse_mrco_records
Parse extended header bytes as typed records.
parse_seri_records
Parse extended header bytes as typed records.
read_as
Read an entire MRC volume into a Vec<T> with auto-mode detection.
reinterpret_m0
Reinterpret Mode 0 (8-bit) data as signed or unsigned and convert to f32.
write_as
Write an entire MRC volume from a &[T] with a single call.