Skip to content

visualdynamics.io.escdf_objects

escdf_objects

A project to and from an ESCDF file: the objects as the standard's own types, and whole as Visual Dynamics keeps them.

The standard has a type for the things a test produces — geometry, channel_table, data and response_spectrum, mode — and every one of them is written here as that type, so any reader of the format gets nodes, channels, curves and modes with nothing of ours in the way. It has no field for much of what a project holds: element blocks and what they are made of, coordinate systems, the processing record and the provenance, reports, matched modes, sine and random specifications, a geometry's view. Those ride in the attachments every group inherits (Brandon, 2026-09-30: nothing that would make their reader fail), one attachment per object, holding the object in its own .vdyn layout — the one schema owner, io.native, run into memory — so a file written here reads back whole, and a foreign reader sees an attachment it can ignore. A report rides twice: rendered to its self-contained HTML, for anyone with the file, and as its definition, so it reopens editable.

An activity is an object group (Brandon: an Activity should perhaps be equal to a linked group of objects): the group's results are the activity's data, and the geometry and channel tables it reads against are metadata at the root, linked. A named group is an activity with that name; an unnamed one takes its basis object's name. Objects linked to nothing form one activity of their own. Photos, reports, matched modes and sine specifications are metadata too — parameter sets, linked to the activity whose group holds them — because the standard puts anything that is not a result at the root.

Reading a file this program did not write builds each object from the standard fields: a geometry's nodes, lines, elements and per-node directions (a node whose axes are not the global ones gets a coordinate system of its own), a data type's records and units, a mode set's frequencies and shapes. The unit strings travel as the format spells them; both spellings parse here.

Functions:

Name Description
handles

A whole project, or any one object of it.

to_file

A project as an ESCDF File: the objects as the standard's

save

Write a project, or one object as a project of one, as an ESCDF

from_file

A project from an ESCDF File: every dataset an object, each

load

Read an ESCDF file as a project.

Classes

Functions:

handles

handles(obj: Any) -> bool

A whole project, or any one object of it.

Source code in src/visualdynamics/io/escdf_objects.py
def handles(obj: Any) -> bool:
    """A whole project, or any one object of it."""
    return isinstance(obj, (Project, Geometry, DataArray, ShapeSet, ChannelTable,
                            Report, Photos, MatchedModes, SineSweepSpecification,
                            SineLevelSet))

to_file

to_file(project: Project, created_by: str | None = None, unit_system: Any = None) -> File

A project as an ESCDF File: the objects as the standard's types, each whole in an attachment, an activity per object group.

Parameters:

Name Type Description Default
project Project

What to write.

required
created_by str

The file's creator; the login name when left out.

None
unit_system UnitSystem

The system the reports are rendered in; SI when left out.

None

Returns:

