Plotting¶
Every plot the app draws has a call, and every call takes path= to
render to a file instead of opening a window. One renderer serves
both, so a figure in a script is the figure on the screen.
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')
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')
An FRF (or a complex spectrum) selected beside a geometry in the app animates the same thing: the records' magnitude and phase at one frequency line, swept like a complex mode. The cursor on the plot picks the line — it starts on the strongest one — and Play sweeps the phase.
A PSD has no phase, so it gets the envelope instead: two translucent
copies of the geometry deflected +sqrt(PSD) and -sqrt(PSD) at the
cursor line — every extreme every channel reaches, with no claim about
when. Size shows each line at full scale; color is absolute, dB below
the loudest node at any line, so a quiet line shows its shape but wears
its quietness. Play walks the cursor up the spectrum. One quantity
deflects at a time (the toolbar box picks), and a node measured along
two axes reaches their in-phase diagonal, which two copies cannot
avoid claiming.
A CPSD holds more: its cross records carry each channel's phase relative to the others, so a whole CPSD beside a geometry animates the principal operating deflection shape — the dominant eigenvector of the cross-spectral matrix at each line, the direction of the output spectra's own CMIF, no reference to choose. At a well-excited resonance it is the same shape the FRF ODS shows, read from operating data alone; where a mode is weakly excited the dominant eigenvector honestly belongs to whatever carries more power there. Picking a reference column in the grid reads the shape relative to that one channel instead, and picking autos alone falls back to the envelope — phase relative to nothing is no phase at all.
coherence.plot_map() # frequency across, channel down
geometry.plot() # the 3D scene, with its bar
geometry.plot_dofs(frf, 'force') # labeled DOF arrows
The readings on a time history's bar have calls of their own, in
visualdynamics.plot: plot_scalogram(history) (the wavelet reading,
drawn at a picture's width whatever the record's length — the
wavelet guide says how), plot_kurtosis(history), and
for a specification comparison plot_bars(measured, specification),
plot_comparison(...), plot_ratio(...), plot_snr(...) and plot_replication(...)
— each the figure the app shows, each taking path=.
Copying what is shown¶
Whenever something is drawn, the plot bar or the 3-D view's bar ends in 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 at the screen's own pixel density — for pasting into a document, a chat or an email without a screenshot. Every table in the application carries the same button in its top-right corner while the pointer is over it; a table copies whole, headers and all, as cells for a spreadsheet and as a table for a document or a mail. The report's figures, tables and photographs have the same button (Reports).
A plot from a script is the app's own pane¶
Shown rather than written to a file, a plot comes up in the same widget the application uses, with the same bar over it — so a control you would reach for in the window is there in a script too, and does not have to be known about in advance as an argument.
geometry.plot() returns a ScenePane, opened on the geometry's
default view (geometry.view): Reset View returns to it, and the
labeled axes and the orientation triad are toggles on its bar. frf.plot() returns a
DataPane, and complex data gets the component box, so
frf.plot(component='imag') sets where it starts rather than fixing it
— you can switch to the real part without calling again. Both expose
what they wrap: pane.plotter is the PyVista plotter, pane.graphics
the pyqtgraph layout.
The rest of the app's bar is absent because it is not a live option outside the window: filtering an FRF to its drive points is a record selection, and Edit Fit opens a fitting session.
show=False returns the pane without starting an event loop, which is
what tests and notebooks want. Passing path= never comes near a pane
or a toolbar: a bare plot widget is laid out off screen just long
enough to export the image.
From a project¶
project.plot('FRF') # dispatches on what it is
project.animate('Experimental Modes', mode=0) # finds its geometry
What is drawn, and why¶
The reading rules are the app's own: records grouped by dimension so mixed quantities land on comparable axes, unit-aware labels in the display system, log magnitude in the frequency domain, curves peak-downsampled so a million-sample history stays interactive. A complex FRF reads as magnitude unless you ask for a component; the signed ones go linear, because a signed quantity on a log axis is a lie.
The frequency axis is the viewer's to choose. By convention a
shock response spectrum reads in decades and everything else in
hertz, and that is how each opens; 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 session (visualdynamics.frequency_axis('log'),
'linear' or 'default' from a script). It is a habit rather than
a property of the data, so it is not saved with a project. A
controller's target starts at 0 Hz, which no log axis can draw; the
status line says the line is off the axis rather than letting it
vanish. On the 3-D stage the decades are drawn the way the flat plot
draws them — 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, one horizontal row centered on the axes and wrapping to their width, rather than floating over the curves — a legend inside the axes hides the data it names, and a long one hid most of it.
The 3-D reading: a waterfall¶
Many channels on one axis hide each other exactly where it matters — resonances line up, and the tenth curve lands on the first nine. So the 3-D reading is the default: selecting data spreads its records along a depth axis, one curve per record — a single record included — labeled with the channel it is, colored by level on one scale for the whole scene, so a channel four decades quieter draws four decades darker. Log-read data plots as log10 and the vertical axis title says so. The 3D button on the plot bar is how the flat plot is asked for instead; the choice sticks 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 hand back to the reading you chose.
The scene follows the app's standing rules. The camera is yours — redraws, unit switches and record picks happen under the view you set, and it 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 what waits; pick the other records in the grid to see them. Records are peak-decimated first, so a million-sample history arrives as the few thousand points that keep every peak.
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. The overlay stages both on the 3-D axes, station by station where their channels share a record, the louder set colored by level and the quieter 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 less the quieter, over 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, computed where they are looked at rather than stored as a third object; the system ID guide gives the definition and its sources. The overlay and the line-by-line reading honor the 2D/3D toggle, and a mixed pair offers a quantity box naming what to compare.
A measurement and its resynthesis¶
Picking modes from a shape set beside measured FRFs asks the modal model to predict them, and the pair reads on that same stage: each shared channel a station, the measurement colored by level and the synthesis behind it in gray — the emphasis the flat plot gives them when it draws the synthesis dashed under the measurement. Ninety-six FRFs and their ninety-six predictions on one axis is a band with the answer buried in it; given depth, each channel's fit can be read on its own. The 2D/3D toggle hands back to the dashed flat overlay.
Plots draw in a display system, SI by default:
Files¶
.png or .svg — the extension picks the exporter. 3D scenes use
screenshot= rather than path=, because a scene renders through
its own plotter.
Figures for print¶
A printed figure 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 at a ratio of dpi/96 — text,
lines, markers and layout together — and writes the resolution into
the file. The legend's text 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 once. On the light theme the curves take a
darker shade of each color, 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 the printed size, labels print as points, and nodes,
lines and the bounds box's labels scale with it; bounds=False and
orientation=False leave the box and the corner triad out.
One figure as an interactive page¶
visualdynamics.export_html writes one figure — the report's own, with
zoom, pan and the readout on a plot, turning and animation on a scene —
as a single HTML file that opens offline and fills whatever frame holds
it, 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; left out, it follows the reader's.
fill=False lays it out as a report page does.