Source code for monalisa.io._mat73

"""Minimal MATLAB v7.3 (HDF5) MAT-file reader and writer for numeric arrays.

Writes files that MATLAB ``load`` reads (512-byte userblock with the MAT
header, ``MATLAB_class`` attributes, dimensions reversed, complex as a
``real``/``imag`` compound) and reads numeric/logical arrays written by MATLAB.
"""

from __future__ import annotations

import datetime as dt
import os
from pathlib import Path

import h5py
import numpy as np

_CLASS = {
    np.dtype(np.float64): "double", np.dtype(np.float32): "single",
    np.dtype(np.complex128): "double", np.dtype(np.complex64): "single",
    np.dtype(np.int8): "int8", np.dtype(np.int16): "int16", np.dtype(np.int32): "int32",
    np.dtype(np.int64): "int64", np.dtype(np.uint8): "uint8", np.dtype(np.uint16): "uint16",
    np.dtype(np.uint32): "uint32", np.dtype(np.uint64): "uint64", np.dtype(np.bool_): "logical",
}
_DTYPE = {"double": np.float64, "single": np.float32, "logical": np.bool_, "int8": np.int8,
          "int16": np.int16, "int32": np.int32, "int64": np.int64, "uint8": np.uint8,
          "uint16": np.uint16, "uint32": np.uint32, "uint64": np.uint64}


def _header() -> bytes:
    now = dt.datetime.now().strftime("%a %b %d %H:%M:%S %Y")
    text = f"MATLAB 7.3 MAT-file, Platform: GLNXA64, Created on: {now} HDF5 schema 1.00 ."
    return text.encode("ascii").ljust(116, b" ") + b"\x00" * 8 + b"\x00\x02" + b"IM"


[docs] def savemat73(path, variables: dict) -> None: """Save numeric/logical arrays as a MATLAB v7.3 MAT-file (MATLAB shapes kept). The file is written next to ``path`` under a temporary name and renamed when complete, so that an interrupted run (killed kernel, full disk) never leaves a truncated file at ``path``.""" path = Path(path) tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp") try: _write(tmp, variables) os.replace(tmp, path) finally: if tmp.exists(): tmp.unlink()
def _write(path, variables: dict) -> None: with h5py.File(path, "w", userblock_size=512) as f: for name, value in variables.items(): a = np.asarray(value) if a.ndim < 2: a = a.reshape(1, -1) if a.ndim == 1 else a.reshape(1, 1) cls = _CLASS[a.dtype] data = np.ascontiguousarray(a.T) # HDF5 stores MATLAB arrays with reversed dims if np.iscomplexobj(a): comp = np.dtype([("real", data.real.dtype), ("imag", data.real.dtype)]) buf = np.empty(data.shape, dtype=comp) buf["real"], buf["imag"] = data.real, data.imag data = buf elif cls == "logical": data = data.astype(np.uint8) ds = f.create_dataset(name, data=data) ds.attrs["MATLAB_class"] = np.bytes_(cls) if cls == "logical": ds.attrs["MATLAB_int_decode"] = np.int32(1) with open(path, "r+b") as fh: fh.write(_header()) def _read(f, ds): cls = ds.attrs.get("MATLAB_class", b"double") cls = cls.decode() if isinstance(cls, bytes) else str(cls) if "MATLAB_empty" in ds.attrs and ds.attrs["MATLAB_empty"]: return np.zeros((0, 0), dtype=_DTYPE.get(cls, np.float64)) data = ds[()] if cls == "cell": refs = np.asarray(data).T out = np.empty(refs.shape, dtype=object) for idx in np.ndindex(refs.shape): out[idx] = _read(f, f[refs[idx]]) return out if data.dtype.names and "real" in data.dtype.names: data = data["real"] + 1j * data["imag"] elif cls in _DTYPE: data = data.astype(_DTYPE[cls], copy=False) return np.asarray(data).T def _not_hdf5(path) -> Exception: """Error for a file that h5py cannot open: an older MAT-file, or an incomplete file.""" size = os.path.getsize(path) with open(path, "rb") as fh: head = fh.read(128) if size == 0: why = "the file is empty" elif head.startswith(b"MATLAB") and not head.startswith(b"MATLAB 7.3"): return None # a MAT-file of version 7 or older: read with scipy elif head.startswith(b"MATLAB 7.3"): why = f"the file is incomplete ({size} bytes): it was probably cut off while being written or copied" else: why = f"it is not a MAT-file ({size} bytes, starts with {head[:16]!r})" return OSError(f"loadmat73: cannot read {path}: {why}. Write it again (re-run the step that " f"saves it), or copy it again completely.")
[docs] def loadmat73(path, variable_names=None) -> dict: """Numeric, logical and cell variables of a MATLAB v7.3 MAT-file, with MATLAB shapes. Older MAT-files (MATLAB ``save`` without ``-v7.3``) are read with ``scipy.io.loadmat``.""" if not h5py.is_hdf5(path): err = _not_hdf5(path) if err is not None: raise err import scipy.io as sio data = sio.loadmat(path, variable_names=variable_names) return {k: v for k, v in data.items() if not k.startswith("__")} out = {} with h5py.File(path, "r") as f: for name, ds in f.items(): if not isinstance(ds, h5py.Dataset) or name.startswith("#"): continue if variable_names is not None and name not in variable_names: continue out[name] = _read(f, ds) return out