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.
    """
    path = 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:
        # A newer stamp means a newer Visual Dynamics wrote fields this
        # reader has no idea exist, and half-loading someone's project
        # quietly is worse than telling them to update. A missing stamp
        # means the file is not ours at all — every writer stamps.
        if 'visualdynamics_schema' not in f.attrs:
            raise ValueError(f'{path} is not a Visual Dynamics file '
                             '(no schema stamp)')
        written = int(f.attrs['visualdynamics_schema'])
        if written > SCHEMA_VERSION:
            raise ValueError(
                f'{path} was written by a newer Visual Dynamics '
                f'(schema {written}; this build reads up to '
                f'{SCHEMA_VERSION}). Update to open it.')
        if 'objects' in f:
            objects = {}
            keys = sorted(f['objects'])
            if progress is not None:
                progress(0, len(keys))
            for done, key in enumerate(keys, start=1):
                group = f['objects'][key]
                objects[group.attrs['name']] = (
                    _LOADERS[group.attrs['kind']](group))
                if progress is not None:
                    progress(done, len(keys))
            import json
            return Project(f.attrs['test_name'], objects,
                           f.attrs['active_geometry'] or None,
                           f.attrs['project_type'] or None,
                           json.loads(f.attrs['links']),
                           provenance=json.loads(f.attrs['provenance']))
        for group_name, loader in _LOADERS.items():
            if group_name in f:
                return loader(f[group_name])
        raise ValueError(f"No recognized content in {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

    group_name, saver = _saver_for(obj)
    with h5py.File(_visualdynamics_path(path), 'w') as f:
        f.attrs['visualdynamics_schema'] = SCHEMA_VERSION
        saver(obj, f.create_group(group_name))

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:
        f.attrs['visualdynamics_schema'] = SCHEMA_VERSION
        f.attrs['test_name'] = name
        f.attrs['active_geometry'] = active_geometry or ''
        f.attrs['project_type'] = project_type or ''
        import json
        f.attrs['links'] = json.dumps(links or [])
        # how each derived object was computed, for the staleness
        # badges — settings fingerprints, so they survive the file
        f.attrs['provenance'] = json.dumps(provenance or {})
        container = f.create_group('objects')
        for i, (obj_name, obj) in enumerate(objects.items()):
            kind, saver = _saver_for(obj)
            group = container.create_group(f'{i:04d}')
            group.attrs['name'] = obj_name
            group.attrs['kind'] = kind
            saver(obj, group)

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 recognises — 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 recognises — 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, honoured 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, honoured 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.
    """
    path = str(path)
    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)
    raise ValueError(f"No importer recognizes {path}")