Skip to content

visualdynamics.io

io

File import and export.

Importers and exporters each register themselves in a small registry, so a new format is added without touching existing code.

Anything visualdynamics can read, it can write. .vdyn (HDF5) is the native format and the one Save and Load use — it is the only one that keeps everything, including units. The foreign formats are for getting data to other tools, and each loses whatever it has no way to record; see each module.

Functions:

Name Description
export_file

Write obj to a foreign format, chosen by name or by suffix.

register_exporter

Teach visualdynamics to write a format.

load

Load a .vdyn file: the object it contains, or a whole test.

save

Save a visualdynamics object to a .vdyn (HDF5) file.

save_test

Save a whole test — every named object — to one .vdyn file.

from_sep005

SEP 005 timeseries into TimeHistory objects.

register_importer

Teach visualdynamics a format. Registered ones are tried in order, so a

importers

Every format visualdynamics can read, in the order they are tried.

project_type_of

The kind of project a file says it is a run of, or None.

import_file

Import a foreign file, returning the visualdynamics object it contains.

Classes

Importer dataclass

Importer(name: str, description: str, sniff: Callable, load: Callable, project_type: Callable | None = None)

One format visualdynamics can read.

sniff(path) says whether this is that format — by content where the content says, never by the extension alone — and load(path) returns the object, the dict of objects, or the whole Project the file holds.

Functions:

export_file

export_file(obj: Any, path: str | PathLike, format: str | None = None, unit_system: UnitSystem | None = None, **kwargs: Any) -> None

Write obj to a foreign format, chosen by name or by suffix.

unit_system is the system to write in; without one the stored values go out as they are. Raises ValueError naming what the object can be written as, since "cannot export" is nearly always a question of which format.

Source code in src/visualdynamics/io/exporters.py
def export_file(obj: Any, path: str | os.PathLike, format: str | None = None,
                unit_system: UnitSystem | None = None,
                **kwargs: Any) -> None:
    """Write `obj` to a foreign format, chosen by name or by suffix.

    `unit_system` is the system to write in; without one the stored values
    go out as they are. Raises ValueError naming what the object *can* be
    written as, since "cannot export" is nearly always a question of which
    format.
    """
    # `~` is the writer's to expand too, or a file lands in a folder
    # named for a tilde (the import's own courtesy, 2026-09-20)
    path = os.path.expanduser(str(path))
    available = exporters(obj)
    if format is not None:
        for exporter in _EXPORTERS:
            if exporter.name == format:
                if not exporter.handles(obj):
                    raise ValueError(
                        f'{exporter.name} cannot write {type(obj).__name__}; '
                        f'it takes {[e.name for e in available]}')
                _refuse_modal(exporter, obj)
                return exporter.save(obj, path, unit_system=unit_system,
                                     **kwargs)
        raise ValueError(f'No exporter named {format!r}; '
                         f'available: {[e.name for e in _EXPORTERS]}')
    for exporter in available:
        if path.endswith(exporter.suffix):
            _refuse_modal(exporter, obj)
            return exporter.save(obj, path, unit_system=unit_system,
                                 **kwargs)
    raise ValueError(
        f'Nothing writes {type(obj).__name__} to {path}; it can be written '
        f'as {[(e.name, e.suffix) for e in available]}')

register_exporter

register_exporter(name: str, description: str, suffix: str, handles: Callable, save: Callable) -> None

Teach visualdynamics to write a format.

Source code in src/visualdynamics/io/exporters.py
def register_exporter(name: str, description: str, suffix: str,
                      handles: Callable, save: Callable) -> None:
    """Teach visualdynamics to write a format."""
    _EXPORTERS.append(Exporter(name, description, suffix, handles, save))

load

load(path: str | PathLike, progress: Callable[[int, int], None] | None = None) -> Any

Load a .vdyn file: the object it contains, or a whole test.

progress is called as (objects loaded, objects in the file) — once up front with 0 and once per object — because a project file is minutes of someone's day and the reader is the only thing that knows how far along it is. A single-object file reports nothing: one object is one step, and a bar with one step is a light bulb.