Type Description
File
Source code in src/visualdynamics/io/escdf_objects.py
def to_file(project: Project, created_by: str | None = None,
            unit_system: Any = None) -> escdf.File:
    """A project as an ESCDF `File`: the objects as the standard's
    types, each whole in an attachment, an activity per object group.

    Parameters
    ----------
    project : Project
        What to write.
    created_by : str, optional
        The file's creator; the login name when left out.
    unit_system : UnitSystem, optional
        The system the reports are rendered in; SI when left out.

    Returns
    -------
    escdf.File
    """
    from .. import SI

    file = escdf.File(created_by=created_by or getpass.getuser(),
                      created_date=dt.datetime.now(dt.UTC))
    taken: set[str] = {PROJECT_RECORD}

    def identifier(name: str) -> str:
        base = escdf.valid_identifier(name)
        candidate, k = base, 2
        while candidate in taken:
            candidate, k = f'{base}_{k}', k + 1
        taken.add(candidate)
        return candidate

    names = {name: identifier(name) for name in project}
    metadata: dict[str, escdf.Dataset] = {}
    results: dict[str, escdf.Dataset] = {}
    for name, obj in project.items():
        key = names[name]
        if isinstance(obj, Geometry):
            dataset = _geometry_dataset(key, obj)
            _attach(dataset, [(WHOLE, _whole(obj))], _note('geometry'))
            metadata[key] = dataset
        elif isinstance(obj, ChannelTable):
            dataset = _channel_table_dataset(key, obj)
            _attach(dataset, [(WHOLE, _whole(obj))], _note('channel table'))
            metadata[key] = dataset
        elif isinstance(obj, DataArray):
            dataset = _data_dataset(key, obj)
            _attach(dataset, [(WHOLE, _whole(obj))], _note(type(obj).__name__))
            results[key] = dataset
        elif isinstance(obj, ShapeSet):
            dataset = _mode_dataset(key, obj)
            _attach(dataset, [(WHOLE, _whole(obj))], _note('shape set'))
            results[key] = dataset
        elif isinstance(obj, Report):
            from ..report import render_html

            html = render_html(obj, dict(project.items()), unit_system or SI,
                               object_groups=project.object_groups)
            dataset = escdf.Dataset(key, 'parameter_set', name)
            _attach(dataset, [(f'{key}.html', np.frombuffer(html.encode('utf-8'),
                                                            dtype=np.uint8)),
                              (WHOLE, _whole(obj))],
                    _note('report') + f' {key}.html is the report rendered, '
                    'complete in one page.')
            metadata[key] = dataset
        elif isinstance(obj, Photos):
            dataset = escdf.Dataset(key, 'parameter_set', name)
            _attach(dataset, [(f'{n}.{f}', np.frombuffer(bytes(b), dtype=np.uint8))
                              for n, f, b in zip(obj.names, obj.formats, obj.images)],
                    'Photographs, one attachment each, named as taken.')
            metadata[key] = dataset
        else:
            dataset = escdf.Dataset(key, 'parameter_set', name)
            _attach(dataset, [(WHOLE, _whole(obj))], _note(type(obj).__name__))
            metadata[key] = dataset
        dataset.descriptive_name = name
    # the project's own record: what the standard has no field for
    record = escdf.Dataset(PROJECT_RECORD, 'parameter_set', project.name)
    record.values['notes'] = np.array([(
        'Visual Dynamics project record: attachments hold the object groups, '
        'the provenance of derived objects and the project settings, as JSON.')],
        dtype=object)
    settings = {'name': project.name, 'project_type': project.project_type,
                'active_geometry': project.active_geometry,
                'names': names}
    record.values['attachment_names'] = np.array(
        ['links.json', 'provenance.json', 'project.json'], dtype=object)
    record.values['attachments'] = [
        np.frombuffer(json.dumps(project.object_groups).encode('utf-8'), dtype=np.uint8),
        np.frombuffer(json.dumps(project.provenance).encode('utf-8'), dtype=np.uint8),
        np.frombuffer(json.dumps(settings).encode('utf-8'), dtype=np.uint8)]
    file.metadata[PROJECT_RECORD] = record
    file.metadata.update(metadata)
    # activities: one per object group, its results inside, its metadata linked
    placed: set[str] = set()
    activity_names: set[str] = set()
    for group in project.object_groups:
        members = [m for m in group['members'] if m in names]
        if not members:
            continue
        basis = next((m for m in members if isinstance(project[m], Geometry)), members[0])
        label = group.get('name') or basis
        key = escdf.valid_identifier(label, 'activity_')
        k = 2
        while key in activity_names:
            key, k = f'{escdf.valid_identifier(label, "activity_")}_{k}', k + 1
        activity_names.add(key)
        activity = escdf.Activity(key, label, file.created_date)
        for member in members:
            dataset_name = names[member]
            if dataset_name in results:
                activity.data[dataset_name] = results[dataset_name]
            else:
                activity.links.append(dataset_name)
            placed.add(member)
        file.activities[key] = activity
    loose = [name for name in project if name not in placed]
    if loose:
        key = escdf.valid_identifier(project.name or 'project', 'activity_')
        while key in activity_names:
            key += '_'
        activity = escdf.Activity(key, project.name or 'Unlinked objects',
                                  file.created_date)
        for member in loose:
            dataset_name = names[member]
            if dataset_name in results:
                activity.data[dataset_name] = results[dataset_name]
            else:
                activity.links.append(dataset_name)
        file.activities[key] = activity
    return file

save

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

Write a project, or one object as a project of one, as an ESCDF file.

Parameters:

Name Type Description Default
obj Project or object

What to write.

required
path path - like

Where; .h5 is added unless it already ends in .h5, .hdf5 or .escdf.

required
unit_system UnitSystem

The system reports are rendered in.

None
created_by str

The creator recorded in the file.

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

    Parameters
    ----------
    obj : Project or object
        What to write.
    path : path-like
        Where; `.h5` is added unless it already ends in `.h5`,
        `.hdf5` or `.escdf`.
    unit_system : UnitSystem, optional
        The system reports are rendered in.
    created_by : str, optional
        The creator recorded in the file.
    """
    path = str(path)
    if not path.endswith(SUFFIXES):
        path += SUFFIX
    if not isinstance(obj, Project):
        project = Project(getattr(obj, 'name', '') or type(obj).__name__)
        project.add(type(obj).__name__, obj)
        obj = project
    escdf.write(to_file(obj, created_by, unit_system), path)

from_file

from_file(file: File) -> Project

A project from an ESCDF File: every dataset an object, each activity an object group named for it, and the project's own record when the file carries one.

Parameters:

Name Type Description Default
file File

As escdf.read returns it.

required

Returns:

Type Description
Project
Source code in src/visualdynamics/io/escdf_objects.py
def from_file(file: escdf.File) -> Project:
    """A project from an ESCDF `File`: every dataset an object, each
    activity an object group named for it, and the project's own record
    when the file carries one.

    Parameters
    ----------
    file : escdf.File
        As `escdf.read` returns it.

    Returns
    -------
    Project
    """
    record = file.metadata.get(PROJECT_RECORD)
    settings: dict[str, Any] = {}
    links_record: list[dict[str, Any]] = []
    provenance: dict[str, Any] = {}
    if record is not None:
        for label, target in (('project.json', settings), ('links.json', links_record),
                              ('provenance.json', provenance)):
            blob = _attached(record, label)
            if blob is not None:
                loaded = json.loads(blob.decode('utf-8'))
                (target.update if isinstance(target, dict) else target.extend)(loaded)
    display = {v: k for k, v in settings.get('names', {}).items()}
    objects: dict[str, Any] = {}
    kept: dict[str, str] = {}                 # dataset name -> object name
    for name, dataset in file.metadata.items():
        if name == PROJECT_RECORD:
            continue
        obj = _object_from(dataset)
        if obj is not None:
            label = display.get(name) or dataset.descriptive_name or name
            objects[label] = obj
            kept[name] = label
    for activity in file.activities.values():
        for name, dataset in activity.data.items():
            obj = _object_from(dataset)
            if obj is not None:
                label = display.get(name) or dataset.descriptive_name or name
                while label in objects:
                    label += ' (2)'
                objects[label] = obj
                kept[name] = label
    # in the order the project held them, when the file says; a
    # foreign file's order is its own
    order = [display[k] for k in settings.get('names', {}).values() if k in display]
    objects = {**{name: objects[name] for name in order if name in objects},
               **objects}
    project = Project(settings.get('name') or 'ESCDF import', objects,
                      settings.get('active_geometry'), settings.get('project_type'),
                      provenance=provenance)
    if links_record and all(m in objects for g in links_record for m in g['members']):
        project.object_groups = Project('x', object_groups=links_record).object_groups
    else:
        for activity in file.activities.values():
            members = [kept[n] for n in list(activity.data) + list(activity.links)
                       if n in kept]
            if len(members) >= 2:
                project.link(*members, name=activity.descriptive_name or activity.name)
    return project

load

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

Read an ESCDF file as a project.

Parameters:

Name Type Description Default
path path - like

The file.

required

Returns:

Type Description
Project
Source code in src/visualdynamics/io/escdf_objects.py
def load(path: str | os.PathLike, **_ignored: Any) -> Project:
    """Read an ESCDF file as a project.

    Parameters
    ----------
    path : path-like
        The file.

    Returns
    -------
    Project
    """
    return from_file(escdf.read(path))