Skip to content

Repository files navigation

mrc

RustLicense: MITCrates.ioDocs.rsCI

Type-safe MRC-2014 reader/writer for Rust — SIMD-accelerated, mmap-enabled, with full cryo-EM metadata support.

A type-safe Rust encoder/decoder for MRC files — the standard format in cryo-electron microscopy and structural biology. Automatically handles endianness, type conversion, and compression with SIMD acceleration, exposing a powerful yet intuitive and friendly read/write API so you can focus on your data.


Quick Start

One line to read any MRC file. One line to write one.

use mrc::{read_as, write_as};// Read — auto-detects gzip/bzip2, handles quirky headerslet(header, data):(_,Vec<f32>) = read_as("density.mrc")?;println!("{}×{}×{} = {} voxels",
header.nx, header.ny, header.nz, data.len());// Write — type-safe, single callwrite_as("output.mrc",&data,[512,512,256])?;

Power & Simplicity at a Glance

The mrc API is designed so that common operations are one-liners and complex workflows read naturally.

What you wantHow you write it
Open any MRC file (plain / gzip / bzip2)Reader::open("file.mrc")?
One-shot read (open + read_volume)let (h, d): (_, Vec<f32>) = read_as("file.mrc")?;
One-shot write (create + write + finalize)write_as("out.mrc", &data, [512, 512, 256])?;
Read the whole volume as f32reader.convert::<f32>().read_volume()?
Read a sub-regionreader.subregion([x, y, z], [sx, sy, sz])?
Iterate Z-slicesreader.slices()for slice in ...
Iterate sub-volumes in a stackreader.volumes()?for vol in ...
Create a new filecreate("out.mrc").shape([512, 512, 256]).mode::<f32>().finish()?
Write with auto-conversion (f32 → i16)writer.write_block_as(&f32_block)?
Parse tilt-series metadatareader.fei1_metadata() or reader.parse_extended_header()
Validate a filevalidate_full("file.mrc", false)?
Open a quirky fileReader::open_permissive("broken.mrc")?

No trait imports required. Every one of these is an inherent method — no use SomeTrait needed.

Installation

[dependencies]
mrc = "0.8"

Enable optional features in Cargo.toml:

mrc = { version = "0.7", features = ["ndarray", "serde", "bzip2"] }

For the mrc-cli binary, install the companion crate:

cargo install mrc-cli
FeatureDefaultWhat it adds
mmapMemory-mapped I/O (auto-selected for large files)
f16Half-precision float (half::f16) support
simdAVX2/NEON acceleration
parallelParallel decode/convert/encode via rayon — transparent for any block ≥512³
gzipGzip auto-detection and compressed writer
bzip2Bzip2 auto-detection and compressed writer
ndarrayReturn volumes as ndarray::Array3<T> via to_ndarray()
serdeSerialize/Deserialize for all public types

Quick Tour

See docs.rs/mrc for the full API documentation, runnable examples, and detailed guidance. The examples below are just a few highlights.

Reading — any file, any mode, any shape

use mrc::Reader;// Open — auto-detects compression and byte orderlet reader = Reader::open("tiltseries.mrc")?;println!("{}×{}×{} voxels, mode {:?}",
reader.shape().nx, reader.shape().ny, reader.shape().nz,
reader.mode());// Check the file's mode at runtime and dispatch accordingly:match reader.mode(){
mrc::Mode::Float32 => {/* process f32 slices */}
mrc::Mode::Int16 => {/* process i16 slices */}
mrc::Mode::Uint16 => {/* process u16 slices */}
mrc::Mode::Int8 => {/* process i8 slices */}
other => todo!("mode {other:?}"),}// Or just use slices() and match DataView:for slice in reader.slices(){let block = slice?;match block.data(){
mrc::DataView::Float32(data) => {/* process &[f32] */}
mrc::DataView::Int16(data) => {/* process &[i16] */}
_ => {}}}// Full volume in one calllet block = reader.read_volume()?;letDataView::Float32(data) = block.data()else{panic!("expected Float32")};println!("{} voxels", data.len());// Any sub-region by coordinatelet block = reader.subregion([10,10,5],[32,32,8])?;letDataView::Float32(patch) = block.data()else{panic!("expected Float32")};

