Skip to content

visualdynamics.io.rattlesnake

rattlesnake

Importer for Rattlesnake vibration controller output files (.nc4).

Rattlesnake streams results to netCDF4 with a channel table that includes engineering units, so imports are fully unit-aware — no unit declaration needed. Layout (file_version 3.x):

  • root attrs: sample_rate, hardware, file_version, ...
  • time_data (response_channels, time_samples), in channel engineering units
  • channels group: node_number, node_direction, unit, channel_type, and other per-channel metadata
  • one group per environment (e.g. 'Random') that may carry a specification CPSD matrix (specification_frequency_lines, specification_cpsd_matrix_*) and the bands around it (specification_warning_matrix, specification_abort_matrix, each (2, lines, channels), lower first)

A run saves twice and the two files share nothing: streaming writes the time histories, and a separate save writes what the environment computed from them. Both are read here.

Returns a dict: 'channel_table' -> ChannelTable, and whichever of these the file holds — 'time_data' -> TimeHistory, '_specification' -> Specification (diagonal ASDs with their warning and abort limits; full_cpsd=True for the whole matrix as cross-PSD records), '_frf' -> Frf, '_coherence' -> MultipleCoherence, '_response_cpsd' and '_drive_cpsd' -> Psd.

A modal environment counts every enabled channel as a response and never excludes its references, so the drives appear among them and the saved matrix carries each drive against the drives. Those rows are identity and cross-talk by construction — |H| exactly 1 against itself, numerically zero against the other, coherence exactly 1 — so they are dropped: they are arithmetic rather than measurement, and would put a force-per-force axis on the plot beside the real one.

A spectral file carries neither time data nor, once rattlesnake's own random save has been through it, the root attributes — so the sample rate may be missing and the frequency axis has to come from the specification's own lines. The noise cross spectra a random save also writes are deliberately left alone: they describe the measurement's noise floor and would double the object count for something rarely looked at.

Reading only, and deliberately: a .nc4 records a controller run — hardware settings, environments, the lot — that visualdynamics does not hold, so a file written from here would describe a test that never happened. See docs/export.md.

Functions:

Name Description
environment_kinds

{environment name: kind} the file says it holds.

run_kind

What kind of test the file holds, by its own account.

project_type

The visualdynamics project this file is a run of, or None if visualdynamics has no

streamed_sysid_candidate

Whether a streamed save has the shape a system ID leaves:

Classes

Functions:

environment_kinds

environment_kinds(path: str | PathLike) -> dict[str, str]

{environment name: kind} the file says it holds.

Empty for a file that does not say — an nc4 assembled by hand, or an older save from before the controller wrote its types down.

Source code in src/visualdynamics/io/rattlesnake.py
def environment_kinds(path: str | os.PathLike) -> dict[str, str]:
    """{environment name: kind} the file says it holds.

    Empty for a file that does not say — an nc4 assembled by hand, or an
    older save from before the controller wrote its types down.
    """
    import netCDF4

    with netCDF4.Dataset(str(path)) as ds:
        return _environment_kinds(ds)

run_kind

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

What kind of test the file holds, by its own account.

'modal', 'random', 'transient', 'sine' or 'time' for a run of one kind; 'mixed' when more than one environment drove the article at once, which is what a random-plus-sine-sweep run is; None when the file does not say.

Source code in src/visualdynamics/io/rattlesnake.py
def run_kind(path: str | os.PathLike) -> str | None:
    """What kind of test the file holds, by its own account.

    'modal', 'random', 'transient', 'sine' or 'time' for a run of one
    kind; 'mixed' when more than one environment drove the article at
    once, which is what a random-plus-sine-sweep run is; None when the
    file does not say.
    """
    return _run_kind(environment_kinds(path).values())

project_type

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

The visualdynamics project this file is a run of, or None if visualdynamics has no project of that kind — or the file never said what kind it was.

A mixed run answers with its leading half (MIXED_PRECEDENCE): every environment's specification imports regardless, so the type only chooses which workflow the tree leads with.

Source code in src/visualdynamics/io/rattlesnake.py
def project_type(path: str | os.PathLike) -> str | None:
    """The visualdynamics project this file is a run of, or None if visualdynamics has no
    project of that kind — or the file never said what kind it was.

    A mixed run answers with its leading half (`MIXED_PRECEDENCE`):
    every environment's specification imports regardless, so the type
    only chooses which workflow the tree leads with.
    """
    import netCDF4

    with netCDF4.Dataset(str(path)) as ds:
        kinds = set(_environment_kinds(ds).values())
        # a saved system-ID package carries no environment_types — it
        # is the measured plant alone, and that is its own kind of
        # test. A *streamed* sysid save is indistinguishable from a
        # run of its environment and keeps that type; the person can
        # switch it to System ID.
        if not kinds and any('frf_data_real' in group.variables
                             for group in ds.groups.values()):
            return 'System ID'
    kind = _run_kind(kinds)
    if kind == 'mixed':
        kind = next((k for k in MIXED_PRECEDENCE if k in kinds), None)
    return PROJECT_FOR_KIND.get(kind)

streamed_sysid_candidate

streamed_sysid_candidate(path: str | PathLike) -> bool

Whether a streamed save has the shape a system ID leaves: exactly two streams, a quiet one then a loud one — the ambient measurement and the driven excitation.

The file itself cannot settle the question (a run of the environment that stopped and restarted its stream once looks the same on paper), so this is grounds to ask the person, never to decide. The quiet-then-loud check is what keeps the question from being asked about every two-stream file: a restarted run's halves play at one level, a system ID's differ by the whole test. Ten decibels is well under any real ambient-to-driven gap and well over a level change within one run.

Source code in src/visualdynamics/io/rattlesnake.py
def streamed_sysid_candidate(path: str | os.PathLike) -> bool:
    """Whether a streamed save has the shape a system ID leaves:
    exactly two streams, a quiet one then a loud one — the ambient
    measurement and the driven excitation.

    The file itself cannot settle the question (a run of the
    environment that stopped and restarted its stream once looks the
    same on paper), so this is grounds to *ask the person*, never to
    decide. The quiet-then-loud check is what keeps the question from
    being asked about every two-stream file: a restarted run's halves
    play at one level, a system ID's differ by the whole test. Ten
    decibels is well under any real ambient-to-driven gap and well
    over a level change within one run.
    """
    if not str(path).lower().endswith(('.nc4', '.nc')):
        return False
    try:
        import netCDF4
        import numpy as np

        with netCDF4.Dataset(str(path)) as ds:
            if ('time_data' not in ds.variables
                    or 'time_data_1' not in ds.variables
                    or 'time_data_2' in ds.variables):
                return False
            quiet = float(np.asarray(ds.variables['time_data'][()]).std())
            driven = float(
                np.asarray(ds.variables['time_data_1'][()]).std())
        # a simulated ambient can be exact silence; that is the
        # extreme of the same shape, not a different case
        return driven > 10 ** (10 / 20) * quiet and driven > 0.0
    except Exception:  # noqa: BLE001 - sniffers must not raise on foreign files
        return False