visualdynamics.plot¶
plot
¶
2D plotting, built on pyqtgraph.
One renderer serves both the GUI and scripting, so the rules for how data is drawn — grouping by dimension, unit-aware labels, log magnitude in the frequency domain, legends — live here only.
Records are grouped by ordinate dimension, one stacked plot per dimension,
so an object mixing accelerations, forces and voltages lands on comparable
axes. Curves are clipped to the view and, on an evenly spaced axis,
peak-downsampled — what keeps million-sample time histories interactive.
In decades the spacing is not even and the downsampling stays off (see
_draw_shaped).
Functions:
| Name | Description |
|---|---|
axis_label |
Axis label for a dimension: a typeset unit, or a note when undefined. |
build_plot |
Draw one data array. See |
build_plots |
Draw several data arrays into one pyqtgraph GraphicsLayout. |
plot_data |
Plot a data array in a window, or to a file with |
save_plot |
Render a data array to an image file (.png or .svg), no window. |
plot_mac |
The MAC grid the GUI draws: |
plot_cmif |
The CMIF the fitting screen draws: the measured indicator, and |
plot_coherence_map |
The coherence map: frequency across, channel down, pinned 0..1. |
build_photos |
Photos stacked down a layout, each at its own aspect ratio with |
plot_photos |
The photos, as the app shows them. |
plot_series |
Several data arrays on one figure — what selecting more than one |
plot_comparison |
One control channel against its specification, shaded. |
plot_bars |
The comparison as a bar per control channel. |
plot_replication |
How closely a transient replicated the waveform it was asked for. |
Classes¶
Functions:¶
legend_below
¶
A horizontal legend in its own layout row under the plot.
pyqtgraph's default legend floats inside the view, anchored to a
corner, and on a plot with any real content it lands on top of the
data — a specification's bands, an FRF's peaks — and neither can be
read (Brandon, 2026-09-01). Outside is the honest place: the plot
keeps its whole area, the names line up beneath it in columns, and
the entries still register themselves through plot.legend, so
every name= a curve was drawn with arrives as before.
Plots occupy the even rows of a layout (2 * row) and their
legends the odd ones — the window's row walker keys on
series_key, which a legend does not carry, so it steps past.
Source code in src/visualdynamics/plot/__init__.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
mac_frame_ratio
¶
Width over height for a MAC grid of this shape, clamped.
The frame outside the plot is sized by this and the aspect lock inside it is decided by the same clamp, so the two cannot disagree about whether cells are square — a frame narrower than the grid's own shape plus a lock inside it scrolls columns off the screen.
Source code in src/visualdynamics/plot/__init__.py
curve_color
¶
The nth curve's colour, wrapping. The same cycle everywhere a curve is drawn, so a record keeps its colour between the app, a standalone plot and the report.
Source code in src/visualdynamics/plot/__init__.py
axis_label
¶
axis_label(
dimension: str,
unit_system: UnitSystem,
hint: str | None = None,
) -> str
Axis label for a dimension: a typeset unit, or a note when undefined.
pyqtgraph renders label HTML, so a quotient dimension shows as a real stacked fraction rather than '(in/s**2)/lbf'.
A hint is what the source said the quantity was without saying what
scale it was on. The axis names it, since an unlabelled axis of
accelerations is less use than one that at least says 'acceleration'.
Source code in src/visualdynamics/plot/__init__.py
build_plot
¶
build_plot(
layout: Any,
data: Any,
unit_system: UnitSystem | None = None,
theme: Any = None,
max_records: int = MAX_RECORDS,
records: Sequence[int] | None = None,
component: str = "magnitude",
) -> tuple[int, int]
Draw one data array. See build_plots for several at once.
Source code in src/visualdynamics/plot/__init__.py
background_brush
¶
The plots' background: the same gradient the 3D view uses.
Anchored to the device rather than the scene, so it stays put while the data is panned and zoomed instead of sliding around behind it.
Source code in src/visualdynamics/plot/__init__.py
display_limits
¶
display_limits(
data: Any, unit_system: UnitSystem
) -> dict[str, ndarray]
A specification's limit curves in display units, or nothing.
Anything else is not a specification and has none, which is why this asks rather than testing the type: the plot does not need to know what a Specification is, only that some data brings bounds with it.
Source code in src/visualdynamics/plot/__init__.py
curve_budget
¶
How many curves each plot may draw, sharing one limit between them.
First come, first served lets the first plot spend the whole budget and leaves the next one empty — which is what selecting a spectrum and a time history did: one plot drew 45 curves and the other drew five.
Plots wanting less than an equal share release the rest to the others, so a two-channel plot beside a large one costs the large one almost nothing.
Source code in src/visualdynamics/plot/__init__.py
bounded_by_specification
¶
bounded_by_specification(
series: Sequence[
tuple[str | None, Any, Sequence[int] | None]
],
scales: dict[str, int] | None = None,
) -> tuple[
Sequence[tuple[str | None, Any, Sequence[int] | None]],
int,
]
Restrict PSDs to the records a selected specification actually bounds.
A specification is normally written for autospectra only, while a measured CPSD carries every cross term too. Drawn together, the 30 cross terms of a 6-channel CPSD are 30 curves with no limit anywhere near them — they are not wrong, they are just not the comparison being asked for, and they bury the six that are.
So when a specification is plotted alongside other PSDs, both are cut to the pairs they have in common. Nothing else is touched: a specification says nothing about a time history, and restricting one against it would drop every record for no reason.
The measured spectra are also drawn scaled to the specification
when a scaling applies — the standard practice for a run captured
below the 0 dB requirement (compliance.comparison_scale_db; held
on the object, or detected in whole dB). The scaling exists only in
the comparison: it is applied to a throwaway copy, the object's own
values are never touched, and a scaled curve says so in its legend
name. scales lets a caller who resolved the number with more
context — the app, which knows an octave PSD and the PSD it was
banded from share one scale — say what each named entry gets.
Returns (series, dropped).
Source code in src/visualdynamics/plot/__init__.py
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | |
scaled_for_comparison
¶
A throwaway copy of a spectrum with the comparison scale applied.
A shallow copy sharing everything but the values, because the whole point is that the object itself is never changed — the scale exists on the drawn comparison and nowhere else. The copy holds its scale at 0 so nothing downstream scales it again.
Source code in src/visualdynamics/plot/__init__.py
specification_pairs
¶
specification_pairs(
series: Sequence[
tuple[str | None, Any, Sequence[int] | None]
],
) -> list[tuple[str, str]]
The DOF pairs the plot draws one at a time, in the specification's own order.
Where there is a measurement, these are the pairs both sides have: a specification with its response is a comparison, and six of them on one axis is a thicket with no comparison visible in it.
A specification on its own is the same story without the second curve. Six targets and their two dozen limit lines are exactly as unreadable stacked together, so the channels are still offered one at a time — every channel the specification carries, since there is nothing to intersect them with.
Source code in src/visualdynamics/plot/__init__.py
only_pairs
¶
only_pairs(
series: Sequence[
tuple[str | None, Any, Sequence[int] | None]
],
pairs: Sequence[tuple[str, str]],
) -> Sequence[tuple[str | None, Any, Sequence[int] | None]]
The series cut to these DOF pairs, leaving anything else alone.
The plural of only_pair, because a table under the plot can name
several channels at once where a drop-down names one.
Source code in src/visualdynamics/plot/__init__.py
only_pair
¶
only_pair(
series: Sequence[
tuple[str | None, Any, Sequence[int] | None]
],
pair: tuple[str, str],
) -> Sequence[tuple[str | None, Any, Sequence[int] | None]]
The series cut to one DOF pair, leaving anything else alone.
Source code in src/visualdynamics/plot/__init__.py
pair_label
¶
How a DOF pair reads: the DOF alone when it is its own reference.
build_coherence_map
¶
build_coherence_map(
layout: Any,
series: Sequence[
tuple[str | None, Any, Sequence[int] | None]
],
unit_system: UnitSystem | None = None,
theme: Any = None,
) -> tuple[int, int]
Every channel's coherence at once: frequency across, channel down.
A line plot answers "is this channel good"; there is no reading 339 of them at once, and the curve budget means most are not even drawn. As a map the same data answers "which channels are bad, and where" in one look — a poor channel is a dark row, a poor band is a dark column.
Coherence is a bounded ratio, so the colour scale is pinned to 0..1 rather than fitted to the data: a map whose scale moved with the selection would make a good channel look bad next to a better one.
Returns (rows_drawn, rows_requested); nothing is dropped, so they match.
Source code in src/visualdynamics/plot/__init__.py
cmif_curves
¶
cmif_curves(
data: Any,
records: Sequence[int] | None = None,
unit_system: UnitSystem | None = None,
) -> tuple[ndarray, ndarray]
(singular values (k, freqs), display abscissa) — the CMIF.
The records assemble into the response x reference matrix they are — an absent pair is zero, the standard practical treatment of an incomplete matrix — and every frequency line gets a singular value decomposition. The largest singular value peaks at every mode; the second one peaking too is how a repeated root shows itself.
Source code in src/visualdynamics/plot/__init__.py
build_mac
¶
build_mac(
view: Any,
frequencies: ArrayLike,
matrix: ArrayLike,
theme: Any = None,
column_frequencies: ArrayLike | None = None,
) -> tuple[int, int]
A MAC matrix as a viridis grid.
Rows and columns are modes, labelled by their frequencies; with
column_frequencies the grid is a cross-MAC — rows one set, columns
the other — and rectangular when the counts differ. The scale is
pinned 0..1 like the coherence map, because a MAC is a bounded
ratio. The colour is the reading — per-cell numbers were tried and
made the grid too busy to read at a glance.
Source code in src/visualdynamics/plot/__init__.py
add_mode_markers
¶
Dashed bookmarks at each mode's frequency, labels staggered so clusters stay readable — the fitting screen's markers, wherever a modal synthesis is drawn.
The fitting screen keeps its subtle grey (the default); the resynthesis overlays pass the theme foreground instead — white on the dark theme, black on the light — so the markers read apart from the grid.
Source code in src/visualdynamics/plot/__init__.py
build_cmif
¶
build_cmif(
layout: Any,
series: Sequence[
tuple[str | None, Any, Sequence[int] | None]
],
unit_system: UnitSystem | None = None,
theme: Any = None,
) -> int
The Complex Mode Indicator Function of each selected FRF set.
One curve per singular value, log magnitude over frequency. A synthesized overlay's CMIF draws dashed in the measured set's colors, singular value for singular value — the modal model's indicator against the measurement's. Singular values that are numerically zero (a synthesis truncated below the reference count is rank-deficient) are dropped rather than drawn twenty decades down.
Returns the number of curves drawn.
Source code in src/visualdynamics/plot/__init__.py
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 | |
build_plots
¶
build_plots(
layout: Any,
series: Sequence[
tuple[str | None, Any, Sequence[int] | None]
],
unit_system: UnitSystem | None = None,
theme: Any = None,
max_records: int = MAX_RECORDS,
component: str = "magnitude",
) -> tuple[int, int]
Draw several data arrays into one pyqtgraph GraphicsLayout.
series is a list of (name, data, records); records may be None for
every record. Curves are grouped into a plot per (abscissa dimension,
ordinate dimension) pair, so selecting an FRF and a time history at once
stacks them on separate axes instead of nonsense-sharing one. Names
prefix the legend when more than one object is drawn.
component picks how complex frequency data is read: 'magnitude'
(log ordinate, the default), 'real', 'imag', or 'phase' (degrees,
axis pinned to ±180). Real data ignores it.
Returns (curves_drawn, curves_requested).
Source code in src/visualdynamics/plot/__init__.py
946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 | |
drawing_shape
¶
'steps' | 'law' | 'line': how this object's curve is drawn.
The hard rule, for every reading of the plot — 2-D and the 3-D
waterfall alike: a density is drawn as the energy beneath it, so
the RMS is always the plain area under the curve on screen. A PSD,
a CPSD's terms and an octave-band PSD (interpolation == 'bin')
draw flat across each bin — what is summed when the object is
integrated — and a specification ('log_log') follows the power
law its breakpoints mean. Anything that is a value at a frequency
rather than an area — an FRF, a coherence, a linear spectrum, any
signed component — draws as the line it is.
One implementation, consulted by build_plots and by
viz.waterfall, because a picture that disagrees with
Psd.area() is a picture of a number nobody computed. tagged
says a signed component (real/imag/phase) is being read, which is
never a density.
Source code in src/visualdynamics/plot/__init__.py
step_outline
¶
step_outline(
centres: ArrayLike,
values: ArrayLike,
widths: ArrayLike | None = None,
) -> tuple[ndarray, ndarray]
The flat-across-each-bin trace as explicit points.
What stepMode='center' has pyqtgraph draw for the 2-D plot, made
concrete for a renderer that only takes polylines — the 3-D
waterfall. Two points per bin on the bin's own edges (the same
bin_edges), values duplicated across, so the trapezoid under the
trace is exactly sum(value * width): the RMS is the area drawn.
values may be (records, lines); the outline is per row.
Source code in src/visualdynamics/plot/__init__.py
bin_edges
¶
The boundaries of the bins a set of line centres stands for.
A line of a discrete spectrum is a density over its own bin, so without more to go on the bin runs to the midpoint of the gap either side. Arithmetic midpoints, because FFT lines are evenly spaced in frequency.
widths is for a spectrum that knows better. An octave band's
centre is the geometric mean of its own edges, so neither the
midpoints between neighbours nor the centre plus and minus half a
width lands on them. Given the centre and the width both, the edges
follow exactly: with c = sqrt(l u) and w = u - l,
u = (w + sqrt(w^2 + 4 c^2)) / 2, l = u - w
which is the positive root and needs no assumption about the bands tiling — though these do, so one edge array serves them all.
Source code in src/visualdynamics/plot/__init__.py
as_power_law
¶
as_power_law(
frequencies: ArrayLike,
values: ArrayLike,
per_decade: int = POWER_LAW_POINTS,
) -> tuple[ndarray, ndarray]
A breakpoint curve resampled onto enough points to draw as one.
A specification's segments are power laws, and the plot's frequency axis is linear — so a straight line drawn between two breakpoints is not the curve the breakpoints mean, and for a decade-wide segment it runs well above it through the middle. Filled in, the polyline follows the law closely enough that the difference is under a pixel.
Left alone when the points are already dense: a controller writes its specification on the same line grid as everything else, and there is nothing between two adjacent lines to fill in.
Only over the band the specification is written for. A controller
writes zero outside it, and zero is a value the curve cannot be
drawn at on a log axis — it maps to -inf — while filling in across
it gives NaN. Either way the curve comes back full of holes, and a
gapped array is the one thing pyqtgraph cannot build a path from
safely (see gapless).
Source code in src/visualdynamics/plot/__init__.py
gapless
¶
One curve's points cut to the stretch it can be drawn over.
pyqtgraph builds a QPainterPath two ways. With no gaps it fills a
QPolygonF, which is Qt's own API. With gaps — more than 2% of the
points non-finite — it packs a byte buffer by hand and has Qt
deserialise it, against a private binary layout its own docstring
warns "may change in future versions of Qt". That is the path the
intermittent Bus error was raised from, and the only curve on the
plot taking it was a specification: 404 non-finite points in 1083,
because it is written to zero outside its band.
So a curve is cut to the run of it that says something rather than handed over with holes in. Nothing is bridged: the cut is the first and last point that can be drawn, so a curve that stops is drawn stopping.
Source code in src/visualdynamics/plot/__init__.py
data_curves
¶
The curves on a plot that are data, not the invisible edges the limit shading is built from.
Source code in src/visualdynamics/plot/__init__.py
plot_data
¶
plot_data(
data: DataArray,
unit_system: UnitSystem | None = None,
theme: Any = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] = (1000, 700),
path: str | PathLike | None = None,
marks: str | None = None,
**kwargs: Any,
) -> Any
Plot a data array in a window, or to a file with path.
Returns the DataPane (or the path). Complex data gets the component
box over it — the same control the app offers — so which part is
drawn is a choice rather than an argument. With show=True and no
Qt event loop already running, this blocks until the window is
closed.
marks puts a time history's own reading over the trace, which is
what the app's two toggles do: 'averaging' brackets the frames a
spectrum is averaged over, 'shocks' brackets the events an SRS is
computed from. Both are read from the history — whatever it carries,
or what the detector would suggest — and neither can be dragged
here, since there is nothing on the other end of a drag in a file.
Source code in src/visualdynamics/plot/__init__.py
save_plot
¶
save_plot(
data: DataArray,
path: str | PathLike,
unit_system: UnitSystem | None = None,
theme: Any = None,
size: tuple[int, int] = (1200, 800),
**kwargs: Any,
) -> Any
Render a data array to an image file (.png or .svg), no window.
Source code in src/visualdynamics/plot/__init__.py
plot_mac
¶
plot_mac(
shapes: ShapeSet,
other: ShapeSet | None = None,
*,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] | None = None,
bars: bool = False,
screenshot: str | None = None,
) -> Any
The MAC grid the GUI draws: shapes against itself, or the
cross-MAC against other.
The picture defaults to 700 tall by the grid's own shape, clamped the
way the app clamps it, so a 139-against-8 cross-MAC renders as the
same tall panel it is on screen rather than being stretched into a
square. Pass size to say otherwise.
bars=True is the plot bar's 3-D reading: the matrix as bars,
height and colour both the value. A scene renders through its own
plotter, so it takes screenshot= rather than path=, like every
other 3-D view.
Source code in src/visualdynamics/plot/__init__.py
plot_mac_matrix
¶
plot_mac_matrix(
frequencies: ArrayLike,
matrix: ArrayLike,
*,
column_frequencies: ArrayLike | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] | None = None,
bars: bool = False,
screenshot: str | None = None,
) -> Any
Draw a MAC matrix somebody else computed — plot_mac for two
sets that share DOF names, Project.plot_mac for the projected
comparison across geometries. One drawing, whoever did the sum.
frequencies label the rows; column_frequencies the columns
when they are another set's.
Source code in src/visualdynamics/plot/__init__.py
plot_cmif
¶
plot_cmif(
frf: Frf,
shapes: ShapeSet | None = None,
*,
modes: Iterable[int] | None = None,
unit_system: UnitSystem | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] = (1000, 700),
) -> Any
The CMIF the fitting screen draws: the measured indicator, and
with shapes the modal model's synthesis dashed over it.
Source code in src/visualdynamics/plot/__init__.py
plot_coherence_map
¶
plot_coherence_map(
coherence: _CoherenceBase,
*,
unit_system: UnitSystem | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] = (1000, 700),
) -> Any
The coherence map: frequency across, channel down, pinned 0..1.
Source code in src/visualdynamics/plot/__init__.py
build_photos
¶
build_photos(
layout: Any,
photos: Photos,
picks: Sequence[int] | None = None,
start_row: int = 0,
) -> int
Photos stacked down a layout, each at its own aspect ratio with its name as the title.
Drawing starts at start_row and the layout is not cleared, so
several sets can stack into one layout — which is what selecting
two Photos objects in the app does. Returns how many were drawn.
Source code in src/visualdynamics/plot/__init__.py
plot_photos
¶
plot_photos(
photos: Photos,
picks: Iterable[int] | None = None,
*,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] = (900, 700),
) -> Any
The photos, as the app shows them.
Source code in src/visualdynamics/plot/__init__.py
plot_series
¶
plot_series(
items: Sequence[
tuple[str | None, Any, Sequence[int] | None]
],
unit_system: UnitSystem | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] = (1000, 700),
component: str = "magnitude",
**kwargs: Any,
) -> Any
Several data arrays on one figure — what selecting more than one object in the tree draws.
visualdynamics.plot.plot_series([frf, synthesized], path='both.png')
Each item is a data array, or (name, data), or
(name, data, records) to draw only some of its records. Curves
group onto an axis per quantity, so an FRF and a time history stack
rather than sharing one, and names prefix the legend.
Source code in src/visualdynamics/plot/__init__.py
build_ratio
¶
build_ratio(
layout: Any,
signal: Any,
floor: Any,
records: Sequence[int] | None = None,
theme: Any = None,
) -> int
The louder density over the quieter, in decibels, one curve per shared channel — the signal-to-noise reading of two selected PSDs (Brandon, 2026-08-25). Returns how many channels drew.
Linear in dB on purpose, not a log axis of the linear ratio: the
number being read is the decibel. A dashed line at 0 dB marks
where the two densities are equal — for a noise floor, where the
measurement is the room. records restricts to the named rows of
the louder side, the grid's own picks.
Source code in src/visualdynamics/plot/__init__.py
plot_ratio
¶
plot_ratio(
signal: Any,
floor: Any,
*,
records: Sequence[int] | None = None,
unit_system: UnitSystem | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] = (1000, 700),
) -> Any
The ratio of two densities in decibels — the headless call for the toolbar's Ratio reading of two selected PSDs.
visualdynamics.plot.plot_ratio(driven_psds, noise_psds,
path='snr.png')
Source code in src/visualdynamics/plot/__init__.py
plot_comparison
¶
plot_comparison(
measured: Any,
specification: Any,
*,
pair: tuple[str, str] | None = None,
unit_system: UnitSystem | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] = (1000, 700),
) -> Any
One control channel against its specification, shaded.
visualdynamics.plot.plot_comparison(psds, spec, path='control.png')
The comparison the app draws when a specification and the PSDs it bounds are selected together: the measurement stepped flat across each bin, the requirement as the power law its breakpoints mean, and the ground outside the abort limits shaded — red above the upper, blue below the lower.
One channel at a time, because six of them and their two dozen limit
lines on one axis is a thicket. pair names which, as the DOF pair
the dropdown lists (('101Z+', '101Z+')); the first the two have in
common by default. Band it first — psds.to_octave(6) — to compare
on proportional bands instead.
Source code in src/visualdynamics/plot/__init__.py
plot_kurtosis
¶
plot_kurtosis(
history: Any,
*,
records: Sequence[int] | None = None,
low: float | None = None,
high: float | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] | None = None,
) -> Any
How Gaussian each channel of a record is, a bar apiece.
visualdynamics.plot.plot_kurtosis(history, path='kurtosis.png')
Pearson kurtosis, where 3 is Gaussian: above the band the record carries peaks the spectrum does not predict, below it the record is clipped or was never random. Every channel whatever it measures — kurtosis is dimensionless — and the thresholds default to one either side of nominal.
Source code in src/visualdynamics/plot/__init__.py
plot_scalogram
¶
plot_scalogram(
history: Any,
channel: int = 0,
*,
low: float | None = None,
high: float | None = None,
per_octave: int | None = None,
omega0: float | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] | None = None,
) -> Any
Where one channel's frequencies are, moment by moment.
from visualdynamics.plot import plot_scalogram
plot_scalogram(history, path='scalogram.png')
The flat form of the app's wavelet reading: time across, frequency up a logarithmic axis, magnitude as colour in the record's own units. The cone of influence is shaded, because inside it the picture is an artefact of where the record was cut and looks exactly like data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
history
|
TimeHistory
|
The record to read. |
required |
channel
|
int
|
Which channel. One at a time: a scalogram is dense enough that two side by side read as noise rather than as two answers. |
0
|
low
|
float
|
The frequency range, in Hz. Defaults span from where a handful of the longest wavelets still fit inside the record up to a fraction of Nyquist — a range the record can actually carry. |
None
|
high
|
float
|
The frequency range, in Hz. Defaults span from where a handful of the longest wavelets still fit inside the record up to a fraction of Nyquist — a range the record can actually carry. |
None
|
per_octave
|
int
|
Lines per octave. Defaults to |
None
|
omega0
|
float
|
Cycles under the wavelet: the time-against-frequency trade.
Defaults to |
None
|
theme
|
Any
|
As every other plot here. |
None
|
path
|
Any
|
As every other plot here. |
None
|
show
|
Any
|
As every other plot here. |
None
|
title
|
Any
|
As every other plot here. |
None
|
size
|
Any
|
As every other plot here. |
None
|
Returns:
| Type | Description |
|---|---|
The pane, or the path written.
|
|
Source code in src/visualdynamics/plot/__init__.py
2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 | |
plot_bars
¶
plot_bars(
measured: Any,
specification: Any,
mode: str = "error",
*,
low: float | None = None,
high: float | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] | None = None,
) -> Any
The comparison as a bar per control channel.
visualdynamics.plot.plot_bars(psds, spec, 'error', path='error.png')
mode picks the reading, which is the choice the app's toolbar
offers: 'error' is the level — how far each channel's RMS sits
from what was asked for, in dB, over the band they share — and
'lines' is the shape — how much of each channel's band fell
outside an abort limit. A channel can sit at exactly the right level
and still be out of tolerance across half its band, which is why
both exist. 'srs' is the shock reading: each channel's SRS
against its target, as a signed RMS deviation in dB.
low and high are the thresholds, defaulting to +/-3 dB and 10%.
The chart grows with the channel count rather than squeezing them
in, so size defaults to whatever fits the channels there are.
Source code in src/visualdynamics/plot/__init__.py
2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 | |
plot_replication
¶
plot_replication(
measured: Any,
specification: Any,
mode: str = "waveform",
*,
event: int | None = None,
channel: str | None = None,
low: float | None = None,
high: float | None = None,
theme: Any = None,
path: str | PathLike | None = None,
show: bool = True,
title: str | None = None,
size: tuple[int, int] | None = None,
) -> Any
How closely a transient replicated the waveform it was asked for.
visualdynamics.plot.plot_replication(record, target, 'srs', path='srs.png')
mode picks the reading, the same four the app's bar offers:
'overlay' draws one repeat over the target it was aiming at, and
'waveform', 'srs' and 'level' are the three numbers — the
difference between the waveforms as a share of the target, the
worst deviation of the shock response spectra, and the scale alone.
event is which repeat, counting from zero; left out, the bars
report every repeat and the overlay draws the first. No repeat is
singled out as the worst one — that is a judgement about what the
article is for, not a measurement, and the numbers are all returned
for the caller to make it with.
channel is which control DOF the overlay draws, defaulting to the
first. One at a time and not all of them: six measured curves over
six targets share one set of colours, and nothing on the plot then
says which curve is the target.
Source code in src/visualdynamics/plot/__init__.py
2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 | |