Auto-conversion — read any MRC mode as f32

Don't care whether the file is Int8, Int16, Uint16, Float16, or even Packed4Bit? Use convert::<f32>() and the crate handles the rest — or match on reader.mode() to handle each type individually.

// Option A: auto-convert everything to f32 in one callfor slice in reader.convert::<f32>().slices(){let block: mrc::VoxelBlock<f32> = slice?;println!("slice {}: mean = {:.2}",
block.offset[2],
block.data.iter().sum::<f32>() / block.data.len()asf32);}// Option B: use default reader methods and match on DataViewmatch reader.mode(){
mrc::Mode::Int16 => {for slice in reader.slices(){let block = slice?;letDataView::Int16(data) = block.data()else{panic!("expected Int16")};println!("i16 slice, min={}", data.iter().min().unwrap());}}
mrc::Mode::Float32 => {let block = reader.read_volume()?;letDataView::Float32(data) = block.data()else{panic!("expected Float32")};println!("f32 volume: {} voxels", data.len());}
_ => {/* handle other modes */}}// Or read the whole converted volume in one calllet block = reader.convert::<f32>().read_volume()?;// The same converter also supports slabs, tiles, subregion,// with_complex_strategy, with_m0_interpretation, and to_ndarray().

Writing — type-safe, flexible, fast

use mrc::create;// Create a Float32 fileletmut writer = create("output.mrc").shape([512,512,256]).mode::<f32>().finish()?;// Write one slice at a timefor z in0..256{let block = mrc::DataBlock::Owned{offset:[0,0, z],shape:[512,512,1],data: mrc::OwnedData::Float32(vec![0.0f32;512*512]),};
writer.write_data_block(&block)?;}// Or write with auto-conversion from any supported type
writer.write_block_as(&mrc::VoxelBlock::new([0,0,0],[512,512,1],vec![0.0f32;512*512],)?)?;// Parallel encoding is auto-selected for full XY slabs on file-backed writers
writer.update_header_stats()?;// fills dmin/dmax/dmean/rms
writer.finalize()?;// **required** — rewrites header

Writing compressed files

use mrc::{create,CompressionLevel};// Gzip-compressed output — same API, just finish_gzip()letmut writer = create("output.mrc.gz").shape([256,256,128]).mode::<f32>().compression(CompressionLevel::Best).finish_gzip()?;let block = mrc::DataBlock::Owned{offset:[0,0,0],shape:[256,256,128],data: mrc::OwnedData::Float32(vec![0.0f32;256*256*128]),};
writer.write_data_block(&block)?;
writer.finalize()?;// compresses & writes to disk

Memory-mapped I/O — zero-copy for large files

Files too large for RAM? Reader::open automatically uses memory-mapped I/O (requires mmap feature). The OS pages data on demand. The default reader methods return DataBlock views that borrow directly from the mapped memory.

let reader = Reader::open("huge_volume.mrc")?;// Default methods return DataBlock with zero-copy DataViewfor slice in reader.slices(){let block = slice?;letDataView::Float32(data) = block.data()else{continue;};println!("plane with {} voxels (zero-copy)", data.len());}

Reading Extended Metadata — one method call

use mrc::ExtHeaderData;// Auto-detect and parse whatever extended header the file hasmatch reader.parse_extended_header(){ExtHeaderData::Fei1(records) => {println!("FEI1 tilt series ({} images)", records.len());println!("first: tilt {:.1}°, defocus {:.1}µm",
records[0].alpha_tilt, records[0].defocus);}ExtHeaderData::Fei2(records) => {println!("FEI2 — {} records", records.len());}ExtHeaderData::Ccp4(records) => {println!("CCP4 symmetry — {} records", records.len());}ExtHeaderData::Seri(records) => {println!("SerialEM — first tilt {:.1}°", records[0].alpha_tilt);}ExtHeaderData::None => println!("No extended header"),
_ => {}}// Or use typed convenience methods directlyifletSome(records) = reader.fei1_metadata(){println!("{} FEI1 records", records.len());}ifletSome(imod) = reader.imod_metadata(){println!("IMOD: {:?}, tilt increment {:.1}°",
imod.image_type, imod.tilt_increment);}

