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.

This crate handles file I/O, byte-order detection, and type-safe data access so you can focus on your science. It’s fast (SIMD, parallel encoding) and works with plain, gzip, and (optionally) bzip2 files.

See the README for installation instructions. The mrc-cli companion crate provides a mrc-cli binary with subcommands for inspection, validation, conversion, PNG/GIF export, and resampling — install it with cargo install mrc-cli.

§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 VoxelBlock<T> chunks:

  • 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

All iteration methods share the same API pattern:

// One slice at a time:
for slice in reader.slices::<f32>() {
    let block = slice?;
}
// Or batches of 16 Z-planes:
for slab in reader.slabs::<f32>(16) {
    let block = slab?;
}
// Or arbitrary 3D tiles:
for tile in reader.tiles::<f32>([64, 64, 64])? {
    let block = tile?;
}

Each block contains the offset, shape, and flat data: Vec<T>. Read the entire volume with read_volume:

let block: mrc::VoxelBlock<f32> = reader.read_volume::<f32>()?;
println!("shape: {:?}, voxels: {}", block.shape, block.data.len());

No trait imports needed: All iterator and conversion methods are available as inherent methods on Reader without any import.

For automatic mode conversion, use convert:

for slice in reader.convert::<f32>().slices() {
    let block = slice?;
    println!("slice {} mean density: {:.2}",
        block.offset[2],
        block.data.iter().sum::<f32>() / block.data.len() as f32);
}

Or read the full volume in one call:

let block = reader.convert::<f32>().read_volume()?;
println!("read {} voxels", block.data.len());

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 slab_as, OS-managed paging.

For buffered readers (in-memory buffers, compressed files), slab_as also provides zero-copy typed access under the same conditions (native endian, matching type).

§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.

  2. Optionally call update_header_stats to fill in dmin/dmax/dmean/rms.

  3. 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)Writer::from_writerMemory buffer, e.g. Cursor<Vec<u8>>
Writer (mmap)finish_mmap() / from_writer_mmapVery large files (mmap feature)
Writer (gzip)finish_gzip() / from_writer_gzipCompressed output (gzip feature)
Writer (bzip2)finish_bzip2() / from_writer_bzip2Compressed output (bzip2 feature)

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 encoding via rayon
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:

use mrc::Header;
let h = Header::new();
let vol = h.cell_volume();      // unit cell volume in ų
let (dmin, dmax, dmean, rms) = h.density_stats();
let sampling = h.sampling();    // [mx, my, mz]
let label = h.label_at(0);      // first label, or None
assert!(h.is_standard_map());   // MAP field is "MAP "

§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:

  • Io — the file could not be read or written
  • InvalidHeader — not a valid MRC file
  • ModeMismatch — calling slices::<f32>() on an Int16 file; use convert::<f32>() instead
  • BoundsError — read or write outside the volume
  • FileSizeMismatch — file truncated or has trailing garbage

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::<f32>()? {
    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
ModeMismatchUsing slices::<f32>() on an Int16 fileUse convert::<f32>() — auto-converts any mode
BoundsErrorBlock outside volumeCheck offset + shape against dimensions
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.
RegionIter
Lazy iterator over MRC voxel blocks. Lazy iterator over a volume as a sequence of VoxelBlocks.
SeriRecord
SerialEM extended header record.
SlabStepper
Stepping strategies for RegionIter. Step k contiguous Z-slices at a time ([nx, ny, k]).
SliceStepper
Stepping strategies for RegionIter. Step one Z-plane at a time ([nx, ny, 1]).
TileStepper
Stepping strategies for RegionIter. Step arbitrary 3D tiles across a volume.
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.
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.

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.