Skip to content

visualdynamics.io.native

native

Native .vdyn file format: HDF5.

A single object is one group named for its kind at the root. A whole test is an objects/ container of numbered groups — number for order, name and kind as attributes, so a user's name is free to contain anything h5py would read as structure — plus the test's own name and active geometry as root attributes. Ragged connectivity is stored as a flat array plus offsets.

Functions:

Name Description
save_sine_specification

One subgroup per tone, the file's own shape: breakpoints with

save_sine_levels

One subgroup per tone, each the ordinary data layout — the set

save

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

save_test

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

load

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

Classes

Functions:

save_sine_specification

save_sine_specification(spec, group) -> None

One subgroup per tone, the file's own shape: breakpoints with the per-segment sweep law, bands where they exist.

Source code in src/visualdynamics/io/native.py
def save_sine_specification(spec, group) -> None:
    """One subgroup per tone, the file's own shape: breakpoints with
    the per-segment sweep law, bands where they exist."""
    _write_strings(group, 'response_dof', spec.response_dof)
    group.attrs['ordinate_dim'] = spec.ordinate_dim
    group.attrs['ordinate_unit'] = spec.ordinate_unit or ''
    group.attrs['comment'] = spec.comment
    _write_strings(group, 'tone_order', [tone.name for tone in spec.tones])
    tones = group.create_group('tones')
    for k, tone in enumerate(spec.tones):
        # index-named subgroups: tone names are user text and h5py
        # group names cannot hold a '/', so the order array holds the
        # names and the groups hold the numbers
        sub = tones.create_group(str(k))
        sub.attrs['name'] = tone.name
        sub.attrs['start_time'] = tone.start_time
        sub.create_dataset('frequency', data=tone.frequency)
        sub.create_dataset('amplitude', data=tone.amplitude)
        sub.create_dataset('phase', data=tone.phase)
        sub.create_dataset('segment_type', data=tone.segment_type)
        sub.create_dataset('segment_rate', data=tone.segment_rate)
        for name, values in tone.limits.items():
            sub.create_dataset(name, data=values)

save_sine_levels

save_sine_levels(levels, group) -> None

One subgroup per tone, each the ordinary data layout — the set is grouping, not a new format.

Source code in src/visualdynamics/io/native.py
def save_sine_levels(levels, group) -> None:
    """One subgroup per tone, each the ordinary data layout — the set
    is grouping, not a new format."""
    _write_strings(group, 'tone_order',
                   [level.tone for level in levels.levels])
    tones = group.create_group('tones')
    for k, level in enumerate(levels.levels):
        save_data(level, tones.create_group(str(k)))

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)

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}")