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.

Classes:

Name Description
ImportNote

A reader's note about a file it imported: what it read

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

ImportNote

Bases: UserWarning

A reader's note about a file it imported: what it read differently from how the file wrote it, and why.

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.

Parameters:

Name Type Description Default
obj object

What to write: a geometry, data, shapes, a project, and so on.

required
path str or PathLike

Where; the suffix picks the format when format is not given.

required
format str

An exporter by name (visualdynamics.io.exporters), whatever the suffix says.

None
unit_system UnitSystem

The units to write in; the stored values as they are when omitted.

None
**kwargs Any

Passed to the format's writer, e.g. an ESCDF file's created_by.

{}

Returns:

Type Description
None
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.

    Parameters
    ----------
    obj : object
        What to write: a geometry, data, shapes, a project, and so on.
    path : str or os.PathLike
        Where; the suffix picks the format when `format` is not given.
    format : str, optional
        An exporter by name (`visualdynamics.io.exporters`), whatever the
        suffix says.
    unit_system : UnitSystem, optional
        The units to write in; the stored values as they are when omitted.
    **kwargs
        Passed to the format's writer, e.g. an ESCDF file's `created_by`.

    Returns
    -------
    None
    """
    # `~` 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.

Parameters:

Name Type Description Default
path str or PathLike

The .vdyn file.

required
progress callable

Called as progress(loaded, total), once up front and once per object.

None

Returns:

Type Description
object

The object the file holds, or a Project for a whole test.

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.

    Parameters
    ----------
    path : str or os.PathLike
        The `.vdyn` file.
    progress : callable, optional
        Called as ``progress(loaded, total)``, once up front and once per
        object.

    Returns
    -------
    object
        The object the file holds, or a `Project` for a whole test.
    """
    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.

Parameters:

Name Type Description Default
obj object

The object to save.

required
path str or PathLike

Where; .vdyn is added when the suffix is not there.

required

Returns:

Type Description
None
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.

    Parameters
    ----------
    obj : object
        The object to save.
    path : str or os.PathLike
        Where; `.vdyn` is added when the suffix is not there.

    Returns
    -------
    None
    """
    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, object_groups: 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,
              object_groups: 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,
                       object_groups, 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.

Parameters:

Name Type Description Default
timeseries dict or list of dict

One SEP 005 series, or a list of them.

required

Returns:

Type Description
TimeHistory or dict of str to TimeHistory

One for one dict; for a list, every series by name.

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.

    Parameters
    ----------
    timeseries : dict or list of dict
        One SEP 005 series, or a list of them.

    Returns
    -------
    TimeHistory or dict of str to TimeHistory
        One for one dict; for a list, every series by name.
    """
    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.

Parameters:

Name Type Description Default
name str

The importer's name, which import_file(format=) takes.

required
description str

What the format is, as a file dialog lists it.

required
sniff callable

sniff(path) -> bool: whether a file is this format.

required
load callable

load(path, **kwargs): the file read into objects.

required
project_type callable

project_type(path) -> str or None: the project type the file declares, for a format that carries one.

None

Returns:

Type Description
None
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.

    Parameters
    ----------
    name : str
        The importer's name, which `import_file(format=)` takes.
    description : str
        What the format is, as a file dialog lists it.
    sniff : callable
        ``sniff(path) -> bool``: whether a file is this format.
    load : callable
        ``load(path, **kwargs)``: the file read into objects.
    project_type : callable, optional
        ``project_type(path) -> str or None``: the project type the file
        declares, for a format that carries one.

    Returns
    -------
    None
    """
    _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, a Rattlesnake run's samples (2026-10-02) — and quietly unused where it cannot: a UFF file is one parse, and nothing inside it reports fractions worth relaying.

Parameters:

Name Type Description Default
path str or PathLike

The file to read.

required
format str

An importer by name, rather than the one the file is recognized as.

None
progress callable

Called as progress(done, total) where the reader can count.

None
**kwargs Any

Passed to the importer: declared units (length_unit='m'), a window of a long run (start=, stop=), channels, and so on.

{}

Returns:

Type Description
object

The object the file holds; a dict of them by name for a file that holds several, and a Project for a project file.

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, a Rattlesnake run's samples (2026-10-02) —
    and quietly unused where it cannot: a UFF file is one parse, and
    nothing inside it reports fractions worth relaying.

    Parameters
    ----------
    path : str or os.PathLike
        The file to read.
    format : str, optional
        An importer by name, rather than the one the file is recognized as.
    progress : callable, optional
        Called as ``progress(done, total)`` where the reader can count.
    **kwargs
        Passed to the importer: declared units (``length_unit='m'``), a
        window of a long run (``start=``, ``stop=``), channels, and so on.

    Returns
    -------
    object
        The object the file holds; a dict of them by name for a file that
        holds several, and a `Project` for a project file.
    """
    import inspect

    def load_with(imp):
        kwargs_for = dict(kwargs)
        if progress is not None and 'progress' in inspect.signature(
                imp.load).parameters:
            kwargs_for['progress'] = progress
        return imp.load(path, **kwargs_for)

    # 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 load_with(imp)
        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 load_with(imp)
    # 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 ''))