Source code in src/visualdynamics/io/native.py
def load(path: str | os.PathLike,
         progress: Callable[[int, int], None] | None = None) -> Any:
    """Load a .vdyn file: the object it contains, or a whole test.

    `progress` is called as (objects loaded, objects in the file) —
    once up front with 0 and once per object — because a project file
    is minutes of someone's day and the reader is the only thing that
    knows how far along it is. A single-object file reports nothing:
    one object is one step, and a bar with one step is a light bulb.
    """
    import h5py

    with h5py.File(path, 'r') as f:
        return load_from(f, progress, str(path))

save

save(obj: Any, path: str | PathLike) -> None

Save a visualdynamics object to a .vdyn (HDF5) file.

Source code in src/visualdynamics/io/native.py
def save(obj: Any, path: str | os.PathLike) -> None:
    """Save a visualdynamics object to a .vdyn (HDF5) file."""
    import h5py

    with h5py.File(_visualdynamics_path(path), 'w') as f:
        save_into(obj, f)

save_test

save_test(path: str | PathLike, name: str, objects: Mapping[str, Any], active_geometry: str | None = None, project_type: str | None = None, links: Sequence[Mapping[str, Any]] | None = None, provenance: Mapping[str, Any] | None = None) -> None

Save a whole test — every named object — to one .vdyn file.

Objects go in numbered groups with the name as an attribute, so a name is free to contain anything h5py would read as structure. links is the explicit association groups, lists of object names.

Source code in src/visualdynamics/io/native.py
def save_test(path: str | os.PathLike, name: str,
              objects: Mapping[str, Any],
              active_geometry: str | None = None,
              project_type: str | None = None,
              links: Sequence[Mapping[str, Any]] | None = None,
              provenance: Mapping[str, Any] | None = None) -> None:
    """Save a whole test — every named object — to one .vdyn file.

    Objects go in numbered groups with the name as an attribute, so a name
    is free to contain anything h5py would read as structure. `links` is
    the explicit association groups, lists of object names.
    """
    import h5py

    with h5py.File(_visualdynamics_path(path), 'w') as f:
        save_test_into(f, name, objects, active_geometry, project_type,
                       links, provenance)

from_sep005

from_sep005(timeseries: dict[str, Any] | list[dict[str, Any]]) -> Any

SEP 005 timeseries into TimeHistory objects.

history = visualdynamics.from_sep005({'data': y, 'fs': 256.0,
                                      'name': 'run 4',
                                      'unit_str': 'm/s²'})

One dict returns one TimeHistory; a list — the standard's form for several series — returns {name: TimeHistory}, numbering a repeated name the way the project tree would.

unit_str entries that parse are declared on the object (values converted to SI, exactly as define_units would), because the producer stated them; one that does not parse leaves that channel's values raw with the claim kept in dimension_hint, where quantity also lands when there is no unit at all. Nothing is ever scaled by a guess.

Refused, with the reason: a series with no data, with neither fs nor time, a time vector of the wrong length, or a channel_name list that does not match the channel count.

Source code in src/visualdynamics/io/sep005.py
def from_sep005(timeseries: dict[str, Any] | list[dict[str, Any]]
                ) -> Any:
    """SEP 005 timeseries into `TimeHistory` objects.

        history = visualdynamics.from_sep005({'data': y, 'fs': 256.0,
                                              'name': 'run 4',
                                              'unit_str': 'm/s²'})

    One dict returns one `TimeHistory`; a list — the standard's form
    for several series — returns ``{name: TimeHistory}``, numbering a
    repeated name the way the project tree would.

    ``unit_str`` entries that parse are *declared* on the object
    (values converted to SI, exactly as `define_units` would), because
    the producer stated them; one that does not parse leaves that
    channel's values raw with the claim kept in `dimension_hint`, where
    ``quantity`` also lands when there is no unit at all. Nothing is
    ever scaled by a guess.

    Refused, with the reason: a series with no ``data``, with neither
    ``fs`` nor ``time``, a ``time`` vector of the wrong length, or a
    ``channel_name`` list that does not match the channel count.
    """
    if isinstance(timeseries, dict):
        return _one(timeseries)
    out: dict[str, Any] = {}
    for series in timeseries:
        name = str(series.get('name', 'Time History')) or 'Time History'
        unique, n = name, 1
        while unique in out:
            n += 1
            unique = f'{name} ({n})'
        out[unique] = _one(series)
    return out

register_importer