Volume stacks — iterate sub-volumes

Volume stacks (ISPG 401–630) pack multiple sub-volumes in one file.

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

Validation — catch issues early

use mrc::{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);}}}

Working with quirky files

Common microscope quirks (NVERSION left at 0, "MAP\0" instead of "MAP ") are handled transparently by open(). For truly broken files, permissive mode turns non-critical errors into warnings:

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

Real-world workflow — the full pipeline

use mrc::{open, create,VoxelBlock};// 1. Open a tilt series from any microscope formatlet reader = open("tiltseries.mrc")?;println!("{}×{}×{}, mode {:?}",
reader.shape().nx, reader.shape().ny, reader.shape().nz,
reader.mode());// 2. Read FEI metadata (or CCP4, SerialEM, Agard...)ifletSome(records) = reader.fei1_metadata(){for(i, r)in records.iter().enumerate(){println!("tilt {i}: α={:.1}°, defocus={:.1} µm",
r.alpha_tilt, r.defocus);}}// 3. Process each slice as f32 (auto-converts from any mode)for slice in reader.convert::<f32>().slices(){let block = slice?;// block.data: Vec<f32> — ready for filtering, CTF, alignment}// 4. Write the reconstructed volumeletmut writer = create("reconstructed.mrc").shape([512,512,256]).mode::<f32>().finish()?;let block = mrc::DataBlock::Owned{offset:[0,0,0],shape:[512,512,256],data: mrc::OwnedData::Float32(processed_data),};
writer.write_data_block(&block)?;
writer.update_header_stats()?;
writer.finalize()?;

CLI Tools

The mrc-cli crate provides the mrc-cli command-line tool with subcommands for inspection, validation, conversion, PNG/GIF export, and resampling.

cargo install mrc-cli
mrc-cli info protein.mrc
mrc-cli header density.mrc
mrc-cli validate tiltseries.mrc
mrc-cli stats protein.mrc
mrc-cli invert input.mrc output.mrc
mrc-cli convert input.mrc output.mrc --mode i16
mrc-cli slice volume.mrc -z 42 -o slice.mrc
mrc-cli crop volume.mrc -o roi.mrc --x 100 --y 100 --z 50 -s 128,128,64
mrc-cli unstack tiltseries.mrc -o frame
mrc-cli rescale volume.mrc output.mrc --down 2
mrc-cli png volume.mrc -z 0 -o slice.png
mrc-cli movie volume.mrc -o movie.gif --pingpong

See the mrc-cli crate on crates.io for the full command reference and examples.

Further Reading

ResourceWhat you'll find
docs.rs/mrcComplete API reference with runnable examples on every method
APIs.mdLocal API surface overview (offline-friendly)
mrc-cli on crates.ioCLI binary reference and examples
roadmap.mdRelease history and planned features
AGENTS.mdCode organization & conventions for contributors
mrcfile-official.mdThe MRC-2014 specification
update.mdPer-release changelogs

Acknowledgments

  • CCP-EM for the MRC-2014 specification
  • EMDB for providing real-world test data
  • The cryo-EM community for invaluable feedback

Contributing

Contributions are welcome — whatever your skill level.

This crate is built by and for the cryo-EM community. Whether you're fixing a typo, adding a test, implementing a new feature, or just asking a question, your input makes the project better.

  • Report bugs — open an issue with steps to reproduce
  • Request features — what format feature or workflow is missing from your pipeline?
  • Submit PRs — see AGENTS.md for code organization and conventions
  • Improve docs — better examples, clearer explanations, fix typos
  • Share real files — MRC files with unusual extended headers or edge cases help us test
  • Adapt the test suite to your own data — the tests/real_data_tests.rs file is designed to be a template. Place any MRC files (from EMDB, EMPIAR, or your own microscope) into real_data/ and the tests exercise every API path against them. The more diverse the files, the more edge cases we catch.

All contributions are subject to the MIT License.


Format specs come and go, but cryo-EM data is forever — make yours readable by the next generation of tools.

MIT — see the LICENSE file.

About

High-performance MRC-2014 file format reader/writer for Rust and python, using in cryo-EM/ET

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages