Skip to content

visualdynamics.io.matlab

matlab

MATLAB .mat files: the project file in MATLAB's container.

A .mat written here is a .vdyn respelled — the same schema, the same names, the same values (docs/vdyn-format.md) — so a MATLAB user gets load('modal.mat') and structs with the right shapes and complex numbers assembled, rather than HDF5 datasets to walk with h5read (which a .vdyn already allows). Everything a project holds has a MATLAB form, so the round trip is lossless and the reader takes its own files back exactly.

There is one implementation of the layout, and it is not here. The writer runs the native .vdyn writer into an HDF5 file held in memory and carries that tree into .mat variables; the reader rebuilds an in-memory HDF5 tree from the .mat and hands it to the native loader. A field the schema grows tomorrow travels tomorrow with no change here, and the two files cannot drift apart because neither this module nor native.py knows two layouts (principle 9). The price is one in-memory HDF5 round trip per file, which is cheap.

Three things MATLAB spells differently from HDF5, and the rules for them, applied mechanically both ways:

  • Ragged arrays — the <name>_flat / <name>_offsets pairs (traceline and element connectivity) — are one cell array <name>, one row vector per entry, which is how MATLAB holds a ragged list.
  • Numbered groups — the project's objects/0000, 0001, … and a sine specification's tones/0, 1, … — are a cell array of structs in the same order (p.objects{3}.name); each object struct keeps the name and kind its group's attributes carried.
  • Vectors are columns. A one-dimensional dataset is written N×1, MATLAB's own vector orientation, so it reads back one-dimensional; a genuinely two-dimensional array (ordinate, records × samples) stays as it is even with one row. MATLAB has no one-dimensional array, so a matrix that happens to have one column (a sine tone's amplitude over one channel) would read back as a vector; a struct holding one carries a matrix_fields cellstr naming it, written only then, and read if present.

HDF5 attributes and datasets both become plain fields — frf.function_type beside frf.ordinate — because a MATLAB struct has no second kind of member and a hidden marker would be clutter. On the way back the native loader asks for each by the kind it expects, so the rebuild writes every field as a dataset and, where it is small enough to be one, as an attribute too; the loader reads the one it wants and the other is never looked at. Strings are char arrays, lists of strings are cellstr, and '' (an undeclared unit) is MATLAB's empty char.

Values are SI, exactly as in a .vdyn, with every record's unit named beside it — the one other format that stores SI regardless of the display system is ADF, whose definition requires it; here it is what keeps the file the project file. A record whose units were never declared is written as it stands, marked '', and comes back undeclared. Convert in MATLAB from the unit strings if a display system is wanted.

The stamp visualdynamics_schema travels as a variable, and the reader keeps the .vdyn contract: a newer stamp is refused, an older one always loads. A .mat with no stamp is accepted in one case only — a single struct in the documented layout of one object (a data array with abscissa, ordinate and data_class, a geometry with node_xyz, a shape set with shape_matrix), which is a user's own struct built to the page — and refused by name otherwise: reading a stranger's workspace would be guessing.

.mat version 5 (what save -v7 writes, and what scipy reads and writes) holds at most 4 GB per variable. A record larger than that is refused before anything is written, naming the array; version 7.3 files are HDF5 and are not read or written here yet.

Functions:

Name Description
handles

Every object the project file holds, and a whole project.

sniff

A .mat by suffix; whether it is ours is load's to say, by

save

Write an object, or a whole project, as a .mat file.

load

Read a .mat written here (or one struct built to the layout):

Functions:

handles

handles(obj: Any) -> bool

Every object the project file holds, and a whole project.

Source code in src/visualdynamics/io/matlab.py
def handles(obj: Any) -> bool:
    """Every object the project file holds, and a whole project."""
    from ..project import Project

    if isinstance(obj, Project):
        return True
    try:
        native._saver_for(obj)
    except TypeError:
        return False
    return True

sniff

sniff(path: str | PathLike) -> bool

A .mat by suffix; whether it is ours is load's to say, by name, since a wrong-format error beats "no importer recognises".

Source code in src/visualdynamics/io/matlab.py
def sniff(path: str | os.PathLike) -> bool:
    """A `.mat` by suffix; whether it is ours is `load`'s to say, by
    name, since a wrong-format error beats "no importer recognises"."""
    return str(path).endswith(SUFFIX)

save

save(obj: Any, path: str | PathLike, unit_system: Any = None, **_ignored: Any) -> None

Write an object, or a whole project, as a .mat file.

unit_system is taken for the exporter's uniform signature and unused: the file stores SI with the units named, like the project file it is (see the module docstring).

Source code in src/visualdynamics/io/matlab.py
def save(obj: Any, path: str | os.PathLike, unit_system: Any = None,
         **_ignored: Any) -> None:
    """Write an object, or a whole project, as a `.mat` file.

    `unit_system` is taken for the exporter's uniform signature and
    unused: the file stores SI with the units named, like the project
    file it is (see the module docstring).
    """
    from scipy.io import savemat

    from ..project import Project

    with _memory_file() as f:
        if isinstance(obj, Project):
            native.save_test_into(f, obj.name, dict(obj),
                                  active_geometry=obj.active_geometry,
                                  project_type=obj.project_type,
                                  links=obj.links, provenance=obj.provenance)
        else:
            native.save_into(obj, f)
        tree = _tree_out(f)
    _check_size(tree)
    path = str(path)
    if not path.endswith(SUFFIX):
        path += SUFFIX
    savemat(path, tree, do_compression=True, long_field_names=True,
            oned_as='column')

load

load(path: str | PathLike, **_ignored: Any) -> Any

Read a .mat written here (or one struct built to the layout): the object it holds, or a whole project.

Source code in src/visualdynamics/io/matlab.py
def load(path: str | os.PathLike, **_ignored: Any) -> Any:
    """Read a `.mat` written here (or one struct built to the layout):
    the object it holds, or a whole project."""
    from scipy.io import loadmat

    path = str(path)
    try:
        raw = loadmat(path, squeeze_me=False, struct_as_record=False,
                      chars_as_strings=True)
    except NotImplementedError:
        raise ValueError(
            f'{path} is a MATLAB version 7.3 file, which is HDF5; save it '
            "with '-v7', or save the data as .vdyn") from None
    variables = {name: _value_in(value) for name, value in raw.items()
                 if not name.startswith('__')}
    if STAMP not in variables:
        bare = _bare_kind(variables)
        if bare is None:
            found = ', '.join(sorted(variables)) or 'nothing'
            raise ValueError(
                f'{path} is not a Visual Dynamics file (no {STAMP} '
                f'variable; it holds {found}). A struct built to the '
                'documented layout of one object is read too.')
        kind, fields = bare
        variables = {STAMP: native.SCHEMA_VERSION, kind: fields}
    with _memory_file() as f:
        _write_tree(f, variables)
        return native.load_from(f, path=path)