Plotting¶
Every plot the app draws can also be drawn from a script, and every
plotting call takes path= to write the figure to a file instead of
opening a window. The script and the window use the same renderer, so
a figure made in a script is the figure the app shows.
From an object¶
frf.plot() # curves, in a window
frf.plot(records=[0, 1], path='frf.png') # or straight to a file
frf.plot(component='imag') # magnitude / real / imag / phase
frf.plot_cmif(shapes) # CMIF, synthesis dashed over it
shapes.plot_mac() # auto-MAC
shapes.plot_mac(fem_shapes) # cross-MAC, on shared DOF names
project.plot_mac('Test', 'FEM') # cross-MAC across two geometries,
# projected the way the app shows it
shapes.animate(geometry, mode=2) # the deflection animation
shapes.animate(geometry, 2, screenshot='mode3.png')
shapes.animate(geometry, 2, movie='mode3.mp4') # whole cycles, as a video
shapes.animate_pair(geometry, 2, fem_shapes, 3, fem_geometry)
# two modes overlaid, as a MAC
# cell animates them
history.animate(geometry) # time records on the geometry
history.animate(geometry, quantity='force') # one quantity of a mixed record
history.animate(geometry, movie='run.mp4', seconds=10.0)
frf.animate(geometry) # the operating deflection shape,
# at the strongest line
frf.animate(geometry, frequency=647.0) # or at a chosen frequency
psd.animate(geometry) # the envelope: two copies at
# +/- sqrt(PSD), no phase claimed
psd.animate(geometry, frequency=1000.0, quantity='acceleration')
psd.animate(geometry, movie='envelope.mp4') # every line, once through
In the app, an FRF (or a complex spectrum) selected together with a geometry animates the same way: the records' magnitude and phase at one frequency line, swept like a complex mode. The cursor on the plot picks the line, starting on the strongest one, and Play sweeps the phase. Each line is drawn at full scale, so the shape at an antiresonance is as easy to see as the shape at a peak; how strongly the structure responds at that line is read from the plot.
A PSD has no phase, so it is drawn as an envelope instead: two
translucent copies of the geometry, deflected by +sqrt(PSD) and
-sqrt(PSD) at the cursor line. Together they show the extreme each
channel reaches, with no claim about when it reaches it. Size shows
each line at full scale. Color is absolute: each node is colored in dB
below the loudest node at any line, down to a floor of 40 dB, so a
quiet line still shows its shape but is colored as quiet. Play moves
the cursor up the spectrum. A node measured along two axes deflects to
the in-phase diagonal of the two, because two copies of the geometry
cannot show anything else.
A CPSD carries more information than a set of autospectra: its cross records give each channel's phase relative to the others. A whole CPSD selected with a geometry therefore animates the principal operating deflection shape, which is the dominant eigenvector of the cross-spectral matrix at each line (the direction of the output spectra's own CMIF). No reference has to be chosen. At a well-excited resonance it is the same shape the FRF's operating deflection shape shows, obtained from operating data alone. Where a mode is weakly excited, the dominant eigenvector belongs to whatever carries more power at that frequency. Picking a reference column in the grid reads the shape relative to that one channel instead, and picking only autospectra falls back to the envelope, since phase relative to nothing is not defined. Two whole CPSDs have no principal shape and also show the envelope, with the reason on the status line. In one, every cross term is zero, as in a specification of uncorrelated drives: the channels share no phase, and the dominant eigenvector would be the channel with the highest level alone. In the other, the largest eigenvalue is repeated at some line, so no one shape is dominant there.
Every animation shows one quantity at a time, because a node moved by
an acceleration and the next one moved by a force would show neither.
A record of accelerations and forces animates its accelerations, or
whichever quantity it holds most of, and a box on the toolbar offers
the others. The plot under the animation shows only the quantity being
animated, and the cursor is on that plot. In a script, quantity=
picks it, as in the examples above.
coherence.plot_map() # frequency across, channel down
geometry.plot() # the 3D scene, with its bar
geometry.plot_dofs(frf, 'force') # labeled DOF arrows
geometry.plot_channels(table, 'reference') # a channel table's, by role
project.plot_transformation('Rigid', dofs='reference', row=5) # a row of Φᵀ as arrows
The readings on a time history's bar have their own calls in
visualdynamics.plot: plot_scalogram(history) (the wavelet reading,
drawn at a picture's width whatever the record's length; the
wavelet guide explains how), plot_kurtosis(history),
plot_time_statistics(history, record) (one channel's trace, histogram
against the Gaussian and kurtosis frame by frame; see
time statistics),
plot_overload(history, table) (each channel against the range its
channel table gives it: the worst channel's trace over its range, and
the headroom bars beside the kurtosis bars; see
checking each channel against its range),
plot_replication_fit(history, rating) (whether a shaker can play the
record; see checking a replication reference),
plot_stroke(history) (a pulse's acceleration, velocity and
displacement integrated raw, against the shaker's shock ratings; see
will a pulse fit on the shaker),
plot_resample(history, rate) (the record's spectrum taken to a lower
rate, filtered first, bare and low-passed after, with the span, the
dropped lines and the folds marked; see
taking a record to a lower sample rate),
and, for a specification comparison, plot_bars(measured,
specification), plot_comparison(...), plot_ratio(...),
plot_snr(...) and plot_replication(...). Each draws the figure the
app shows, and each takes path=. plot_srs_tolerance(...) draws a
shock response spectrum against its tolerance band, point by point
(Judging an SRS against its tolerance).
plot_breakpoints(specification) draws one channel of a random
specification written at breakpoints (a density per bin is refused)
with each segment's slope and area written on it and
the running rms beneath, as the specification sheet's Areas and
running rms does (Writing a random specification by
slope).
plot_chance(psd, spec, combine=8) draws one channel of a random
comparison as its level over the target in dB, the lines outside each
band counted beside what chance puts there
(Lines outside a band by chance).
plot_multi_point(psd, specification) draws a multi-point control
run, the composite heavy over its channels with both bands, the
Composite reading (Judging a multi-point control
run).
plot_shock_check(history) draws a shock record's raw velocity beside
its positive and negative spectra, a figure the app does not show: the
Shocks view states the same check in words
(checking a shock record).
plot_half_power(frf, 180.0) draws one record with the half-power
reading of a peak on it, the reading the fitting screen's cursor shows
(Reading Q from a peak's half-power width).
plot_tone_readout(asd, [19.44, 58.32], window=...) draws an
autospectrum with each tone read off it, the reading the Tones cursor
shows (Reading a tone off an ASD).
plot_amplitude_spectrum(history, 0, window='flattop') draws one
channel's amplitude spectrum in peak units with its window and
correction in the title, the Amplitude reading's plot
(Reading a tone's amplitude from a record).
plot_rms_check(asd, history) draws an autospectrum with its rms
check written in the corner, the reading the RMS Check toggle shows
(Checking a spectrum against its record).
plot_fatigue(record, b=7.5, duration=3600.0, against=test,
against_duration=360.0) draws a record's extreme response and fatigue
damage spectra against a candidate test, with the time-compression rule
and its cap, the reading the Fatigue toggle shows (Fatigue damage,
extreme response and time compression).
plot_replication_segments(...) draws a replicated run judged segment
by segment (Judging a replication by
segment). A sine specification's sweep
against the modes it passes is plot_sweep_rate(specification, [(22.0,
40.0)]), the Sweep Rate reading (checking a sweep
rate). plot_signature(before, after, modes) draws
two signatures with a window on each mode for its limits and the table
of shifts beside (comparing signatures).
plot_dwell(history, specification) draws a held tone's level against
time, the reading the sine view's Dwell readout shows (Reading a
dwell against time). plot_transmissibility(reading) draws
H1 solid and the ASD ratio dashed with H1's band and the coherence
beneath, the reading the FRF view's Transmissibility toggle shows (The
two transmissibilities).
plot_conditioning(reading) draws a matrix's singular values with its
condition number beneath, and plot_realizability(reading) the
eigenvalues of a cross-spectral matrix's coherence matrix with what the
repair does to one pair, the readings of the Conditioning and
Realizability toggles (Checking the matrices of a multi-input
test).
plot_drive_prediction(frf, specification) draws the drives a
specification asks for through an FRF, a channel against its target and
every quantity against its rating, the reading the Drive Prediction
toggle shows for an FRF and a specification selected together
(predicting the drives).
plot_limit_prediction(transmissibility, specification, profiles)
draws the control with its predicted notches hatched and each response
channel against its limit profile, the reading the Notch Prediction
toggle shows for a transmissibility and a specification selected
together (predicting the notch).
plot_sine_distortion(history, specification, levels) draws a tone's
distortion against frequency with the limit across it, the reading the
sine view's Distortion readout shows (Checking a sine record for
distortion).
plot_sine_frequency(history, specification, levels) draws a tone's
frequency error against frequency with the band either side of zero,
the reading the sine view's Frequency readout shows (Checking a sine
tone's frequency). plot_sine_profile(profile)
draws a sine profile, or a specification's tone read as one, three
ways with its crossovers marked, the reading the Sine Profile toggle on
a sine specification's bar shows (Writing a sine profile in
segments).
plot_field_limit(flights) draws a set of measured PSDs or SRSs with
the limit taken over them and the straight-line envelope over the
limit, the reading the Field Limit toggle shows (Deriving a
specification from field data).
plot_import_preview('run.csv') draws each channel of a delimited text
import as read, its missing samples crossed and a dead channel named,
the preview the import dialog shows (Importing a time history from
delimited text).
plot_mixed_mode(history, floor, sweep) draws a mixed test's data
sheet beside the spectrum it was read from, the reading the sine view's
Mixed Mode readout shows (A mixed-mode test as one
specification).
plot_matrix_comparison(specification, cpsd) draws a run's
cross-spectral matrix against its specification as a grid, the levels
on the diagonal, the coherence above and the phase below, each cell
with its verdict, the reading the Matrix toggle shows (Judging a
cross-spectral matrix).
plot_partial_coherence(reading, response) draws a response's multiple
coherence over each drive's partial coherence, the coherence between
the drives and the principal drives' powers, the reading the multiple
coherence's Partial Coherence toggle shows (Which drive explains a
response). plot_transformation_residual(reading)
draws the measured power at a transformation's channels, the part its
outputs explain and the residual, the reading a transformed density's
Rigid-Body Residual toggle shows (The transformation to a
point).
The 3-D forms are in
visualdynamics.viz: scalogram.plot_scalogram_stage(history) is the
surface the wavelet reading opens in, and
sinespec.plot_sine_stage(specification, levels) puts measured sine
levels on the specification's stage. Both take screenshot=.
The Pulse Check reading has its call too:
plot_pulse_check(history, shape, peak, duration, edge) draws a
classical pulse on its tolerance envelope with its velocity change
beneath; see checking a classical pulse.
The toggles that draw a reading over the data are an argument:
history.plot(marks='filter') # also 'averaging', 'shocks', 'truncation'
psd.plot(marks='octave', per_octave=3) # the bands it would integrate to
psd.plot(marks='confidence') # its chi-square band, 90 and 95 %
psd.plot_waterfall(marks='octave', screenshot='bands.png') # on the stage
Each reading is taken from the object the same way the app takes it:
from the settings the object holds, or from the ones its detector
would suggest. In a script the marks are locked and cannot be dragged.
The confidence band is the exception: it is not a setting but what the
PSD's own average is worth, read from the degrees of freedom it carries
or from dof= when it carries none
(What an average is worth), and it is drawn
flat only.
A spectrum saved for a report can carry its caption block under it,
read off the record: plot_captioned(psd, record, caption=...,
specification=...) with the caption Project.caption_of gives
(A report plot's caption).
Copying what is shown¶
Whenever something is drawn, the plot bar or the 3-D view's bar ends with a copy button. It puts what the pane shows on the clipboard as an image (the flat plot or the stage, whichever is up), in the current theme and at the screen's own pixel density, for pasting into a document, a chat or an email without taking a screenshot. Every table in the application has the same button in its top-right corner while the pointer is over it. A table is copied whole, headers included, as cells for a spreadsheet and as a table for a document or an email. The report's figures, tables and photographs have the same button (Reports).
Saving an animation¶
While the 3-D view is animating something (a mode shape, an operating
deflection shape, a pair of modes picked on the MAC, a time history,
or a PSD envelope on its geometry), its bar has a second button after
Copy, Save Animation, with a strip of film on it. It saves the
animation as an MP4 video, exactly as the view is playing it: the same
camera, deflection scale and speed. A mode shape is saved as whole
cycles, at least six seconds of them, and the last frame stops one
step short of the first so that a player set to loop shows no jump. A
time history or an envelope is saved once through, at the speed it is
playing, with its plot under the 3-D view and the cursor on it where
each frame is, so the video shows where in the record (or at which
line) every moment of the animation is. A script's movie= for a time
history or an envelope makes the same picture.
The video is H.264, which plays with nothing extra installed on macOS,
Windows, Linux, phones and in a browser. It is encoded at a rate set
from the picture's size, enough to keep the edges of a fine mesh: six
seconds of a plate mode at 1024 × 768 is about 10 MB, and a dense mesh
on a large view about half a megabyte a second. It is encoded through
Qt by the operating system's own H.264 encoder. Every Mac and Windows computer has one. A Linux computer has
one only when its graphics driver provides it (VAAPI or NVENC), and
where there is none the button is not shown. From a script, the same
check is visualdynamics.gui.movie.unavailable_reason(), which returns
the reason or None. An installed copy can be tested with
visualdynamics --check-movie out.mp4, which writes one second of
video and reads it back.
A plot from a script is the app's own pane¶
When a plot is shown rather than written to a file, it opens in the same widget the application uses, with the same bar over it. A control you would use in the window is therefore there in a script too, and does not have to be passed in advance as an argument.
geometry.plot() returns a ScenePane opened on the geometry's
default view (geometry.view). Reset View returns to that view, and
the labeled axes and the orientation triad are toggles on its bar.
frf.plot() returns a DataPane. Complex data gets the component
box, so frf.plot(component='imag') sets the starting component
without fixing it, and you can switch to the real part without calling
again. Both panes expose what they wrap: pane.plotter is the PyVista
plotter and pane.graphics is the pyqtgraph layout.
The rest of the app's bar is left out because those controls only make
sense inside the window: in a script, filtering an FRF to its drive
points is done by choosing records=, and Edit Fit opens a fitting
session.
show=False returns the pane without starting an event loop, which is
what tests and notebooks need. Passing path= never creates a pane or
a toolbar: a bare plot widget is laid out off screen just long enough
to export the image.
A figure of your own¶
When no one view draws the figure you need (two runs on one axis with
one dashed, a mode marked and named, panels side by side on one
scale), visualdynamics.plot.compose draws it in the same frame the
standalone plots use, with as_series, figure_plots, to_view
and pin_range to put the readings, marks and ranges where they
belong. See
Composing a figure from a script.
From a project¶
project.plot('FRF') # dispatches on what it is
project.animate('Experimental Modes', mode=0) # finds its geometry
project.animate_pair('Test Modes', 0, 'FEM Modes', 0, movie='pair.mp4')
# each set on its own geometry, the
# Basis solid and the other drawn
# through it
What is drawn, and why¶
The plots follow the same rules as the app. Records are grouped by dimension, so mixed quantities are drawn on separate, comparable axes. Axis labels carry units in the display system. Magnitudes in the frequency domain are drawn on a log axis. Curves are peak-downsampled, so a history of a million samples stays interactive. A complex FRF is drawn as its magnitude unless you ask for another component; the signed components (real, imaginary and phase) are drawn on a linear axis, because a log axis cannot show a sign.
You choose how the frequency axis is drawn. By convention a shock
response spectrum opens in decades and everything else in hertz.
Log f on the plot bar, offered whenever what is drawn is over
frequency, switches every frequency plot, the 3-D stage and the
report's figures to decades, or back, for the rest of the session.
From a script it is visualdynamics.frequency_axis('log'), 'linear'
or 'default'. The choice is a viewing preference and not a property
of the data, so it is not saved with a project. A
controller's target starts at 0 Hz, which a log axis cannot draw, and
the status line says that the 0 Hz line is off the axis. On the 3-D
stage the decades are drawn the same way the flat plot draws them,
with a grid line and a label at every power of ten (1, 10¹, 10²
…) in place of the axis's even divisions.
Legends sit below the plot, in one horizontal row centered on the axes and wrapping to their width, so they never cover the curves they name.
The 3-D reading: a waterfall¶
With many channels on one axis, the curves hide each other exactly where it matters: resonances line up, and the tenth curve lands on top of the first nine. The 3-D reading is therefore the default. Selecting data spreads its records along a depth axis, one curve per record (a single record included), each labeled with its channel and colored by level on one scale for the whole scene, so a channel four decades quieter is drawn four decades darker. Data read on a log axis is plotted as log10, and the vertical axis title says so. The 3D button on the plot bar switches to the flat plot instead, and the choice stays either way. Views that mark the flat plot (the averaging and shock frames, the animation cursor, the fitting screen) put it up while they are in use and then go back to the reading you chose.
The camera is yours: redraws, unit changes and record picks happen under the view you set, and the scene re-frames only when you look at a different object. One vertical axis holds one quantity. A mixed object draws its largest quantity group, and the status bar names the quantities not shown; pick those records in the grid to see them. Records are peak-decimated first, so a history of a million samples arrives as the few thousand points that keep every peak. A scene holds at most ten million vertices; when the records need more, they are split into pages and the page box on the bar picks which page is on the stage. Nothing is thinned further to make them fit.
From a script it is one call:
psd.plot_waterfall() # a window of its own
psd.plot_waterfall(screenshot='stack.png') # headless, to a file
Two PSDs together: overlaid, or divided¶
Selecting two PSD objects offers three more readings on the bar. Overlaid puts both on the 3-D axes, station by station where their channels share a record, with the louder set colored by level and the quieter one drawn behind it in gray. Signal to noise (dB) pairs them where they match (same DOF, same quantity) and takes the louder as signal plus noise and the quieter as the noise: the louder minus the quieter, divided by the quieter, in decibels, line by line. RMS signal to noise is the same ratio over the whole band, one bar per channel, judged against a threshold that can be dragged. With the driven and ambient densities of a system ID, these readings are the signal-to-noise ratio. It is computed when you look at it and is not stored as a third object. The system ID guide gives the definition and its sources. Overlaid and the line-by-line reading follow the 3D toggle, and a pair with mixed quantities gets a quantity box that names what to compare.
Two FRFs together¶
Selecting two FRF objects (one survey measured twice, or two system ID runs) pairs them on the same stage. They are matched channel by channel, by response and reference; the one higher in the project tree is colored by level and the other is drawn behind it in gray. Picking the same record in each object's grid (click it in one and Cmd-click it in the other; Ctrl-click on Windows and Linux) puts just those two curves at one station. The 3D toggle switches back to the flat curves.
A measurement and its resynthesis¶
Picking modes from a shape set together with measured FRFs asks the modal model to predict those FRFs, and the pair is drawn on the same kind of stage: each shared channel is a station, with the measurement colored by level and the synthesis behind it in gray. This matches the flat plot, which draws the synthesis dashed under the measurement. Ninety-six FRFs and their ninety-six predictions on one axis are hard to read; spread in depth, the fit of each channel can be read on its own. The 3D toggle switches back to the dashed flat overlay.
Plots are drawn in a display system, SI by default:
Files¶
The extension picks the format: .png or .svg. 3-D scenes take
screenshot= in place of path=, because a scene renders through its
own plotter.
Figures for print¶
A figure for print is laid out at its printed size, in logical pixels (inches times 96), and written at a print resolution, so every font prints at its set size:
import visualdynamics.plot as vdplot
vdplot.EXPORT_DPI = 300 # every standalone .png
psd.save_plot('psd.png', size=(328, 240)) # 3.42 in by 2.5 in
vdplot.save_image(layout, 'fig.png', dpi=300) # a layout of your own
save_image paints the laid-out plot scaled by dpi/96 (text, lines,
markers and layout together) and writes the resolution into the file.
The legend's text size is vdplot.LEGEND_TEXT_SIZE, 8 pt. On screen a
curve is one pixel wide (vdplot.CURVE_WIDTH), the width Qt draws
fastest. An image of a plot (a .png, a print figure, a copy to the
clipboard) draws every curve at least vdplot.EXPORT_CURVE_WIDTH (2)
wide, since it is drawn only once. On the light theme the curves use a
darker shade of each color, with a contrast of 4.5:1 or better against
white. A 3-D scene takes the same two numbers:
from visualdynamics.viz.geometry import plot_dofs
plot_dofs(geometry, frf, 'acceleration', screenshot='dofs.png',
size_in=(4.5, 3.25), dpi=300, bounds=False)
The window is set to the printed size, labels are sized in points, and
nodes, lines and the bounds box's labels scale with it. bounds=False
and orientation=False leave out the box and the corner triad.
One figure as an interactive page¶
visualdynamics.export_html writes one figure, the same one a report
would draw, as a single HTML file that opens offline. A plot keeps its
zoom, pan and readout, and a scene can be turned and animated. The
page fills whatever frame holds it, such as an iframe or a box on a
slide:
import visualdynamics
visualdynamics.export_html('spec.html', psd, specification=spec,
channel='101Z+', theme='light')
visualdynamics.export_html('modes.html', geometry=geometry, shapes=modes)
visualdynamics.export_html('dofs.html', geometry=geometry,
dofs=('acceleration', frf))
theme fixes the figure's colors; without it, the figure follows the
reader's theme. fill=False lays the figure out the way a report page
does. channels keeps only some records and max_points thins every
curve, keeping its peaks. For a website that allows no inline script,
export_figure writes the same figure as a data file and a script;
see A figure in a page of your own.