register_importer(name: str, description: str, sniff: Callable, load: Callable, project_type: Callable | None = None) -> None

Teach visualdynamics a format. Registered ones are tried in order, so a reader added later is asked last.

Source code in src/visualdynamics/io/__init__.py
def register_importer(name: str, description: str, sniff: Callable,
                      load: Callable,
                      project_type: Callable | None = None) -> None:
    """Teach visualdynamics a format. Registered ones are tried in order, so a
    reader added later is asked last."""
    _IMPORTERS.append(Importer(name, description, sniff, load, project_type))

importers

importers() -> list[Importer]

Every format visualdynamics can read, in the order they are tried.

Source code in src/visualdynamics/io/__init__.py
def importers() -> list[Importer]:
    """Every format visualdynamics can read, in the order they are tried."""
    return list(_IMPORTERS)

project_type_of

project_type_of(path: str | PathLike) -> str | None

The kind of project a file says it is a run of, or None.

A controller's own save knows whether it was a modal test or a random vibration run; asked before or after importing it, this is how it says so. Anything else — a geometry, a photo, a file no importer recognizes — answers None rather than raising: not knowing is the ordinary case, not a failure.

Source code in src/visualdynamics/io/__init__.py
def project_type_of(path: str | os.PathLike) -> str | None:
    """The kind of project a file says it is a run of, or None.

    A controller's own save knows whether it was a modal test or a
    random vibration run; asked before or after importing it, this is
    how it says so. Anything else — a geometry, a photo, a file no
    importer recognizes — answers None rather than raising: not knowing
    is the ordinary case, not a failure.
    """
    path = str(path)
    for imp in _IMPORTERS:
        if imp.project_type is None:
            continue
        try:
            if imp.sniff(path):
                return imp.project_type(path)
        except (ValueError, OSError):
            return None
    return None

import_file

import_file(path: str | PathLike, format: str | None = None, progress: Any | None = None, **kwargs: Any) -> Any

Import a foreign file, returning the visualdynamics object it contains.

Units may be declared here (e.g. length_unit='m') for sources that do not carry them; without a declaration the object imports unit-less, holding the file's raw values until define_units() is called. format forces a specific importer by name. progress is a (done, total) callable, honored where the reader can count — a project file's objects — and quietly unused where it cannot: a foreign file is one read, and nothing inside netCDF or UFF parsing reports fractions worth relaying.

Source code in src/visualdynamics/io/__init__.py
def import_file(path: str | os.PathLike, format: str | None = None,
                progress: Any | None = None, **kwargs: Any) -> Any:
    """Import a foreign file, returning the visualdynamics object it contains.

    Units may be declared here (e.g. length_unit='m') for sources that do not
    carry them; without a declaration the object imports unit-less, holding
    the file's raw values until `define_units()` is called.
    `format` forces a specific importer by name. `progress` is a
    (done, total) callable, honored where the reader can count — a
    project file's objects — and quietly unused where it cannot: a
    foreign file is one read, and nothing inside netCDF or UFF parsing
    reports fractions worth relaying.
    """
    # a person types a path where the window hands one over: `~` is
    # theirs to write and Python's to expand, and a path that is not
    # there has to say so. Both came back as "No importer recognizes",
    # which reads as "your file is the wrong kind" and is how a typo
    # looked like an unsupported format (Brandon, 2026-09-20).
    path = os.path.expanduser(str(path))
    if not os.path.exists(path):
        raise FileNotFoundError(f'no file at {path}')
    if os.path.isdir(path):
        raise IsADirectoryError(f'{path} is a folder, not a file')
    if path.endswith('.vdyn'):
        return load(path, progress=progress)
    if format is not None:
        for imp in _IMPORTERS:
            if imp.name == format:
                return imp.load(path, **kwargs)
        raise ValueError(f"No importer named {format!r}; "
                         f"available: {[i.name for i in _IMPORTERS]}")
    for imp in _IMPORTERS:
        if imp.sniff(path):
            return imp.load(path, **kwargs)
    # a refusal that says what the file *is* saves a round trip: a
    # container whose contents decide the reader, and whose name does
    # not, is the case that brought this up (Brandon, 2026-09-20)
    from .sniffing import describe

    said = describe(path)
    raise ValueError(f"No importer recognizes {path}"
                     + (f" — {said}" if said else ''))