Skip to content

Basic Examples

Collection of basic examples for common tasks.

Complete workflow: acquire → analyse → plot

A full command-line session from a notebook or script — acquire (with and without a played output, using either the built-in generator or a hand-built NumPy array), compute spectra, PSDs/CSDs and transfer functions, then plot. Every call below is part of the public API; the numbered examples further down drill into individual steps.

1. Imports and settings

import pydvma as dvma
import numpy as np
import matplotlib.pyplot as plt

settings = dvma.MySettings(
    channels=2,          # ch0 = reference/input, ch1 = response/output
    fs=10000,
    stored_time=1.0,
    output_channels=1,   # one AO drive (only used when you play an output)
)
# device_driver defaults to 'soundcard'; set 'nidaq' for an NI device.
# Playing an output needs an output-capable device (soundcard out or NI AO).

2. Acquire — three ways

Each log_data call returns a fresh DataSet holding one TimeData (shape (n_samples, channels)) in dataset.time_data_list.

# (a) plain capture, no excitation
dataset = dvma.log_data(settings, test_name='plain')

# (b) drive the built-in generator (band-limited Gaussian noise).
#     signal_generator returns (t, output); output is (N, output_channels) in volts.
t, output = dvma.signal_generator(settings, sig='gaussian',
                                  T=settings.stored_time, amplitude=0.1,
                                  f=[20, 2000])          # bandpass corners (Hz)
dataset = dvma.log_data(settings, output=output, test_name='driven_noise')

# (c) drive a custom NumPy waveform (multi-tone), in volts.
#     Format: 2-D (N_samples, output_channels), sampled at settings.output_fs,
#     within +/- settings.output_vmax(). (See the acquisition guide for the
#     full contract — a raw array gets no auto fade or safety clamp.)
fs_out = settings.output_fs                              # = settings.fs unless overridden
tt = np.arange(0, settings.stored_time, 1 / fs_out)
drive = 0.2 * np.sin(2 * np.pi * np.outer(tt, [110.0, 370.0, 990.0])).sum(axis=1)
drive = np.clip(drive, -settings.output_vmax(), settings.output_vmax())
output = drive[:, None]                                  # -> (N, 1)
dataset = dvma.log_data(settings, output=output, test_name='driven_multitone')

To ensemble-average several repeats (e.g. impact tests), accumulate the TimeData objects into one dataset:

dataset = dvma.DataSet()
for i in range(5):
    d = dvma.log_data(settings, output=output, test_name=f'rep_{i}')
    dataset.add_to_dataset(d.time_data_list[0])

3. Spectra, PSDs/CSDs and transfer functions

DataSet has high-level *_set methods that run an analysis over every TimeData in time_data_list and store the result back on the dataset:

# Linear spectra (one-sided complex FFT) -> dataset.freq_data_list
dataset.calculate_fft_set(window='hann')

# Cross-spectrum matrix via Welch -> dataset.cross_spec_data_list.
# Pxy[i,i] is the channel-i auto-spectrum (the PSD; scaling='spectrum', V^2),
# Pxy[i,j] the cross-spectrum (CSD), Cxy[i,j] the coherence in [0, 1].
dataset.calculate_cross_spectrum_matrix_set(window='hann', N_frames=8, overlap=0.5)

# Transfer functions H = Pxy[in,out]/Pxy[in,in], one column per non-input
# channel -> dataset.tf_data_list. ch_in selects the reference channel.
dataset.calculate_tf_set(ch_in=0, window='hann', N_frames=8, overlap=0.5)

For an ensemble (a multi-capture time_data_list) average across the set instead — these give a single averaged result:

dataset.calculate_cross_spectra_averaged(window='hann')   # -> 1-item cross_spec_data_list
dataset.calculate_tf_averaged(ch_in=0, window='hann')     # -> 1-item tf_data_list

Two different frequency axes

calculate_fft_set transforms the whole block, so its axis has n_samples//2 + 1 bins. The cross-spectrum and TF use Welch segmenting (N_frames, overlap), so their freq_axis is shorter and is the one to use when plotting Pxy/Cxy/tf_data.

4. Plot

The built-in plotters open one matplotlib figure per data type and apply channel_cal_factors automatically (so they read in engineering units when you set channel_sensitivities). They use matplotlib (a core dependency), so no extras are required — an interactive backend such as %matplotlib widget gives you pan/zoom in a notebook:

dataset.plot_time_data()
dataset.plot_freq_data()   # needs calculate_fft_set() first
dataset.plot_tf_data()     # magnitude + phase + coherence; needs calculate_tf_set()

Cross-spectra (PSD/CSD) have no built-in plot — read the arrays off the CrossSpecData and plot them yourself. The diagonal of Pxy is the PSD, the off-diagonal the CSD:

