visualdynamics.deform¶
deform
¶
Turning data at DOFs into node motion.
Both mode shape animation and time history playback reduce to 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.
Everything expensive is done once, here, so playback is a scalar multiply and a scatter write:
- direction signs and coordinate-system rotations are baked into a unit vector per DOF
- only the nodes that actually move are touched per frame
- DOFs are grouped into passes with no repeated node, so a frame is a handful of vectorized writes rather than a scatter-add
Rotational DOFs are ignored: a rotation does not displace the node it belongs to. Nodes with no data hold still.
Classes:
| Name | Description |
|---|---|
Deflection |
Node offsets as a function of one parameter (phase, or sample index). |
ShapeDeflection |
One mode: offsets sweep with phase, |
OdsDeflection |
An operating deflection shape read off complex spectra. |
EnvelopeDeflection |
The envelope a PSD names at one movable line. |
TimeDeflection |
Records over time: offsets are the samples at one index. |
Functions:
| Name | Description |
|---|---|
animation_records |
Which records a deflection animation shows, and why, as (indices, note). |
envelope_records |
Which records an envelope may honestly show, one quantity at a |
dof_directions |
(rows, directions, used) for DOFs this geometry can move. |
node_displacements |
(num_nodes, 3) displacement from one value per DOF. |
model_size |
Bounding-box diagonal, the yardstick deflections are scaled against. |
auto_scale |
Scale putting the largest deflection at |
Classes¶
Deflection
¶
Node offsets as a function of one parameter (phase, or sample index).
Subclasses expose rows — the node rows that move — and offsets(p),
giving those rows' displacement. Everything else holds still, which is
what keeps playback independent of model size.
offsets returns a buffer it reuses on every call, so a frame allocates
nothing. Consume the result before calling again, or copy it.
Attributes:
| Name | Type | Description |
|---|---|---|
peak_magnitude |
float
|
The largest distance any node travels, over the whole animation. |
Attributes¶
peak_magnitude
property
¶
The largest distance any node travels, over the whole animation.
Not the same as peak, which is the largest value of a single DOF:
a node moving in both X and Z travels sqrt(x^2 + z^2), further than
either. This is what the top of a colour scale should mean, so that
the top colour appears at the one instant the model is at its
furthest and nowhere else.
ShapeDeflection
¶
ShapeDeflection(
geometry: Geometry,
dofs: Sequence[str],
shape: ArrayLike,
)
Bases: Deflection
One mode: offsets sweep with phase, Re(phi * exp(i*theta)).
Methods:
| Name | Description |
|---|---|
offsets |
|
Attributes:
| Name | Type | Description |
|---|---|---|
peak_magnitude |
float
|
Closed form, so no phase sweep is needed. |
Source code in src/visualdynamics/deform.py
Attributes¶
peak_magnitude
property
¶
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
¶
parameter is the phase 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 off complex spectra.
At one abscissa line the records' complex values are a deflection
pattern — magnitude and phase per DOF — and the animation sweeps it
exactly like a complex mode, Re(H(w_line) * exp(i*theta)). The
line is movable: the plot cursor picks which frequency deflects,
so moving it must cost one small bake, not a scene rebuild.
Every line deflects at full scale: the pattern is normalized by its own largest record, so an anti-resonance shows its shape as plainly as a peak. Keeping the true relative amplitude was tried first and read as broken — away from resonance the model barely moved, and what a frequency does is exactly what the cursor is there to ask. How much it responds is the plot's own curve, one glance away.
Methods:
| Name | Description |
|---|---|
offsets |
|
Attributes:
| Name | Type | Description |
|---|---|---|
line |
int
|
The abscissa index whose pattern is deflecting. |
strongest_line |
int
|
The line where a record is largest — where a cursor should |
peak |
float
|
1.0 — the yardstick the animator scales against. Every line |
peak_magnitude |
float
|
The furthest any node gets, over every line and phase, with |
Source code in src/visualdynamics/deform.py
Attributes¶
strongest_line
property
¶
The line where a record is largest — where a cursor should start, because a flat-spectrum line 0 deflects as nothing.
peak
property
¶
1.0 — the yardstick the animator scales against. Every line is normalized to its own strongest record, so the largest DOF value any line ever shows is exactly one.
peak_magnitude
property
¶
The furthest any node gets, over every line and phase, with each line at its normalized scale.
Per line the closed form is ShapeDeflection's; walking the lines in chunks keeps the working set bounded the way TimeDeflection's sample walk does.
Methods:¶
offsets
¶
parameter is the phase in radians.
EnvelopeDeflection
¶
EnvelopeDeflection(
geometry: Geometry,
dofs: Sequence[str],
ordinate: ArrayLike,
)
Bases: Deflection
The envelope a PSD names at one movable line.
An autospectrum gives each DOF an amplitude — the square root of the PSD value — and no phase and no sign. The honest picture is the envelope: every extreme every DOF reaches, with no claim about when. One of these deflects a copy of the geometry by +pattern; the animator mirrors a second copy to −pattern through the sign of its scale, and the pair is the envelope.
The line moves like OdsDeflection's and each line shows at its
own full scale, for the same reason. Colour is the one channel left
to carry level, so it is absolute: node_decibels reads each node
against the loudest node at any line, floored at FLOOR_DB.
A node measured along two axes deflects to their in-phase diagonal — two copies cannot show the four corners of the true rectangle — which is exact for single-axis surveys and stated in the guide for the rest.
Methods:
| Name | Description |
|---|---|
offsets |
The +pattern; |
node_decibels |
Each moving node's level, dB below the loudest node at any |
Attributes:
| Name | Type | Description |
|---|---|---|
line |
int
|
The abscissa index whose envelope is showing. |
strongest_line |
int
|
Where a record is largest — where the cursor starts. |
peak |
float
|
1.0 — every line is normalized to its own strongest record. |
peak_magnitude |
float
|
The furthest any node gets with each line at its normalized |
Source code in src/visualdynamics/deform.py
Attributes¶
peak_magnitude
property
¶
The furthest any node gets with each line at its normalized scale — unused when the animator pins an absolute colour range, but every deflection answers it.
Methods:¶
offsets
¶
The +pattern; parameter is unused — the line is the state,
and the sign lives in the animator's scale.
node_decibels
¶
Each moving node's level, dB below the loudest node at any line — the absolute reading the colour carries.
TimeDeflection
¶
TimeDeflection(
geometry: Geometry,
dofs: Sequence[str],
ordinate: ArrayLike,
)
Bases: Deflection
Records over time: offsets are the samples at one index.
Methods:
| Name | Description |
|---|---|
offsets |
|
Attributes:
| Name | Type | Description |
|---|---|---|
peak_magnitude |
float
|
Walk the samples in chunks, tracking the furthest any node gets. |
Source code in src/visualdynamics/deform.py
Attributes¶
peak_magnitude
property
¶
Walk the samples in chunks, tracking the furthest any node gets.
Chunked rather than all at once: the full (nodes, 3, samples) array would be far larger than the data it came from.
Methods:¶
offsets
¶
parameter is the sample index.
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, and why, as (indices, note).
An animation gives every node one trajectory, so it can use at most one record per DOF. Two rules follow:
- A whole object holding repeated captures animates its first one — all twenty averages summed into one waveform is not a measurement anyone took. The same convention as a shape set: the first, with a note saying how to pick another.
- Within what remains, the first record per DOF wins and the rest are dropped with a note. A drive point carries a load cell beside its accelerometer, and metres per second squared plus newtons is not a deflection. An FRF's reference columns repeat every response DOF the same way, so a whole FRF animates against its first reference. Incompatibility warns; it does not block.
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 an envelope may honestly show, one quantity at a time — (indices, quantity, cross records dropped, other-quantity records dropped).
The envelope is the autospectra's reading: a cross row's phase
belongs to an operating deflection shape, so cross records go
first. Then one quantity — newtons and metres per second squared
cannot share a normalization — with the commonest kind answering
when quantity is None. Both filters run before the
one-record-per-DOF rule in animation_records, or a cross row or
a drive point's force PSD listed first would shadow its own DOF's
genuine accelerometer auto. That ordering broke twice, which is
why the app and the headless call now share this one 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 DOFs this geometry can move.
rows indexes into the geometry's nodes, directions is the global
unit vector each DOF displaces along, and used masks which of the
given DOFs are usable — rotational, unknown, or absent-node DOFs are
dropped.
Source code in src/visualdynamics/deform.py
node_displacements
¶
node_displacements(
geometry: Geometry,
dofs: Sequence[str],
values: ArrayLike,
) -> ndarray
(num_nodes, 3) displacement from one value per DOF.
Values may be complex; the result matches. Contributions to the same node accumulate.
Source code in src/visualdynamics/deform.py
model_size
¶
model_size(geometry: Geometry) -> float
Bounding-box diagonal, the yardstick deflections are scaled against.