# MATLAB v7.3 (.mat) Files
A MATLAB v7.3 `.mat` file is an HDF5 file dressed in MATLAB conventions: a 512-byte userblock carrying the `MATLAB 7.3 MAT-file` signature, a `MATLAB_class` attribute on every dataset and group, column-major 2-D arrays, and UTF-16 strings. This page covers the high-level serde path that writes and reads `.mat` files from ordinary Rust structs, the supported type mapping, MATLAB cell arrays, and the lower-level conventions for hand-built files.
!!! tip "Runnable example"
The [`matlab_fixtures`](https://github.com/stephenberry/hdf5-pure/blob/main/examples/matlab_fixtures.rs) example writes a directory of `.mat` v7.3 fixtures (scalars, vectors, matrices, strings, nested structs, complex data, cell arrays, and edge shapes) for verification in MATLAB and Octave. Run it with:
```bash
cargo run --example matlab_fixtures --features serde
```
## Requires the `serde` feature
The high-level `.mat` API is gated on the `serde` feature, which is off by default. The `mat::Matrix`, `mat::Complex32`, `mat::Complex64`, and `mat::MatElement` items, along with `mat::to_file` / `mat::from_file`, are only available when it is enabled. See the [features reference](../reference/features.md) for the full list.
```toml
[dependencies]
hdf5-pure = { version = "0.14", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
```
## Serializing a struct to `.mat`
Any type deriving `serde::Serialize` / `Deserialize` round-trips through `mat::to_file` and `mat::from_file`. The top-level value must be a struct with named fields (or a `HashMap<String, _>`); each field becomes a top-level MATLAB variable.
```rust
use hdf5_pure::mat::{self, Complex64, Matrix};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Experiment {
name: String,
trial: u32,
samples: Vec<f64>,
data: Matrix<f64>,
waveform: Vec<Complex64>,
config: Config,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Config { threshold: f64, tag: String }
let e = Experiment {
name: "run1".into(), trial: 3,
samples: vec![1.0, 2.0, 3.0],
data: Matrix::from_row_major(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]),
waveform: vec![Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)],
config: Config { threshold: 0.5, tag: "prod".into() },
};
mat::to_file(&e, "experiment.mat").unwrap();
let back: Experiment = mat::from_file("experiment.mat").unwrap();
assert_eq!(back, e);
```
To work with bytes instead of the filesystem, use `mat::to_bytes` and `mat::from_bytes`, which take and return a `Vec<u8>` / `&[u8]`. Both are equally subject to the `serde` feature gate.
## Type mapping
The serializer maps Rust types to HDF5 datasets and the MATLAB classes MATLAB expects on read:
| Rust | HDF5 / MATLAB encoding |
|---|---|
| `f64`, `f32`, `i*`, `u*` | scalar dataset `[1,1]`, `MATLAB_class = "double"` / `"single"` / `"int*"` / `"uint*"` |
| `bool` | `uint8` scalar, `MATLAB_class = "logical"` |
| `String` / `&str` | `uint16` `[1, N]` UTF-16LE, `MATLAB_class = "char"` |
| `Vec<T>` of numeric `T` | `[1, N]` row vector |
| `Matrix<T>` or `Vec<Vec<T>>` of same length | column-major 2-D dataset, HDF5 shape `[cols, rows]` |
| `Complex32` / `Complex64` | compound `{real, imag}` dataset |
| nested struct | HDF5 group with `MATLAB_class = "struct"`, `MATLAB_fields` |
| `Option<T>` (struct field) | omitted if `None` |
| unit enum variant | UTF-16 char dataset holding the variant name |
| `Vec<Struct>` / `Vec<Option<T>>` / ragged `Vec<Vec<T>>` | cell array (`MATLAB_class = "cell"`, object references into `#refs#`); `None` slots become `struct([])` |
### Matrices and the column-major convention
Rust is row-major; MATLAB is column-major. The `mat::Matrix<T>` newtype carries the Rust-side `rows`/`cols` and a row-major `data` vector, and the serializer transposes to column-major byte order and stores the HDF5 dataset with shape `[cols, rows]` so MATLAB sees the intended `rows × cols` matrix. Build one with `Matrix::from_row_major(rows, cols, data)` (it panics if `data.len() != rows * cols`) or `Matrix::zeros(rows, cols)`; read the parts back with `rows()`, `cols()`, `data()`, and `into_data()`.
```rust
use hdf5_pure::mat::{self, Matrix};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Frame { a: Matrix<f64> }
let v = Frame {
a: Matrix::from_row_major(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]),
};
mat::to_file(&v, "matrix.mat").unwrap();
```
A bare `Vec<Vec<T>>` whose rows all share a length is also recognized as a 2-D matrix, but `Matrix` is the unambiguous API. The element type `T` is bounded by the sealed `mat::MatElement` trait, which is implemented for `f32`/`f64`, the 8/16/32/64-bit signed and unsigned integers, `bool`, `Complex32`, and `Complex64`. The trait is sealed because MAT v7.3 admits only this fixed set of numeric classes; you cannot implement it for other types.
### Complex numbers
`Complex32` and `Complex64` are compound `{real, imag}` newtypes constructed with `Complex64::new(re, im)` (or the `re` / `im` fields directly). A bare value becomes a compound scalar `[1, 1]`; a `Vec<Complex64>` becomes a compound dataset `[1, N]`. The on-disk layout is the same `{real, imag}` compound MATLAB uses for complex arrays. For a deeper treatment of HDF5 compound datasets see the [compound types guide](../guide/compound-types.md).
## Cell arrays
A sequence whose elements do not unify into a single numeric matrix lowers to a MATLAB cell array rather than erroring. Each element is interned under the conventional `#refs#` group, and the parent dataset stores HDF5 object references with `MATLAB_class = "cell"`. This covers `Vec<Struct>`, `Vec<Option<T>>` with interspersed `None`, nested cells of cells, and ragged `Vec<Vec<T>>`. An `Option::None` slot inside a sequence becomes `struct([])` so every cell slot has a defined MATLAB type.
```rust
use hdf5_pure::mat;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Point { x: f64, y: f64 }
#[derive(Serialize, Deserialize)]
struct Capture {
/// 3x1 cell array of struct.
path: Vec<Point>,
/// 3x1 cell array; the `None` slot becomes `struct([])`.
optionals: Vec<Option<Point>>,
/// Outer 2x1 cell of cells; rows-of-variable-length-records shape.
grid: Vec<Vec<Option<Point>>>,
/// Ragged numerics also fall back to cell rather than erroring.
ragged: Vec<Vec<f64>>,
}
```
In MATLAB this loads as `iscell(path) == true`, with elements addressed as `path{1}.x`, and so on. Empty `None` slots load as `struct([])` (`isempty(fieldnames(...))`).
!!! note "Reader compatibility"
Cell arrays load correctly in MATLAB, libmatio (the reference C library), Julia's `MAT.jl`, and Python via `pymatreader` / `hdf5storage`. GNU Octave 11's `load` does not yet follow object references for v7.3 cells (it warns "unknown datatype"); load such files with one of the other tools instead.
## Struct arrays (reading)
A struct array authored in MATLAB (`s(1).x = …; s(2).x = …`) is stored as a `MATLAB_class = "struct"` group whose every field is a dataset of per-element object references — a struct-of-arrays. `mat::from_file` / `mat::from_bytes` transpose that into an array-of-structs: a `1×N` / `N×1` array reads into `Vec<T>`, and a true `M×N` array into `Vec<Vec<T>>`, where `T` is your own struct. A scalar struct still reads as a single struct.
!!! note "Write/read asymmetry"
This is a read-only path. Writing a `Vec<Struct>` from Rust produces a MATLAB **cell array** (see [Cell arrays](#cell-arrays)), not a native struct array, so a `.mat` you write and one MATLAB writes from the same Rust type differ on disk. Both read back into `Vec<Struct>`.
## Opaque value classes
Reading (`from_bytes`) decodes the MCOS opaque value classes `datetime`, `duration`, and `categorical` into the public `MatDatetime`, `MatDuration`, and `MatCategorical` types (Unix-epoch millisecond instants, durations in milliseconds, and category codes plus names). Any other opaque class (`table`, `containers.Map`, `dictionary`, user `classdef`s, …) is surfaced losslessly as its raw property map, so it still deserializes into a matching struct; function handles and legacy objects are refused by name with `MatError::UnsupportedMatlabClass`.
## Not supported (writing)
Writing (`to_bytes`) does not encode non-unit enum variants, MATLAB `classdef` objects, or `datetime` / `duration` / `categorical` types. Unit enum variants are supported and serialize to a UTF-16 char dataset holding the variant name.
## Hand-built files (low-level conventions)
If you are not using serde, you can apply the MATLAB conventions yourself on top of `FileBuilder`. Two pieces matter: the userblock header and the `MATLAB_class` / `MATLAB_fields` attributes.
### Userblock header
MATLAB expects a 512-byte userblock beginning with the `MATLAB 7.3 MAT-file` signature. Reserve the block with `with_userblock(512)` and write the header bytes into the leading region after finishing:
```rust
use hdf5_pure::FileBuilder;
let mut builder = FileBuilder::new();
builder.with_userblock(512);
builder.create_dataset("data").with_f64_data(&[1.0]);
let mut bytes = builder.finish().unwrap();
// Write MATLAB header into userblock
bytes[126] = b'I';
bytes[127] = b'M';
```
### Struct pattern
A MATLAB struct is an HDF5 group carrying `MATLAB_class = "struct"` and a `MATLAB_fields` list naming its fields, with each field a child dataset that carries its own `MATLAB_class`. Use `AttrValue::AsciiString` for the fixed-length ASCII class names and `AttrValue::VarLenAsciiArray` for the variable-length field-name array:
```rust
use hdf5_pure::{FileBuilder, AttrValue};
let mut builder = FileBuilder::new();
let mut grp = builder.create_group("my_struct");
let mut fields = Vec::new();
for (name, data) in [("x", vec![1.0, 2.0]), ("y", vec![3.0, 4.0])] {
fields.push(name.to_string());
grp.create_dataset(name).with_f64_data(&data)
.set_attr("MATLAB_class", AttrValue::AsciiString("double".into()));
}
grp.set_attr("MATLAB_class", AttrValue::AsciiString("struct".into()));
grp.set_attr("MATLAB_fields", AttrValue::VarLenAsciiArray(fields));
builder.add_group(grp.finish());
```
See the [groups and attributes guide](../guide/groups-attributes.md) for more on the `AttrValue` variants used here.