csd  = dataset.cross_spec_data_list[0]
f    = csd.freq_axis
psd0 = np.abs(csd.Pxy[0, 0, :])    # PSD of channel 0  (V^2)
psd1 = np.abs(csd.Pxy[1, 1, :])    # PSD of channel 1
cs01 = csd.Pxy[0, 1, :]            # CSD ch0->ch1 (complex)
coh  = csd.Cxy[0, 1, :]            # coherence in [0, 1]

fig, (axp, axc) = plt.subplots(2, 1, figsize=(9, 7), sharex=True)
axp.semilogy(f, psd0, label='PSD ch0')
axp.semilogy(f, psd1, label='PSD ch1')
axp.semilogy(f, np.abs(cs01), '--', label='|CSD 0-1|')
axp.set_ylabel('Power (V$^2$)'); axp.legend(); axp.grid(True, which='both', alpha=0.3)
axc.plot(f, coh)
axc.set_xlabel('Frequency (Hz)'); axc.set_ylabel('Coherence'); axc.set_ylim(0, 1)
axc.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()

The same array-access pattern works for the transfer function — magnitude, phase and coherence straight from the TfData (see Example 2 below for the full plot):

tf = dataset.tf_data_list[0]
H  = tf.tf_data[:, 0]          # complex FRF, first output channel
coh = tf.tf_coherence[:, 0]    # its coherence
# |H| = np.abs(H);  phase = np.angle(H, deg=True);  axis = tf.freq_axis

A notebook session: dvma.launch

The flow above records from the notebook. dvma.launch is the other shape: it opens the web logger in your browser and hands the notebook a Session handle, so you record and monitor point-and-click and analyse in Python. It is the successor to the removed dvma.Logger (see From the Qt logger) and needs the bridge extra:

pip install "pydvma[serve,soundcard]"    # [ni] for National Instruments

1. Launch

import pydvma as dvma

settings = dvma.MySettings(device_driver='soundcard', fs=8000,
                           channels=2, stored_time=2.0)
session = dvma.launch(settings)
print(session.url)          # e.g. http://127.0.0.1:53421/ — also printed

Your browser opens on the app with Setup already filled in from settings. The server runs on a background thread inside this kernel, so the cell returns immediately and the notebook stays usable — and analysis runs in this same Python process rather than in the browser.

2. Capture in the browser

Work in the app as usual: check levels on Live, then record on Acquire. Each capture is registered with the session the moment it is taken, so the notebook can see it without you saving anything. (Closing the tab is safe too — reopen it and the app offers to restore the session. See the session journal.)

3. Pull the data into the notebook

data = session.data                        # a fresh DataSet
print(len(data.time_data_list), 'captures')

session.data is a copy, rebuilt on every access — mutate it freely; nothing reaches the app until you push. It holds the session document: the captures, any file loaded in the app, and any analysis the app has saved into it. Pressing Save Dataset in the browser is what puts the computed FFT and TF views into the document (and posts it to the server immediately), so from that moment they arrive here too:

data.freq_data_list                        # spectra the app computed
data.tf_data_list[0].source_settings       # {'calc': 'tf', 'window': 'hann', ...}
data.tf_data_list[0].source_signature      # hash of the samples it came from

Anything the app computed but never saved is not in the document — compute what you need here.

4. Analyse and plot

Everything on DataSet works as it does anywhere else:

data.calculate_fft_set(window='hann')
data.calculate_tf_set(ch_in=0, window='hann', N_frames=8, overlap=0.5)

data.plot_time_data()
data.plot_tf_data()

5. Push back

session.push(data)

The app raises a "pydvma session updated from a notebook — reload?" offer rather than replacing what is on screen — unsaved work in the tab is never silently clobbered (with an empty tray the pushed session simply loads). Merging follows the item's id:

  • the captures you pulled carry a unique_id, so an edited capture replaces the stored one in place; and
  • results the app materialised on a Save carry ids too, so pulling a session and pushing it straight back changes nothing; and
  • the FFT and TF you just computed are new items with new ids, so they append the first time and replace themselves on any later push of the same objects.

Editing samples invalidates what the app already stored

If you change a capture's time_data and push it back, any spectrum the app had saved for that capture was computed from the old samples. The app notices: each stored result carries a signature of its source, so the measurement comes back flagged ⚠ source changed in the tray, one click from a recompute. Nothing is silently trusted, and nothing is silently thrown away.

6. Save part of a session

save_data takes the same subset pick as the app's Choose sets…:

# measurement 3 only — plus every spectrum, transfer function and modal
# fit derived from it, and nothing else
dvma.save_data(data, filename='measurement-3.dvma', sets=[3])

# or work with the subset in memory (items are SHARED, not copied)
just_two = data.subset([0, 3])

7. Finish

session.close()       # stops the server; session.data still reads

Or scope it, which closes the server even if the cell raises:

with dvma.launch(settings) as session:
    ...
    data = session.data

Example 1: Simple Measurement and FFT

import pydvma as dvma
import numpy as np
import matplotlib.pyplot as plt

# Setup
settings = dvma.MySettings()
settings.fs = 10000
settings.stored_time = 2.0
settings.channels = 1

