visualdynamics.deform¶
deform
¶
Turning values at DOFs into node motion.
Mode shape animation, operating deflection shapes, PSD envelopes and time history playback all answer the same question: given values at DOFs, where does each node go? A DOF is a node plus a signed direction, so a value displaces its node along that direction, rotated out of the node's displacement coordinate system into global coordinates.
The expensive work is done once, when a deflection is built, so drawing a frame is a multiply and a write:
- direction signs and coordinate system rotations are combined into one unit vector per DOF
- only the nodes that move are written each frame
- for time histories, DOFs are grouped into passes with no repeated node, so a frame is a few vectorized adds instead of a scatter-add
Rotational DOFs are ignored, since a rotation does not displace the node it belongs to. Nodes with no data do not move.
Classes:
| Name | Description |
|---|---|
Deflection |
Node offsets as a function of one parameter (a phase, or a sample |
ShapeDeflection |
One mode shape, animated through phase: the offsets at phase |
OdsDeflection |
An operating deflection shape read from complex spectra. |
EnvelopeDeflection |
The envelope of a PSD at one line, which can be moved. |
TimeDeflection |
Time histories played back: the offsets are the samples at one |
ModalTimeDeflection |
Modal coordinates played back through their shapes: the offsets |
Functions:
| Name | Description |
|---|---|
animation_records |
Which records a deflection animation shows, as (indices, note). |
first_capture |
The records an animation starts from, as (indices, note): the |
quantity_records |
The records of one quantity, as (indices, quantity, other-quantity |
envelope_records |
Which records a PSD envelope shows, as (indices, quantity, cross |
dof_directions |
(rows, directions, used) for the DOFs this geometry can move. |
node_displacements |
The (num_nodes, 3) displacement of every node from one value per |
model_size |
The length of the geometry's bounding-box diagonal, which |
auto_scale |
The scale that draws a deflection of |
modal_deflection |
The modal records among |
Classes¶
Deflection
¶
Node offsets as a function of one parameter (a phase, or a sample index).
Subclasses have rows, the geometry rows of the nodes that move,
and offsets(parameter), those rows' (len(rows), 3) displacement.
No other node moves, so the cost of a frame depends on the nodes
with data and not on the size of the model.
offsets returns a buffer it reuses on every call, so a frame
allocates nothing. Use the result before calling again, or copy it.
Methods:
| Name | Description |
|---|---|
offsets |
The (len(rows), 3) displacement of the moving nodes at |
Attributes:
| Name | Type | Description |
|---|---|---|
peak |
float
|
The largest single displacement component over the whole |
peak_magnitude |
float
|
The largest distance any node travels, over the whole animation. |
Attributes¶
peak
property
¶
The largest single displacement component over the whole animation, which the animator scales against.
peak_magnitude
property
¶
The largest distance any node travels, over the whole animation.
This differs from peak, the largest value of a single DOF: a
node moving in both X and Z travels sqrt(x^2 + z^2), further than
either. It is the top of the color scale, so the top color
appears only at the instant the model is at its furthest.
Methods:¶
offsets
¶
ShapeDeflection
¶
ShapeDeflection(geometry: Geometry, dofs: Sequence[str], shape: ArrayLike)
Bases: Deflection
One mode shape, animated through phase: the offsets at phase
theta are Re(phi * exp(i*theta)).
Methods:
| Name | Description |
|---|---|
offsets |
The offsets at phase |
Attributes:
| Name | Type | Description |
|---|---|---|
peak |
float
|
The largest magnitude of any node's shape coefficient along a |
peak_magnitude |
float
|
The largest distance any node travels over a cycle, in closed |
Source code in src/visualdynamics/deform.py
Attributes¶
peak
property
¶
The largest magnitude of any node's shape coefficient along a global axis.
peak_magnitude
property
¶
The largest distance any node travels over a cycle, in closed form, so no phase sweep is needed.
With offsets rcos(t) - msin(t), the squared distance a node travels is (A+B)/2 + (A-B)/2cos(2t) - (r.m)sin(2t), where A=r.r and B=m.m. Its largest value over t is (A+B)/2 + hypot((A-B)/2, r.m).
Methods:¶
offsets
¶
The offsets at phase parameter, in radians.
Source code in src/visualdynamics/deform.py
OdsDeflection
¶
OdsDeflection(geometry: Geometry, dofs: Sequence[str], ordinate: ArrayLike)
Bases: Deflection
An operating deflection shape read from complex spectra.
At one abscissa line the records' complex values are a deflection
pattern, a magnitude and phase per DOF, and the animation sweeps it
the same way as a complex mode: Re(H(w_line) * exp(i*theta)). The
plot cursor sets line, the frequency shown, and moving it
recomputes only the pattern at that line, not the scene.
Every line is drawn at full scale: the pattern is normalized by its own largest record, so an anti-resonance shows its shape as clearly as a peak. How large the response is at that line is shown by the plot's curve. The pattern's overall phase is also rotated so that phase zero is its fullest deflection, since a steady state has no time origin.
Methods:
| Name | Description |
|---|---|
offsets |
The offsets at phase |
Attributes:
| Name | Type | Description |
|---|---|---|
line |
int
|
The abscissa index whose pattern is shown. Setting it clips |
strongest_line |
int
|
The line where a record is largest, where the cursor |
peak |
float
|
1.0, the value the animator scales against, or 0.0 when there |
peak_magnitude |
float
|
The furthest any node gets, over every line and phase, with |
Source code in src/visualdynamics/deform.py
Attributes¶
line
property
writable
¶
The abscissa index whose pattern is shown. Setting it clips to the valid range.
strongest_line
property
¶
The line where a record is largest, where the cursor starts, since line 0 of a spectrum is often nearly zero.
peak
property
¶
1.0, the value the animator scales against, or 0.0 when there is no data. Every line is normalized to its own largest record, so the largest DOF value any line shows is 1.
peak_magnitude
property
¶
The furthest any node gets, over every line and phase, with each line at its normalized scale.
Each line uses ShapeDeflection's closed form. The lines are
processed in chunks to limit memory, as TimeDeflection does
with its samples.
Methods:¶
offsets
¶
The offsets at phase parameter, in radians.
EnvelopeDeflection
¶
EnvelopeDeflection(geometry: Geometry, dofs: Sequence[str], ordinate: ArrayLike)
Bases: Deflection
The envelope of a PSD at one line, which can be moved.
An autospectrum gives each DOF an amplitude, the square root of the PSD value, and no phase or sign. So what is drawn is the envelope: the extreme each DOF reaches, with no claim about when. One of these deflects a copy of the geometry by +pattern; the animator draws a second copy at -pattern by giving it a negative scale, and the pair is the envelope.
line moves as OdsDeflection's does, and each line is drawn at its
own full scale for the same reason. The level is shown by color, on
an absolute scale: node_decibels gives each node's level against
the loudest node at any line, with FLOOR_DB as the bottom.
A node measured along two axes is drawn at the in-phase diagonal of the two, because two copies of the geometry cannot show the four corners of the true rectangle. That is exact for single-axis surveys, and the plotting guide says so for the rest.
Methods:
| Name | Description |
|---|---|
offsets |
The +pattern at the current line. |
node_decibels |
Each moving node's level at the current line, in dB relative |
Attributes:
| Name | Type | Description |
|---|---|---|
line |
int
|
The abscissa index whose envelope is shown. Setting it clips |
strongest_line |
int
|
The line where a record is largest, where the cursor starts. |
peak |
float
|
1.0, since every line is normalized to its own largest |
peak_magnitude |
float
|
The furthest any node gets, with each line at its normalized |
Source code in src/visualdynamics/deform.py
Attributes¶
line
property
writable
¶
The abscissa index whose envelope is shown. Setting it clips to the valid range.
strongest_line
property
¶
The line where a record is largest, where the cursor starts.
peak
property
¶
1.0, since every line is normalized to its own largest record, or 0.0 when there is no data.
peak_magnitude
property
¶
The furthest any node gets, with each line at its normalized scale. The animator does not use it when it fixes the color range in dB, but every deflection provides it.
Methods:¶
offsets
¶
The +pattern at the current line. parameter is not used:
the line sets the pattern, and the sign is the animator's
scale.
node_decibels
¶
Each moving node's level at the current line, in dB relative
to the loudest node at any line (0 dB), no lower than
FLOOR_DB. The color scale shows these values.
TimeDeflection
¶
TimeDeflection(geometry: Geometry, dofs: Sequence[str], ordinate: ArrayLike)
Bases: Deflection
Time histories played back: the offsets are the samples at one index.
Methods:
| Name | Description |
|---|---|
offsets |
The offsets at sample index |
Attributes:
| Name | Type | Description |
|---|---|---|
peak |
float
|
The largest magnitude of any sample of any record used. |
peak_magnitude |
float
|
The furthest any node gets over all samples. |
Source code in src/visualdynamics/deform.py
Attributes¶
peak_magnitude
property
¶
The furthest any node gets over all samples.
The samples are processed in chunks, because the full (nodes, samples, 3) array would be far larger than the data it came from.
Methods:¶
offsets
¶
The offsets at sample index parameter.
Source code in src/visualdynamics/deform.py
ModalTimeDeflection
¶
ModalTimeDeflection(geometry: Geometry, dofs: Sequence[str], shapes: ArrayLike, ordinate: ArrayLike)
Bases: Deflection
Modal coordinates played back through their shapes: the offsets at sample k are the sum over modes of q_r[k] times mode r's node displacements.
The physical response u = Φq is never formed. Each mode's node
displacements (fields, one row per mode) are built once, and a
frame is one vector-matrix product of the modes' values at that
sample with them: modes × moving nodes × 3 multiply-adds, and no
memory beyond the shapes themselves. For a model of a million DOFs
and fifty modes a frame takes about 7 ms, where expanding a record
of ten thousand samples first would take eighty gigabytes.
dofs are the set's DOFs, shapes its real shape values at
them, one row per mode used, and ordinate the modal
coordinates, one row per mode in the same order.
Methods:
| Name | Description |
|---|---|
offsets |
The offsets at sample index |
Attributes:
| Name | Type | Description |
|---|---|---|
peak |
float
|
The largest single displacement component over the samples |
peak_magnitude |
float
|
The furthest any node gets over the samples read |
Source code in src/visualdynamics/deform.py
Functions:¶
animation_records
¶
animation_records(data: DataArray, records: Sequence[int] | None) -> tuple[list[int], str]
Which records a deflection animation shows, as (indices, note).
An animation gives every node one trajectory, so it can use at most one record per DOF. Two rules follow:
- When
recordsis None and the object holds repeated captures (itsblocklabels), only the first capture is animated, since twenty averages summed into one waveform is not a measurement anyone took. This is the same convention as a shape set: the first, with a note saying how to pick another. - Of what remains, the first record for each response DOF is kept and the rest are dropped. A drive point has a load cell beside its accelerometer, and meters per second squared plus newtons is not a deflection. An FRF repeats every response DOF once per reference in the same way, so a whole FRF is animated against its first reference.
The note is empty when nothing was dropped, and otherwise is text to append to a status line, saying what was left out. Dropping records is reported, never refused.
Source code in src/visualdynamics/deform.py
first_capture
¶
first_capture(data: DataArray, records: Sequence[int] | None) -> tuple[list[int], str]
The records an animation starts from, as (indices, note): the
first capture of a whole object that holds repeated captures (its
block labels), with a note saying how to pick another, and
otherwise records (every record for None) with no note. The first
step of animation_records, and run on its own before
quantity_records, which needs the capture chosen first.
Source code in src/visualdynamics/deform.py
quantity_records
¶
quantity_records(data: DataArray, records: Sequence[int] | None = None, quantity: str | None = None) -> tuple[list[int], str | None, int]
The records of one quantity, as (indices, quantity, other-quantity records dropped).
A deflection shows one quantity at a time: newtons and meters per
second squared do not share a scale, and an animation that moved
one node by an acceleration and the next by a force would show
neither. A record's quantity is its response quantity
(core.report.base_quantity), so an FRF of acceleration per force
is acceleration. When quantity is None it is the most common one
among records, the first of them on a tie; a quantity no record measures gives no indices.
The order of records is kept, so animation_records still keeps
the first record of each DOF.
Source code in src/visualdynamics/deform.py
envelope_records
¶
envelope_records(data: DataArray, records: Sequence[int] | None = None, quantity: str | None = None) -> tuple[list[int], str | None, int, int]
Which records a PSD envelope shows, as (indices, quantity, cross records dropped, other-quantity records dropped).
An envelope is drawn from the autospectra: a cross record's phase
belongs to an operating deflection shape, so cross records are
dropped first. Then only one quantity is kept, since newtons and
meters per second squared cannot share a normalization; when
quantity is None it is the most common one among the remaining
records. Both filters run before the one-record-per-DOF rule in
animation_records, or a cross record or a drive point's force PSD
listed first would take the place of that DOF's accelerometer
autospectrum. The app and the headless call both use this function.
Source code in src/visualdynamics/deform.py
dof_directions
¶
dof_directions(geometry: Geometry, dofs: Sequence[str]) -> tuple[ndarray, ndarray, ndarray]
(rows, directions, used) for the DOFs this geometry can move.
rows indexes into the geometry's nodes, directions is the global
unit vector each DOF displaces along, and used is a mask over
dofs of the ones kept. Rotational DOFs, DOFs that do not parse,
and DOFs on nodes the geometry does not have are dropped.
Source code in src/visualdynamics/deform.py
node_displacements
¶
node_displacements(geometry: Geometry, dofs: Sequence[str], values: ArrayLike) -> ndarray
The (num_nodes, 3) displacement of every node from one value per DOF.
Values may be complex, and the result is then complex too. DOFs on the same node add, and nodes with no usable DOF are zero.
Source code in src/visualdynamics/deform.py
model_size
¶
model_size(geometry: Geometry) -> float
The length of the geometry's bounding-box diagonal, which deflections are scaled against; 1.0 for a geometry with no extent.
Source code in src/visualdynamics/deform.py
auto_scale
¶
auto_scale(geometry: Geometry, peak: float, fraction: float = 0.1) -> float
The scale that draws a deflection of peak at fraction of the
model size (model_size); 1.0 when peak is zero or not finite.
Source code in src/visualdynamics/deform.py
modal_deflection
¶
modal_deflection(geometry: Geometry, shapes: ShapeSet, data: DataArray, indices: Sequence[int]) -> ModalTimeDeflection
The modal records among indices played through shapes on
geometry: each record at a modal coordinate Mk of the set moves
the geometry by mode k's shape, scaled by its value at each sample.
The records at other DOFs (a force beside the modal responses computed from it) are not part of it, and a coordinate past the set's last mode is not one of its modes. A complex set is refused: its shapes are not real motion, and taking their real part would show something no record says.