visualdynamics.core.transform¶
transform
¶
Physical responses through a shape set to modal responses, and back.
u = Φq is the whole relationship (PLAN.md, "The virtual point arc",
phase 2). Physical to modal is q = Φ⁺u, the least-squares fit over
the DOFs the data and the set share; modal to physical is u = Φq,
exact by definition. Any shape set serves — a fitted set, an
eigensolution, the six rigid-body shapes of a geometry, which is the
virtual point transformation of the substructuring literature with
no special case in the code.
Every kind of data goes through the same two rules, in the form its
kind needs. A time history or a spectrum is rows, one per channel:
q = Φ⁺u line by line, complex where the rows are. A cross-spectral
matrix is a quadratic form, S_qq = Φ⁺ S_uu Φ⁺ᴴ, so it needs every
cross term between the shared channels — the phase between two
channels is exactly what tells a translation from a rotation — and a
set of autospectra alone is refused rather than completed with a
guess (Brandon, 2026-09-04: no assumptions about cross terms; the
user defines or computes them when they are needed). An FRF is a
matrix from references to responses: its response rows transform
through Φ⁺ like any response, and its reference columns through the
force rule, f = (Φᵀ)⁺ f_m, when the set covers the drives — the
virtual point's FRF in both halves; references the set does not cover
stay physical, and the report says so. A shock response spectrum, a
coherence and a sine level set do not transform at all — a maximum, a
ratio, a magnitude without phase — and the refusal says to transform
the time history and recompute.
Records match shape columns by (DOF, quantity), with the sign
honoured: a 101Z- channel against a 101Z+ column negates. Every
column is a displacement quantity today; matching by quantity as well
is what lets a later set carry strain at gauge DOFs beside
displacement at accelerometer DOFs and slot into the same transform.
The quantity rule, three classes. Motions (acceleration,
velocity, displacement) transform as responses, q = Φ⁺u, each
quantity group with its own rule and unit. Forces transform the other
way — a force is work-conjugate to a motion, so the modal force is
Φᵀf, no pseudo-inverse and no rank condition. Everything else
(temperature, voltage, pressure, strain) is left out and said: a
displacement shape set says nothing about it, and a temperature at a
virtual point would be a fiction.
The unit rule is [q] = [u]/[Φ]. Unit rigid-body shapes have
dimensionless translations and length-per-radian rotations, so the
translational responses keep the data's quantity and the rotational
ones come out per radian (rad/s², or lbf·in for a force). A
mass-normalised set is in 1/√kg, so its modal responses carry a half
power of mass — the modal_* dimension tags. A set with no declared
mass unit gives responses with no unit, hinted with the modal tag for
Define Units to declare. The expansion applies the same rule the
other way, which is what makes the round trip honest.
A specification's bands carry through exactly when they can. The transform is linear, so a band that is the same decibels on every channel at a line is the same decibels on every modal channel there; bands that differ between channels have no single answer, and the transform refuses rather than pick one.
Modal DOF names. A modal coordinate is spelled M and the
mode's 1-based index — M1 … M6 for a rigid set, M1 … M22 for a
fitted one — one rule for every set (validate.modal_coordinate).
Visibly not a node, and stable across renames; which set it belongs
to is the object's provenance and each record's comment, which names
the mode as the set describes it.
Classes:
| Name | Description |
|---|---|
TransformReport |
What a transform used, left out, and left unexplained. |
Functions:
| Name | Description |
|---|---|
modal_dofs |
The DOF names a set's modal coordinates take: |
reads_as |
|
to_modal |
Physical responses through the set: modal responses at the |
to_physical |
Modal responses back through the set: |
carried_modes |
The modal DOFs an expansion carried, in mode order — empty when |
Classes¶
TransformReport
dataclass
¶
TransformReport(
shared: dict[str, list[str]] = dict(),
dropped: list[str] = list(),
skipped: dict[str, int] = dict(),
rank: dict[str, tuple[int, int]] = dict(),
residual: dict[str, float] = dict(),
notes: list[str] = list(),
)
What a transform used, left out, and left unexplained.
Attributes:
shared: The physical DOFs matched to shape columns, per
quantity.
dropped: The DOFs of transformable quantities the set has no
column for — a shaker's synthetic drive DOF, a node the
set never covered.
skipped: How many records of each untransformable quantity
were left out ({'temperature': 2}).
rank: The rank of the shared shape matrix per motion quantity,
against the number of modes.
residual: Per shared motion DOF, the fraction of its RMS (or
of its power, for a density) the modes do not explain — 0
when the fit is exact.
notes: Anything else worth a sentence — references kept
physical, cross-quantity blocks left out.
Methods:
| Name | Description |
|---|---|
describe |
One line: what was shared, dropped, skipped, and left |
Attributes:
| Name | Type | Description |
|---|---|---|
worst_residual |
float
|
The largest per-DOF residual fraction, or 0 with none. |
Attributes¶
worst_residual
property
¶
The largest per-DOF residual fraction, or 0 with none.
Methods:¶
describe
¶
One line: what was shared, dropped, skipped, and left unexplained — the status line's wording.
Source code in src/visualdynamics/core/transform.py
Functions:¶
modal_dofs
¶
modal_dofs(shapes: ShapeSet) -> list[str]
The DOF names a set's modal coordinates take: M1 … Mn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shapes
|
ShapeSet
|
The set. |
required |
Returns:
| Type | Description |
|---|---|
list of str
|
One DOF string per mode, in mode order. |
Source code in src/visualdynamics/core/transform.py
reads_as
¶
'physical' when the data's DOFs are on the set's coordinates,
'modal' when they are the set's modal names, None when neither
— or when the kind of data does not transform at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataArray
|
The object to read. |
required |
shapes
|
ShapeSet
|
The set to read it against. |
required |
Returns:
| Type | Description |
|---|---|
str or None
|
Which way a transform would go. |
Source code in src/visualdynamics/core/transform.py
to_modal
¶
to_modal(
data: DataArray,
shapes: ShapeSet,
records: Sequence[int] | None = None,
) -> tuple[DataArray, TransformReport]
Physical responses through the set: modal responses at the
set's modal DOFs M1 … Mn, in the form the data's kind takes —
rows for a time history or spectrum, the matrix for a density or
specification, both halves for an FRF.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataArray
|
The physical record. Motions are fitted, forces projected, anything else left out and reported. A density needs every cross term between the shared channels; an SRS or a coherence does not transform at all. |
required |
shapes
|
ShapeSet
|
The set to transform through. Complex sets are refused: real data cannot carry a complex modal coordinate. |
required |
records
|
sequence of int
|
Which records to carry through — the channels picked in the tree. All of them when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
tuple of (DataArray, TransformReport)
|
The modal responses, of the source's own class, and what was shared, dropped and left unexplained. |
Source code in src/visualdynamics/core/transform.py
to_physical
¶
to_physical(
data: DataArray,
shapes: ShapeSet,
records: Sequence[int] | None = None,
) -> tuple[DataArray, TransformReport]
Modal responses back through the set: u = Φq at every DOF the
set covers, summed over the modes present, in the form the data's
kind takes.
Every mode gives the motion of the structure; a few — one record picked in the tree — give those modes' contribution to it, which is what a modal contribution plot is. The result's comments say which modes it carries. Modal forces are left out: many force distributions give one modal force.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataArray
|
The modal record, its DOFs the set's modal names. |
required |
shapes
|
ShapeSet
|
The set the record was transformed through. |
required |
records
|
sequence of int
|
Which modal records to expand — the modes picked in the tree. All of them when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
tuple of (DataArray, TransformReport)
|
The physical responses, and which modal DOFs were used. |
Source code in src/visualdynamics/core/transform.py
carried_modes
¶
carried_modes(
report: TransformReport, shapes: ShapeSet
) -> list[str]
The modal DOFs an expansion carried, in mode order — empty when it carried every mode, so a name need only say so when it matters.