# Record
dataset = dvma.log_data(settings, test_name="example_01")
time_data = dataset.time_data_list[0]

# Calculate FFT
freq_data = dvma.calculate_fft(time_data, window='hann')

# Plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

ax1.plot(time_data.time_axis, time_data.time_data[:, 0])
ax1.set_xlabel('Time (s)')
ax1.set_ylabel('Amplitude')
ax1.set_title('Time Domain')
ax1.grid(True)

ax2.semilogy(freq_data.freq_axis, np.abs(freq_data.freq_data[:, 0]))
ax2.set_xlabel('Frequency (Hz)')
ax2.set_ylabel('Magnitude')
ax2.set_title('Frequency Domain')
ax2.set_xlim([0, 2000])
ax2.grid(True)

plt.tight_layout()
plt.show()

Example 2: Transfer Function Measurement

import pydvma as dvma
import numpy as np
import matplotlib.pyplot as plt

# Setup for 2-channel measurement
settings = dvma.MySettings()
settings.fs = 10000
settings.stored_time = 2.0
settings.channels = 2  # Channel 0: input, Channel 1: output

# Record
dataset = dvma.log_data(settings, test_name="tf_measurement")
time_data = dataset.time_data_list[0]

# Calculate transfer function
tf_data = dvma.calculate_tf(time_data, ch_in=0, window='hann',
                             N_frames=4, overlap=0.5)

# Plot
f = tf_data.freq_axis
H = tf_data.tf_data[:, 0]
coh = tf_data.tf_coherence[:, 0]

fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 10))

# Magnitude
ax1.loglog(f, np.abs(H))
ax1.set_ylabel('|H(f)|')
ax1.set_title('FRF Magnitude')
ax1.grid(True, which='both')

# Phase
ax2.semilogx(f, np.angle(H, deg=True))
ax2.set_ylabel('Phase (deg)')
ax2.set_title('FRF Phase')
ax2.grid(True)

# Coherence
ax3.semilogx(f, coh)
ax3.set_xlabel('Frequency (Hz)')
ax3.set_ylabel('Coherence')
ax3.set_ylim([0, 1])
ax3.set_title('Coherence')
ax3.grid(True)

plt.tight_layout()
plt.show()

Example 3: Impact Test with Averaging

import pydvma as dvma
import numpy as np

# Setup
settings = dvma.MySettings()
settings.fs = 10000
settings.stored_time = 1.0
settings.channels = 2
settings.pretrig_samples = 1000

# Collect multiple impacts
time_data_list = dvma.TimeDataList()
n_impacts = 5

for i in range(n_impacts):
    input(f"Press Enter for impact {i+1}/{n_impacts}...")
    data = dvma.log_data(settings, test_name=f"impact_{i}")
    time_data_list.append(data.time_data_list[0])

# Calculate averaged transfer function
tf_data = dvma.calculate_tf_averaged(time_data_list, ch_in=0)

# Results
print(f"Averaged {len(time_data_list)} measurements")
print(f"Average coherence: {np.mean(tf_data.tf_coherence):.3f}")

Example 4: Sonogram Analysis

import pydvma as dvma
import numpy as np
import matplotlib.pyplot as plt

# Record data
settings = dvma.MySettings()
settings.fs = 10000
settings.stored_time = 5.0
dataset = dvma.log_data(settings, test_name="sono_test")
time_data = dataset.time_data_list[0]

# Calculate sonogram
sono_data = dvma.calculate_sonogram(time_data, nperseg=512, noverlap=256)

# Plot
plt.figure(figsize=(12, 6))
plt.pcolormesh(sono_data.time_axis,
               sono_data.freq_axis,
               20*np.log10(np.abs(sono_data.sono_data[:, :, 0])),
               shading='gouraud',
               cmap='viridis')
plt.ylabel('Frequency (Hz)')
plt.xlabel('Time (s)')
plt.colorbar(label='Magnitude (dB)')
plt.ylim([0, 2000])
plt.title('Sonogram')
plt.show()

Example 5: Modal Analysis from Impact

import pydvma as dvma
import numpy as np

# Record impact response
settings = dvma.MySettings()
settings.fs = 10000
settings.stored_time = 2.0
settings.channels = 2
settings.pretrig_samples = 1000

dataset = dvma.log_data(settings, test_name="modal_test")
time_data = dataset.time_data_list[0]

# Extract damping from free decay
fn, Qn, fit_data = dvma.calculate_damping_from_sono(
    time_data,
    n_chan=1,
    nperseg=512
)

# Display results
print("Modal Analysis Results:")
print("-" * 40)
for i, (f, Q) in enumerate(zip(fn, Qn)):
    zeta = 1 / (2 * Q)
    print(f"Mode {i+1}:")
    print(f"  Natural frequency: {f:.2f} Hz")
    print(f"  Damping ratio: {zeta:.4f}")
    print(f"  Q factor: {Q:.1f}")
    print()