visualdynamics.core¶
core
¶
The data model: what a test holds, with no Qt anywhere.
Geometry, the data arrays, shapes, channel tables, and the computations over them. Everything here works in a script, in a notebook and in the app equally, because none of it knows which it is in — the app is a view onto these objects, not the place they live. The guide's The objects maps every kind — what it stores, where it comes from, what can be done with it — and points back here for the signatures.
Classes:
| Name | Description |
|---|---|
ChannelTable |
Per-channel metadata, one fixed column set, each column typed. |
DataArray |
Base class; use a concrete subclass (TimeHistory, Spectrum, Frf, Psd). |
Frf |
Frequency response function: response per unit reference. |
Psd |
Power spectral density: the declared unit is the engineering unit |
ShockSpecification |
What a shock test was controlled to: an SRS, and its band. |
Specification |
What a random vibration test was controlled to: a PSD, and its band. |
Spectrum |
A linear spectrum: amplitude and phase at each frequency line. |
Srs |
A shock response spectrum: the peak an oscillator reached. |
TimeHistory |
A measurement against time: the record as it was acquired. |
TransientSpecification |
What a transient test was controlled to: a target time history. |
Geometry |
Nodes, coordinate systems, tracelines, elements and blocks. |
ShapeSet |
Mode shapes over a shared set of DOFs. |
Classes¶
ChannelTable
¶
Per-channel metadata, one fixed column set, each column typed.
Attributes:
frame: The table, one row per channel, columns in SCHEMA
order. Every cell is text except channel and node,
which are integers — a serial number that happens to be all
digits is not a number, and a column read back as int64
would lose its leading zero and come out of a round trip a
different string.
Methods:
| Name | Description |
|---|---|
set_cell |
Write one cell; a value the column cannot hold is refused. |
units_for |
The units this channel could be in, given its declared type. |
delete_channels |
Remove the given rows in place; the last one is refused. |
orientation |
One channel's measured direction against a geometry. |
derived_cells |
The derived columns' cells for one row, as text: three |
dof_strings |
Each channel's degree of freedom, as '101Z+' strings. |
rename_dof |
Give the channel at coordinate |
roles |
Each channel's declared role, '' where undeclared. |
controls |
Which channels are control channels, as booleans. |
types |
What each channel measures, '' where undeclared. |
sensitivities |
mV per engineering unit, NaN where undeclared. |
ranges |
The instrumentation voltage limit, per channel. |
save |
Write the table to a file of its own. |
Attributes:
| Name | Type | Description |
|---|---|---|
num_channels |
int
|
How many channels the table describes — one per row. |
column_names |
list[str]
|
The table's column headings, in order. |
Source code in src/visualdynamics/core/channel_table.py
Attributes¶
Methods:¶
set_cell
¶
Write one cell; a value the column cannot hold is refused.
Strict where _typed is lenient: a person typing gets the
reason, where a file gets the benefit of the doubt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The column to write. |
required |
row
|
int
|
Which channel. |
required |
value
|
object
|
The value, refused if the column cannot hold it. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/channel_table.py
units_for
¶
The units this channel could be in, given its declared type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
int
|
Which channel. |
required |
Returns:
| Type | Description |
|---|---|
list of str
|
The units this channel could be in, given its declared type. |
Source code in src/visualdynamics/core/channel_table.py
delete_channels
¶
Remove the given rows in place; the last one is refused.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indices
|
sequence of int
|
Which rows to remove. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/channel_table.py
orientation
¶
One channel's measured direction against a geometry.
The DOF the row names — node and direction — read through the
frame that node is measured in (Geometry.dof_direction), as
a unit vector in the geometry's global system; the global axis
it is nearest, signed ('X+', 'Z-'); and the angle between the
two in degrees. What the derived columns show.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
int
|
The channel's row. |
required |
geometry
|
Geometry
|
The geometry the channel is measured on. |
required |
Returns:
| Type | Description |
|---|---|
tuple
|
(vector, axis, angle), each None when the geometry cannot place the channel — a node it lacks, no direction, or a node measured in a frame that turns with position. |
Source code in src/visualdynamics/core/channel_table.py
derived_cells
¶
The derived columns' cells for one row, as text: three components to three decimals, the nearest axis, the angle to a tenth of a degree; empty where the geometry cannot place the channel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
int
|
The channel's row. |
required |
geometry
|
Geometry
|
The geometry the channel is measured on. |
required |
Returns:
| Type | Description |
|---|---|
list of str
|
One cell per |
Source code in src/visualdynamics/core/channel_table.py
dof_strings
¶
Each channel's degree of freedom, as '101Z+' strings.
rename_dof
¶
Give the channel at coordinate old the coordinate new,
in place — its node and direction cells rewritten, since a DOF
string is those two concatenated. The same correction a data
array's rename_dof makes, for the table that names the
channels (Brandon, 2026-09-06), and the same rule: the channel
moves, not every channel at the point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old
|
str
|
The coordinate as the table has it, '101Z+'. |
required |
new
|
str
|
The coordinate to give it: a node number then a direction, normalized the way every DOF is, and refused when it is not one — a table row may lack a node or a direction, but a correction typed by a person is whole. |
required |
quantity
|
str
|
Which channel at |
None
|
Returns:
| Type | Description |
|---|---|
int
|
How many channels changed. |
Source code in src/visualdynamics/core/channel_table.py
roles
¶
controls
¶
types
¶
sensitivities
¶
mV per engineering unit, NaN where undeclared.
ranges
¶
save
¶
Write the table to a file of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
Where to write it. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/channel_table.py
DataArray
¶
DataArray(abscissa: ArrayLike, ordinate: ArrayLike, response_dof: str | Sequence[str], reference_dof: str | Sequence[str] | None = None, ordinate_dim: str | Sequence[str] | None = None, comment: str | Sequence[str] | None = None, ordinate_unit: str | Sequence[str | None] | None = None, reference_unit: str | Sequence[str | None] | None = None, dimension_hint: str | Sequence[str | None] | None = None, block: str | Sequence[str] | None = None)
Base class; use a concrete subclass (TimeHistory, Spectrum, Frf, Psd).
One object holds many records — 36 accelerometer channels, or the
2 592 FRFs of a 36-by-72 matrix — sharing one abscissa. Everything
that varies between records is a list of that length, so record i
is ordinate[i] measured at response_dof[i], and there is no
per-record object to go stale.
Values are stored in SI once their units are known. A record
whose units were never declared keeps the file's raw numbers and
reports ordinate_dim == 'unknown'; what the file said it was,
without saying its scale, is kept beside it in dimension_hint.
Attributes:
abscissa: The x axis, shared by every record — seconds for a time
history, hertz for anything in the frequency domain. Stored
as it arrived: uneven spacing and out-of-order samples are
both allowed, because both are real and refusing them at
the door would refuse real data. What needs an even step —
anything with an FFT under it — asks for one at the point of
use and says so when it cannot have it.
ordinate: (records, len(abscissa)). Complex where the subclass
says so (complex_ordinate), real otherwise.
response_dof: What each record was measured at, as a DOF string
('101X+'). One per record.
reference_dof: What each record was measured against, for the
types that need one — the shaker on an FRF, the other channel
of a cross spectrum. None where the type has no reference.
block: Which repeat of the same measurement each record is: an
average, a run, a shock. A short label, not a time.
ordinate_dim: The quantity each record measures
('acceleration', 'force'), or 'unknown' while its units
are undeclared.
ordinate_unit: The unit its values are in — always the SI one
while the dimension is known, since that is how they are
stored. None means undeclared.
reference_unit: The same for the reference of a ratio, so an FRF
record knows both halves of m/s²/N.
dimension_hint: What the file claimed a record measures without
saying at what scale. Nothing is ever scaled by a hint: it
narrows the units offered, labels an axis, and survives
export.
comment: Free text per record, as the source file carried it.
Methods:
| Name | Description |
|---|---|
known_dim |
What quantity record |
rename_dof |
Give a channel's coordinate a new name, in place. |
delete_records |
Remove records in place — by index, by DOF, or by capture. |
define_units |
Declare what the ordinate values are in, converting them to SI. |
undefine_units |
Take a declaration back, restoring the file's raw values. |
column_keys |
What tells one record from another besides its response. |
record_label |
A short label for one record, for a legend or an axis. |
record_pair |
The (response, reference) record |
log_scaled |
Whether this object's magnitude reads on a log axis. |
display_abscissa |
The abscissa converted into a unit system's own units. |
display_ordinate |
Ordinate in display units; undefined records pass through as-is. |
display_blocks |
|
save |
Write this object to a |
plot |
Draw every record on one set of axes. |
save_plot |
Draw the records and write the figure to |
plot_waterfall |
The records spread along a depth axis, colored by level — |
Attributes:
| Name | Type | Description |
|---|---|---|
num_records |
int
|
How many records this object holds. One measurement per |
units_defined |
bool
|
Whether every record knows what it measures. False while |
undefined_records |
list[int]
|
Which records still have no declared dimension — the ones |
Source code in src/visualdynamics/core/data.py
Attributes¶
num_records
property
¶
How many records this object holds. One measurement per record, all sharing the object's single abscissa.
units_defined
property
¶
Whether every record knows what it measures. False while any record is still in the file's own unconverted numbers.
undefined_records
property
¶
Which records still have no declared dimension — the ones
holding the file's raw numbers, awaiting define_units.
Methods:¶
known_dim
¶
What quantity record i holds, whether or not its unit is known.
The dimension when the units are defined, otherwise the source's claim, otherwise 'unknown'. Never use this to scale anything — a hinted record's values are raw, and the scale is exactly what is missing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i
|
int
|
Which record. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The quantity the record holds, falling back to its dimension hint when the unit is undeclared. |
Source code in src/visualdynamics/core/data.py
rename_dof
¶
Give a channel's coordinate a new name, in place.
A channel is a coordinate and a quantity — a drive point carries a load cell and an accelerometer at one DOF — and the rename is the channel's: a force labeled at the wrong node moves without taking the accelerometer at that node with it, and the other is changed explicitly if it should be (Brandon, 2026-09-06). The channel moves wherever a record wears it, as a response and as a reference alike: a CPSD's accelerometer is on both sides of its cross terms and is one sensor. A rename that would give two records one identity — two accelerometers at one point — is refused, where it would have made two rows of the grid into one and hidden a record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old
|
str
|
The coordinate as it is, '101Z+'. |
required |
new
|
str
|
The coordinate to give it; normalized the way every DOF is ('101Z' is '101Z+'), and refused when it is not one. |
required |
quantity
|
str
|
Which channel at |
None
|
Returns:
| Type | Description |
|---|---|
int
|
How many records changed, counting a response and a reference on one record separately. |
Source code in src/visualdynamics/core/data.py
delete_records
¶
delete_records(indices: Sequence[int] | None = None, *, dof: str | Sequence[str] | None = None, dim: str | Sequence[str] | None = None, reference: str | Sequence[str] | None = None, capture: int | Sequence[int] | None = None) -> None
Remove records in place — by index, by DOF, or by capture.
Everything a record owns goes with it: its row of the ordinate, its DOFs, units, comment, hint, block — and, on a specification, its limit curves, which would otherwise silently belong to the wrong channels. Removing the last record is refused: an empty data array is not a state anything else here can show.
dof and capture are the selectors a person means — "drop
channel 101Z+", "drop the third run" — where indices are the
machine's (Brandon, 2026-08-30, reading thirteen indices in the
journal where one capture number would have said it). They
combine as an intersection, and either combines with explicit
indices as a union.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indices
|
sequence of int
|
Which records to remove, by position. |
None
|
dof
|
str or sequence of str
|
Remove every record at these response DOFs. A drive point
carries two records at one DOF — a force and an
acceleration — and the DOF alone takes both; |
None
|
dim
|
str or sequence of str
|
Restrict to these quantities ('force', 'acceleration', …) — the other half of a channel's identity. |
None
|
reference
|
str or sequence of str
|
Remove every record at these reference DOFs — a column of
an FRF matrix, where |
None
|
capture
|
int or sequence of int
|
Remove these captures — each channel's n-th playing, the
numbering |
None
|
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/data.py
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 | |
define_units
¶
define_units(units: str | Sequence[str | None], reference_units: str | Sequence[str | None] | None = None) -> DataArray
Declare what the ordinate values are in, converting them to SI.
units is a single unit applied to every record, a sequence with one
entry per record, or a {record index: unit} mapping to set only some.
Entries of None leave a record's units undefined. Records that already
have units are reinterpreted, not re-scaled twice.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
units
|
str or sequence of str
|
The unit each record's values are in; one string applies to every record. |
required |
reference_units
|
str or sequence of str
|
The denominator unit, for records that have one. |
None
|
Returns:
| Type | Description |
|---|---|
DataArray
|
Self, converted to SI in place. |
Source code in src/visualdynamics/core/data.py
undefine_units
¶
undefine_units(records: Sequence[int] | None = None) -> DataArray
Take a declaration back, restoring the file's raw values.
The inverse of define_units. A wrong guess should be correctable
without reimporting, and that means being able to withdraw one, not
only to replace it — there is no unit string meaning 'I no longer
know'.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
records
|
sequence of int
|
Which records to revert. All of them when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
DataArray
|
Self, with the file's raw values restored. |
Source code in src/visualdynamics/core/data.py
column_keys
¶
What tells one record from another besides its response.
The reference DOF when records are a matrix of measurements, the block when they are the same measurement repeated, None when the response alone is the whole identity. This is what decides whether an object expands into a grid.
Source code in src/visualdynamics/core/data.py
record_label
¶
A short label for one record, for a legend or an axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i
|
int
|
Which record. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The record's DOF, plus whatever tells it from its neighbors. |
Source code in src/visualdynamics/core/data.py
record_pair
¶
The (response, reference) record i is between.
A record with no reference is an autospectrum — a channel against itself — so it pairs with the diagonal, which is what a specification bounds. The one reading of that rule: the plot, the table beside it and the report all ask here, so their labels cannot disagree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i
|
int
|
The record. |
required |
Returns:
| Type | Description |
|---|---|
tuple of str
|
Response DOF, reference DOF. |
Source code in src/visualdynamics/core/data.py
log_scaled
¶
Whether this object's magnitude reads on a log axis.
Logarithmic for frequency-domain data unless the class pins it
(log_ordinate — a coherence is a 0..1 ratio and says nothing on
a log axis). The object answers so the 2-D plot and the 3-D
waterfall read one rule and cannot disagree about its axis.
Source code in src/visualdynamics/core/data.py
display_abscissa
¶
display_abscissa(unit_system: UnitSystem) -> ndarray
The abscissa converted into a unit system's own units.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit_system
|
UnitSystem
|
The units to present in. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The abscissa in display units. |
Source code in src/visualdynamics/core/data.py
display_ordinate
¶
display_ordinate(unit_system: UnitSystem, records: Iterable[int] | None = None) -> ndarray
Ordinate in display units; undefined records pass through as-is.
records limits the work to the ones asked for. A 1356-record FRF
has one or two distinct dimensions in it, so the conversion is
gathered per dimension and applied to a whole block at once —
converting row by row meant a unit lookup per record, which is how
drawing a single curve came to cost 2713 trips through pint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit_system
|
UnitSystem
|
The units to present the values in. |
required |
records
|
iterable of int
|
Which records to convert. All of them when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The values in display units. Records with undefined units pass through untouched. |
Source code in src/visualdynamics/core/data.py
display_blocks
¶
display_blocks(unit_system: UnitSystem, records: Iterable[int] | None = None, block_bytes: int | None = None)
display_ordinate a block of records at a time.
Yields (start, stop, values): positions within records and
the converted rows for them, each block at most block_bytes
(DISPLAY_BLOCK_BYTES by default) — so a reading that thins
every record before it draws it, the 3-D stage above all,
never holds a converted copy of the whole object. A record
larger than the block is still one block: a row is not split.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit_system
|
UnitSystem
|
The units to present the values in. |
required |
records
|
iterable of int
|
Which records to convert. All of them when omitted. |
None
|
block_bytes
|
int
|
The most a block may hold, in bytes. |
None
|
Yields:
| Type | Description |
|---|---|
tuple of (int, int, numpy.ndarray)
|
The block's positions among |
Source code in src/visualdynamics/core/data.py
save
¶
Write this object to a .vdyn file of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
Where to write it. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/data.py
plot
¶
plot(unit_system: UnitSystem | None = None, **kwargs: Any) -> Any
Draw every record on one set of axes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit_system
|
UnitSystem
|
Units to draw in. |
None
|
**kwargs
|
Any
|
Passed through to the plotting layer. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/data.py
save_plot
¶
save_plot(path: str | PathLike, unit_system: UnitSystem | None = None, **kwargs: Any) -> Any
Draw the records and write the figure to path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
Where to write the image. |
required |
unit_system
|
UnitSystem
|
Units to draw in. |
None
|
**kwargs
|
Any
|
Passed through to the plotting layer. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/data.py
plot_waterfall
¶
The records spread along a depth axis, colored by level —
the plot bar's 3-D reading, scripted. screenshot= renders
headless to a file; without it a window of the app's own 3-D
pane opens.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
records
|
sequence of int
|
Which records to stage. All of them when omitted. |
None
|
**kwargs
|
Any
|
Passed through to the scene. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/data.py
Frf
¶
Frf(abscissa: ArrayLike, ordinate: ArrayLike, response_dof: str | Sequence[str], reference_dof: str | Sequence[str] | None = None, ordinate_dim: str | Sequence[str] | None = None, comment: str | Sequence[str] | None = None, ordinate_unit: str | Sequence[str | None] | None = None, reference_unit: str | Sequence[str | None] | None = None, dimension_hint: str | Sequence[str | None] | None = None, block: str | Sequence[str] | None = None)
Bases: DataArray
Frequency response function: response per unit reference.
Methods:
| Name | Description |
|---|---|
plot_cmif |
The CMIF the fitting screen draws; with |
animate |
The operating deflection shape at one frequency line, moving |
Source code in src/visualdynamics/core/data.py
Methods:¶
plot_cmif
¶
The CMIF the fitting screen draws; with shapes the modal
model's synthesis is drawn dashed over the measurement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shapes
|
ShapeSet
|
A modal fit, drawn as the synthesized CMIF over the measured one. |
None
|
**kwargs
|
Any
|
Passed through to the plotting layer. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/data.py
animate
¶
The operating deflection shape at one frequency line, moving
on a geometry as the GUI animates it. Defaults to the strongest
line; frequency picks another.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry
|
Geometry
|
The geometry to move. |
required |
frequency
|
float
|
Which frequency line. The strongest when omitted. |
None
|
**kwargs
|
Any
|
Passed through to the scene. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/data.py
Psd
¶
Bases: DataArray
Power spectral density: the declared unit is the engineering unit whose square-per-Hz the values are in (declare 'g' for g^2/Hz).
Held complex because a cross spectrum is complex — the phase between two channels is most of what a CPSD is for. An autospectrum is not: a channel against itself is a magnitude squared, real by construction. So the type allows complex and the object stores what it actually has, which for a specification or a set of ASDs is a real array of half the size.
Methods:
| Name | Description |
|---|---|
principal_shapes |
The dominant shape of the cross-spectral matrix at each line, |
animate |
This set on a geometry, as the GUI shows it. |
area |
The area under one record, over a band or over all of it. |
areas |
|
written |
Which lines say something: finite, and for a requirement |
extent |
(low, high) this spectrum speaks for, in hertz. |
bin_edges |
The edges a step plot of this spectrum lands on: one more |
to_octave |
This spectrum integrated onto proportional bands. |
bin_widths |
The width of every line's own bin. |
bin_bounds |
(left, right) of every line's own bin — see |
Source code in src/visualdynamics/core/data.py
Methods:¶
principal_shapes
¶
The dominant shape of the cross-spectral matrix at each line, as (dofs, shapes (channels x lines), quantity).
The channels of one quantity form a square Hermitian matrix per
line; its largest eigenvalue's eigenvector, scaled by the square
root of that eigenvalue, is the principal operating deflection
shape — the direction of the output spectra's own CMIF, with
each channel's phase relative to the others and no reference to
choose. quantity picks which channels (the commonest when not
told). Refuses a set with no cross records — an autospectrum set
has no phase and its reading is the envelope — and an incomplete
block, whose eigenvectors would be shapes of holes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
quantity
|
str
|
Which quantity, for a mixed object. |
None
|
Returns:
| Type | Description |
|---|---|
tuple of (list of str, numpy.ndarray, str)
|
The DOF labels, the dominant shape at each line, and the quantity they are in. |
Source code in src/visualdynamics/core/data.py
2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 | |
animate
¶
animate(geometry: Any, frequency: float | None = None, quantity: str | None = None, **kwargs: Any) -> Any
This set on a geometry, as the GUI shows it.
A CPSD — cross records present — animates its principal
operating deflection shape: the dominant eigenvector of the
cross-spectral matrix per line, each channel's phase relative
to the others. An autospectrum set has no phase, so it shows
the envelope instead: two copies deflected ±sqrt(PSD), color
reading dB below the loudest node at any line. Defaults to the
strongest line; frequency picks another, quantity which
measurement deflects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry
|
Geometry
|
The geometry to move. |
required |
frequency
|
float
|
Which frequency line. |
None
|
quantity
|
str
|
Which quantity, for a mixed object. |
None
|
**kwargs
|
Any
|
Passed through to the scene. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/data.py
area
¶
The area under one record, over a band or over all of it.
The one integral. Whichever way this spectrum is read, it is read the same way here as it is drawn — that is what the field above is for, and why nothing outside this method chooses.
Units are the ordinate's times frequency, so the square root of it is an RMS for a PSD.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
int
|
Which record. |
0
|
low
|
float
|
The band to integrate over. |
None
|
high
|
float
|
The band to integrate over. |
None
|
Returns:
| Type | Description |
|---|---|
float
|
The area beneath the curve — the mean square, whose root is the RMS. |
Source code in src/visualdynamics/core/data.py
areas
¶
area over several bands at once: the comparison is
judged cell by cell, and a thousand cells must not cost a
thousand passes over the lines.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
int
|
Which record. |
required |
lows
|
array - like
|
The bands' edges, paired. |
required |
highs
|
array - like
|
The bands' edges, paired. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
One area per band; NaN where nothing is written. |
Source code in src/visualdynamics/core/data.py
written
¶
Which lines say something: finite, and for a requirement positive — a controller writes zero or NaN on every line it did not control, and those lines are not a requirement of nothing. Over one record, or any record when none is named.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
int
|
The record; every record when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
A boolean per line. |
Source code in src/visualdynamics/core/data.py
extent
¶
(low, high) this spectrum speaks for, in hertz.
For a density per bin, the outer edges of the written bins — a line stands for its whole bin. For a curve between breakpoints, the first and last written points. What octave banding clips its end bands to, and what a comparison is judged over (Brandon, 2026-09-19): a controller's target is stored on every FFT line to Nyquist and speaks only where it is written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
int
|
The record; the union over every record when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
tuple of float, or None
|
None when nothing is written above zero hertz. |
Source code in src/visualdynamics/core/data.py
bin_edges
¶
The edges a step plot of this spectrum lands on: one more
than there are lines, bin_bounds tiled.
to_octave
¶
to_octave(per_octave: int | None = None, low: float | None = None, high: float | None = None) -> Psd
This spectrum integrated onto proportional bands.
An integration, not a resampling: each band takes the mean-square content that falls in it, divided by its own width, so the area under the spectrum — and therefore the RMS it carries — is unchanged. Reading the narrowband curve at each band center would throw away everything between the centers.
The band grid is absolute (see visualdynamics.core.octave), so this
needs no specification to be told about: the frequency range
only chooses which bands of the one fixed grid come back, and
two runs banded the same way land on the same bands whatever
their ranges were.
Cross terms come through complex, which is what makes this work for a CPSD as well as a PSD.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
per_octave
|
int
|
Bands per octave. |
None
|
low
|
float
|
The band to cover. |
None
|
high
|
float
|
The band to cover. |
None
|
Returns:
| Type | Description |
|---|---|
Psd
|
The same power, arranged on proportional bands. |
Source code in src/visualdynamics/core/data.py
bin_widths
¶
The width of every line's own bin.
Its own when it has one, the midpoints between neighbors when it does not — so a caller integrating a spectrum never has to ask which kind it is holding.
Source code in src/visualdynamics/core/data.py
bin_bounds
¶
(left, right) of every line's own bin — see octave.bin_bounds.
Source code in src/visualdynamics/core/data.py
ShockSpecification
¶
ShockSpecification(*args: Any, warning_lower: ArrayLike | None = None, warning_upper: ArrayLike | None = None, abort_lower: ArrayLike | None = None, abort_upper: ArrayLike | None = None, **kwargs: Any)
What a shock test was controlled to: an SRS, and its band.
The same relationship a Specification has to a Psd. A shock
target is written as a required SRS with tolerance either side of
it — conventionally +6 dB and -3 dB, which is a factor of 2 up and
0.707 down — and the limits are those curves.
Written at one Q, and read at that Q: a target quoted at Q = 10 says nothing about what the same shock does to a Q = 50 oscillator, so the amplification travels with the target the way it travels with a measurement.
Source code in src/visualdynamics/core/data.py
Specification
¶
Specification(*args: Any, warning_lower: ArrayLike | None = None, warning_upper: ArrayLike | None = None, abort_lower: ArrayLike | None = None, abort_upper: ArrayLike | None = None, **kwargs: Any)
What a random vibration test was controlled to: a PSD, and its band.
A specification is a PSD in every respect — same abscissa, same
quantity, same conversions — with the four limit curves Bounded
carries. So it inherits both, rather than reimplementing either.
Methods:
| Name | Description |
|---|---|
reading_of |
How a specification written at these frequencies reads: |
written |
Which lines the requirement is written on: finite and |
to_octave |
This specification integrated onto proportional bands, its |
Source code in src/visualdynamics/core/data.py
Methods:¶
reading_of
staticmethod
¶
How a specification written at these frequencies reads: 'bin', a density per line, for many lines on an even grid — a controller's target on its FFT lines — and 'log_log', breakpoints of a power law, for a few unevenly spaced points (Brandon, 2026-09-19: "breakpoint specifications have very few frequency lines and I would expect an uneven spacing of them"). Both octave-band and narrowband specifications step; only a breakpoint curve is drawn as the law between its points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequencies
|
array_like
|
The specification's frequency lines, ascending. |
required |
Returns:
| Type | Description |
|---|---|
str
|
'bin' or 'log_log'. |
Source code in src/visualdynamics/core/data.py
written
¶
Which lines the requirement is written on: finite and positive. A controller writes zero on the lines it did not control, and zero is not a requirement of silence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
int
|
The record; every record when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
A boolean per line. |
Source code in src/visualdynamics/core/data.py
to_octave
¶
to_octave(per_octave: int | None = None, low: float | None = None, high: float | None = None) -> Specification
This specification integrated onto proportional bands, its warning and abort limits with it.
The same area rule as Psd.to_octave, read the way the object
is: a specification computed on lines is a density and each
line is a bin; one written at breakpoints is a power law
between them, and each band takes the exact area under that law
(compliance.log_log_area) over the part of the band the
specification covers, divided by the band's width. The limits
are curves written the same way and go through the same rule,
so a band's warning line stands in the same relation to its
target as the breakpoints did. Cross terms of a breakpoint
specification are not power laws (a phase is not), so they are
read onto a fine log grid the way the authoring sheet reads
them — magnitude log–log, phase straight — and integrated there.
The result is a density per band (interpolation 'bin', the
bands' widths carried), which is what a banded measurement is
compared against band for band. Brandon, 2026-09-18: the
specification and the measurement it judges should be
convertible alike, so a compliance can be read on bands at both
ends — reversing the earlier reasoning (PLAN.md, "Octave
bands") that a written curve needed no banding because the
comparison integrates it exactly; it does, and a banded
specification is still the object a person asks to see and
hand on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
per_octave
|
int
|
Bands per octave. |
None
|
low
|
float
|
The band to cover. |
None
|
high
|
float
|
The band to cover. |
None
|
Returns:
| Type | Description |
|---|---|
Specification
|
The same power and the same limits, arranged on proportional bands. |
Source code in src/visualdynamics/core/data.py
3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 | |
Spectrum
¶
Spectrum(abscissa: ArrayLike, ordinate: ArrayLike, response_dof: str | Sequence[str], reference_dof: str | Sequence[str] | None = None, ordinate_dim: str | Sequence[str] | None = None, comment: str | Sequence[str] | None = None, ordinate_unit: str | Sequence[str | None] | None = None, reference_unit: str | Sequence[str | None] | None = None, dimension_hint: str | Sequence[str | None] | None = None, block: str | Sequence[str] | None = None)
Bases: DataArray
A linear spectrum: amplitude and phase at each frequency line.
The complex average of a record's frames, not a power average — so
content whose phase is random frame to frame averages toward zero,
which is what makes this the wrong reading for burst random and the
right one for a deterministic signal. A Psd is the power average
and does not have that property.
Methods:
| Name | Description |
|---|---|
animate |
The operating deflection shape at one frequency line, moving |
Source code in src/visualdynamics/core/data.py
Methods:¶
animate
¶
The operating deflection shape at one frequency line, moving
on a geometry as the GUI animates it. Defaults to the strongest
line; frequency picks another.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry
|
Geometry
|
The geometry to move. |
required |
frequency
|
float
|
Which frequency line. The strongest when omitted. |
None
|
**kwargs
|
Any
|
Passed through to the scene. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/data.py
Srs
¶
Bases: DataArray
A shock response spectrum: the peak an oscillator reached.
Not a spectrum of the shock. Every point is the largest response a single-degree-of-freedom oscillator of that natural frequency ever reached while its base was shaken by the measured transient — one number out of one whole run of one filter. Two shocks with the same SRS can look nothing alike, and an SRS cannot be turned back into a time history, because the phase that produced each peak is gone.
The abscissa is the oscillator's natural frequency, not a frequency
in the shock — laid out in decades (log_abscissa): an SRS is
specified at octave-spaced natural frequencies, and drawn linear
the bottom five octaves crush into the left margin. The ordinate is in the quantity the base was measured
in — an acceleration transient gives an acceleration SRS.
q and kind change what the curve means, so they belong to the
object rather than to whoever happened to compute it: an SRS at
Q = 10 and the same shock at Q = 50 are different curves, and a
maximax reading is not a positive one. See visualdynamics.core.srs.
Attributes:
| Name | Type | Description |
|---|---|---|
damping |
float
|
The damping ratio the amplification factor means. |
Source code in src/visualdynamics/core/data.py
TimeHistory
¶
TimeHistory(abscissa: ArrayLike, ordinate: ArrayLike, response_dof: str | Sequence[str], reference_dof: str | Sequence[str] | None = None, ordinate_dim: str | Sequence[str] | None = None, comment: str | Sequence[str] | None = None, ordinate_unit: str | Sequence[str | None] | None = None, reference_unit: str | Sequence[str | None] | None = None, dimension_hint: str | Sequence[str | None] | None = None, block: str | Sequence[str] | None = None)
Bases: DataArray
A measurement against time: the record as it was acquired.
Everything else in a random or shock test is derived from one of
these, and the deriving is here — spectra, PSDs, the full CPSD
matrix, multiple coherence, shock response spectra. Two things ride
along that say how to read it: averaging, the frames a spectrum
is averaged over, and shocks, the events an SRS is computed from.
Both are the app's two views of a trace, and both are stored on the
history rather than passed at the call, so a PSD and the coherence
beside it cannot describe different measurements.
Methods:
| Name | Description |
|---|---|
compute_spectra |
The averaged spectrum of every channel — sdynpy's convention. |
channel_key |
What makes record |
suggest_averaging |
Averaging parameters worked out from the record itself. |
capture_indices |
Which playing each record is: 0 for a channel's first |
suggest_truncation |
The whole record — the only neutral span. |
truncate |
This record cut to a span ( |
suggest_filtering |
A starting low-pass: a tenth of the sample rate, order 4. |
filter |
This record through its filter ( |
integrate |
One integration — acceleration to velocity, velocity to |
differentiate |
One differentiation — displacement to velocity, velocity to |
srs_windows |
The stretches an SRS of this record would read, settled. |
srs_band |
(low, high) in Hz: the band these windows can support. |
compute_srs |
A shock response spectrum for every channel of every shock. |
compute_psds |
One-sided auto-power spectral density per channel, averaged |
psd_type |
What a PSD of this history is. |
srs_type |
What an SRS of this history is — see |
compute_cpsds |
The full cross-spectral density matrix, averaged across the |
drive_dofs |
The DOFs this history looks like it was driven at. |
compute_frfs |
The frequency response functions, one per response/drive pair. |
compute_multiple_coherence |
How much of each response the drives together account for. |
to_sep005 |
This record as SEP 005 timeseries — the sdypy ecosystem's |
Attributes:
| Name | Type | Description |
|---|---|---|
sample_rate |
float
|
Samples per second, from the abscissa — which must be even. |
records_per_channel |
dict[tuple[str, str, str | None], int]
|
{channel key: how many records carry it}. |
split_into_frames |
bool
|
Whether the records are already the averages. |
average_counts |
tuple[int, int]
|
(fewest, most) frames any one channel will be averaged over. |
Source code in src/visualdynamics/core/data.py
Attributes¶
sample_rate
property
¶
Samples per second, from the abscissa — which must be even.
records_per_channel
property
¶
{channel key: how many records carry it}.
Usually one. A capture a controller saved frame by frame holds one record per average.
split_into_frames
property
¶
Whether the records are already the averages.
A controller that saves its spectral captures writes each frame as its own record. There is then nothing to slice and nothing to overlap: the frame length and the count are settled by the file, and the only parameter left to choose is the window.
average_counts
property
¶
(fewest, most) frames any one channel will be averaged over.
The two agree for anything a controller wrote, which saves every channel the same number of times. They part only for a history assembled by hand out of unequal captures, and then the table has to say so rather than quote a number that is true of some channels and not others.
Methods:¶
compute_spectra
¶
compute_spectra() -> Spectrum
The averaged spectrum of every channel — sdynpy's convention.
A channel's frames — its records across the averages — are each
FFT'd single-sided with no amplitude scaling (numpy's rfft,
norm='backward': a sine of amplitude A on a bin reads AN/2),
rectangular window, and averaged as the complex mean*, exactly
what sdynpy's TimeHistoryArray.fft does with frames. Phase is
preserved; content whose phase is random frame to frame — a
burst random excitation's response — averages toward zero,
which is that convention's documented behavior. Returns a
Spectrum with one record per channel, carrying the channel's
own quantity and units.
Source code in src/visualdynamics/core/data.py
channel_key
¶
What makes record i the same channel as another.
The DOF and what is measured there, never the DOF alone: a drive point carries a force record and an acceleration record at the same DOF, and those two are not each other's averages. This is the key the framing groups on, so counting channels and averaging them cannot disagree about what a channel is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i
|
int
|
Which record. |
required |
Returns:
| Type | Description |
|---|---|
tuple of (str, str, str or None)
|
The DOF, quantity and unit that identify the channel — records sharing this key are the same channel. |
Source code in src/visualdynamics/core/data.py
suggest_averaging
¶
suggest_averaging(**kwargs: Any) -> Averaging
Averaging parameters worked out from the record itself.
A run holds more than the test — the shaker coming up, a
reduced-level check, whatever was still recording afterwards —
and this finds the settled stretch worth averaging and as many
frames as it will carry. See visualdynamics.core.detect.
A record that already carries an averaging — a controller's own recipe from an import, or one set by hand — keeps its frame length, window, overlap and detrend: only the start and the count are worked out (Brandon, 2026-09-19: Detect must not override the frame length, window and overlap the controller used). A bare record gets the detector's own recipe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**kwargs
|
Any
|
Overrides for individual parameters, such as |
{}
|
Returns:
| Type | Description |
|---|---|
Averaging
|
Parameters worked out from the record itself. |
Source code in src/visualdynamics/core/data.py
capture_indices
¶
Which playing each record is: 0 for a channel's first
record, 1 for its second, and so on — in exactly the order
_spectral_frame pools them, so the playing the averaging
view pages to is one of the playings the average adds up. A
channel is a channel_key group, the same grouping the
pooling uses.
Source code in src/visualdynamics/core/data.py
suggest_truncation
¶
The whole record — the only neutral span.
A starting point for the truncate view's handles, never a default the act adopts: keeping everything is not an act, so Truncate Data refuses until a real span is set.
Source code in src/visualdynamics/core/data.py
truncate
¶
truncate(truncation: Any = None) -> TimeHistory
This record cut to a span (core.truncate.truncate), using
the history's own truncation unless one is passed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
truncation
|
Truncation
|
The start and stop, in seconds on the record's own clock.
Defaults to the record's own |
None
|
Returns:
| Type | Description |
|---|---|
TimeHistory
|
The samples inside the span, every channel, the clock kept. |
Source code in src/visualdynamics/core/data.py
suggest_filtering
¶
A starting low-pass: a tenth of the sample rate, order 4.
A low-pass rather than any other kind, because cutting noise above the content is the reach-for-first case; the filter view offers high- and band-pass beside it.
A judgment, not a detection — nothing in the record says where its content stops being signal. A tenth of the rate is where the integrate/differentiate round trip was measured at ~2% RMS (core.filters), and it sits below the mounted-resonance range a shock accelerometer pollutes. The filter view exists precisely so this number gets looked at rather than trusted.
Source code in src/visualdynamics/core/data.py
filter
¶
filter(filtering: Any = None) -> TimeHistory
This record through its filter (core.filters.filtered) —
low-, high- or band-pass, whichever the filtering describes —
using the history's own filtering unless one is passed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filtering
|
Filtering
|
The pass-band edges and order. Defaults to the record's
own |
None
|
Returns:
| Type | Description |
|---|---|
TimeHistory
|
Every channel through the filter, zero phase. |
Source code in src/visualdynamics/core/data.py
integrate
¶
integrate(drift_corner: Any = ...) -> TimeHistory
One integration — acceleration to velocity, velocity to
displacement (core.filters.integrate). ... takes the
default drift corner; None integrates raw, drift and all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
drift_corner
|
float or None
|
High-pass corner in Hz applied after integrating, so a sensor
bias cannot become a ramp. |
...
|
Returns:
| Type | Description |
|---|---|
TimeHistory
|
Acceleration becomes velocity, velocity becomes displacement. Other quantities are left out. |
Source code in src/visualdynamics/core/data.py
differentiate
¶
differentiate() -> TimeHistory
One differentiation — displacement to velocity, velocity to
acceleration (core.filters.differentiate).
srs_windows
¶
The stretches an SRS of this record would read, settled.
The fallback chain compute_srs has always used, extracted so
the shock panel's derived rows and the spectrum itself cannot
disagree about it (one implementation): the shocks the record
carries; failing those, the averaging frames when the record
is being read as frames; failing everything, the whole record
as one window. Clipped to the record either way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shocks
|
sequence of Shock
|
Override windows; the record's own when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
list of Shock
|
The settled windows, clipped to the record. |
Source code in src/visualdynamics/core/data.py
srs_band
¶
(low, high) in Hz: the band these windows can support.
From a frequency low enough that the shortest window still
holds a cycle of it — a curve is one grid across every event,
so the shortest is what the grid has to fit — up to a fifth
of the sample rate, above which the ramp-invariant filter is
being asked about frequencies the record cannot resolve.
What compute_srs uses when no band is given, and what the
shock panel states beside its settings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shocks
|
sequence of Shock
|
Override windows; the record's own when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
tuple of float
|
(low, high) in Hz. |
Source code in src/visualdynamics/core/data.py
compute_srs
¶
compute_srs(shocks: Sequence[int] | None = None, low: float | None = None, high: float | None = None, per_octave: int | None = None, q: float | None = None, kind: str = 'maximax') -> Srs
A shock response spectrum for every channel of every shock.
The parameters live on the history, the way averaging does: pass
shocks to override, or leave it and the windows already on the
object are used. With none anywhere, the whole record is one
window, which is what a history holding a single trimmed
transient is.
The windows are the shocks the record carries, or — when it is being read as frames rather than events — the frames. A specification carries neither and is one window: it is a single playing of a waveform, whole.
One curve per channel per event, never averaged across events.
A shock test is judged on the worst shock, and the mean of four
of them describes none of them. Which event a curve came from is
in block, exactly as which average a frame came from is.
The band defaults to what the windows can support: from a frequency low enough that the shortest window still holds a cycle of it — a curve is one grid across every event, so the shortest is what the grid has to fit — up to a fifth of the sample rate, above which the ramp-invariant filter is being asked about frequencies the record cannot resolve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shocks
|
sequence of int
|
Which shock windows to use. All of them when omitted. |
None
|
low
|
float
|
The natural-frequency band, in Hz. |
None
|
high
|
float
|
The natural-frequency band, in Hz. |
None
|
per_octave
|
int
|
Frequency lines per octave. |
None
|
q
|
float
|
The oscillator amplification. |
None
|
kind
|
str
|
Which peak to keep: 'maximax' (largest magnitude of either sign), 'positive' or 'negative'. |
'maximax'
|
Returns:
| Type | Description |
|---|---|
Srs
|
One curve per channel per shock. |
Source code in src/visualdynamics/core/data.py
1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 | |
compute_psds
¶
One-sided auto-power spectral density per channel, averaged across the frames.
Welch's method where each average is already its own frame: rectangular window, no overlap, Gxx = 2|X|²/(fs·N) with DC and Nyquist unhalved, the frames' powers averaged. Power is phase-insensitive, so burst random's random phase costs nothing here. Values land in (SI unit)²/Hz with the Psd dimension convention ('acceleration**2/frequency'); a channel with undefined units stays undefined, its hint squared along.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
averaging
|
Averaging
|
How to cut the record into frames. Defaults to the record's
own |
None
|
Returns:
| Type | Description |
|---|---|
Psd
|
One auto-power spectral density per channel. |
Source code in src/visualdynamics/core/data.py
psd_type
¶
psd_type() -> type[Psd]
What a PSD of this history is.
A plain record's spectra are plain spectra. A target's are
still a target: the PSD of a waveform the article was required
to see is the spectrum it was required to see, and losing that
on the way through an FFT would leave two objects of the same
class with nothing but a name to say which was the requirement.
Overridden in TransientSpecification rather than decided by
the caller, so a script and the app cannot disagree.
Source code in src/visualdynamics/core/data.py
compute_cpsds
¶
The full cross-spectral density matrix, averaged across the frames — every channel against every channel, not just each against itself.
Gxy = 2·conj(X)·Y/(fs·N) with DC and Nyquist unhalved, which is
compute_psds on the diagonal, where conj(X)·X is |X|². The
cross terms are how two channels move together, which is most of
what a CPSD is for, and they are what a PSD throws away.
Laid out as the importer lays an imported matrix out: one record
per (response, reference) pair, row by row, so an n-channel
history gives n² records that read as a grid. A cross term's
dimension is the product of the two, a*b/frequency, against
a**2/frequency down the diagonal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
averaging
|
Averaging
|
How to cut the record into frames. Defaults to the record's
own |
None
|
Returns:
| Type | Description |
|---|---|
Psd
|
The full cross-spectral matrix, every channel against every channel. |
Source code in src/visualdynamics/core/data.py
drive_dofs
¶
The DOFs this history looks like it was driven at.
A guess from the quantities alone, and it is only ever a default. What actually makes a channel a drive is that the controller had a feedback device on it, which the channel table records and a time history does not.
Source code in src/visualdynamics/core/data.py
compute_frfs
¶
compute_frfs(references: Sequence[str] | None = None, averaging: Averaging | None = None, method: str = 'Hv') -> Frf
The frequency response functions, one per response/drive pair.
The three estimators differ in one assumption — where the noise is — and agree wherever there is little of it. They part company exactly where a measurement is worst, which is why the choice matters and why it is a choice.
H1 assumes the noise is on the response. It biases low at resonance, where the response is large and the force small, and it is what a controller computes and a modal fit expects.
Gfx = Gff H, so H = Gff^-1 Gfx
With one reference that is the textbook Gfx / Gff. With
several it is the MIMO estimate, and the matrix inverse is the
whole point: two shakers driving one article are correlated, and
dividing each response by each drive separately would credit
both with the same motion.
H2 assumes the noise is on the reference, and biases high
at anti-resonance for the mirror-image reason. With one
reference it is Gxx / Gxf, one response at a time. With
several references it needs as many equations as unknowns, and
there are exactly enough only when the system is square — as
many responses as references — where it becomes the classical
coupled form Gxx * Gfx^-1 (Rocklin, Crowley and Vold, 1985),
computed here exactly as sdynpy computes it (matched by
decision, Brandon 2026-08-28, and pinned against its numbers).
The coupling is worth knowing about: every response feeds one
matrix inverse, so a channel's H2 depends on which other
channels are in the set — measured at 0.2% on the oracle
signals, growing with noise — where H1 and Hv rows never do. A
non-square multi-reference set is refused; Hv answers the same
noise-on-both question per response, uncoupled.
Hv (the default) assumes noise on both and asks for neither: it is the total-least-squares fit, the null direction of
[[Gff, Gfx], [Gxf, Gxx]]
taken as the eigenvector of its smallest eigenvalue, per response and per line. It falls between H1 and H2 — strictly between, wherever the coherence is under one — and needs no claim about which instrument is the better one. It is the default because that claim is the one a test least often gets to make honestly: an accelerometer out on a structure and a force cell in the load path are both imperfect, in different places.
Note what a total-least-squares fit means with units in play: it weighs a unit of error on the force against a unit of error on the acceleration, and those are not the same thing. That is baked into the estimator and is the received formulation; it is why Hv is a middle reading and not a better one.
A pseudo-inverse rather than a solve for H1, because two shakers
can be very nearly the same drive and Gff is then close to
singular — where a solve raises or returns nonsense, a
pseudo-inverse gives the least-squares answer the estimate is
asking for anyway.
Built on the same frames compute_psds averages and the same
references compute_multiple_coherence uses, so the coherence
beside an FRF is that FRF's coherence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
references
|
sequence of str
|
The drive DOFs. Detected from the record when omitted. |
None
|
averaging
|
Averaging
|
How to cut the record into frames. Defaults to the record's
own |
None
|
method
|
str
|
The estimator: 'Hv', 'H1' or 'H2'. |
'Hv'
|
Returns:
| Type | Description |
|---|---|
Frf
|
One record per response and drive pair. |
Source code in src/visualdynamics/core/data.py
1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 | |
compute_multiple_coherence
¶
compute_multiple_coherence(references: Sequence[str] | None = None, averaging: Averaging | None = None) -> MultipleCoherence
How much of each response the drives together account for.
Ordinary coherence asks what one reference explains. Multiple coherence asks what a whole set of them explains at once, which is the only useful question in a MIMO test: two shakers driving one article are correlated with each other, so a response can look poorly coherent with either one alone while being fully accounted for by the pair.
For a response x and references r,
gamma^2 = (Grx^H Grr^-1 Grx) / Gxx
— the power of the best linear prediction of x from all the references at once, over the power actually measured. With one reference it collapses to the ordinary coherence, which is the cheapest check that the algebra is right.
Built on the same frames compute_psds averages, so it covers
the stretch of record the averaging view has set and no other:
a coherence worked out over the whole file would describe a
different measurement from the PSD beside it.
references and the framing are _cross_spectral_frame's, the
same ones compute_frfs uses — so the coherence beside an FRF
is that FRF's coherence and not a differently-framed one.
A pseudo-inverse rather than a solve, because two shakers driving one article can be very nearly the same drive and the reference matrix is then close to singular — where a solve raises or returns nonsense, a pseudo-inverse gives the least-squares answer the estimate is asking for anyway.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
references
|
sequence of str
|
The drive DOFs. Detected from the record when omitted. |
None
|
averaging
|
Averaging
|
How to cut the record into frames. Defaults to the record's
own |
None
|
Returns:
| Type | Description |
|---|---|
MultipleCoherence
|
One curve per response channel. |
Source code in src/visualdynamics/core/data.py
1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 | |
to_sep005
¶
This record as SEP 005 timeseries — the sdypy ecosystem's
interchange form (io.sep005 reads them back).
timeseries = history.to_sep005('run 4')
Returns the standard's list form: usually one dict, and one
per unit where channels mix. The sdypy validator holds a
series' unit_str to a single string, so accelerometers
beside a force gauge cannot be one compliant series — the list
of series is exactly what the standard provides for that, and a
split series wears the unit in its name so two of them stay
distinguishable.
Values go exactly as they are held: SI where units are defined,
with unit_str naming the SI unit, and the file's raw
numbers where they are not, with unit_str empty — the
standard allows an empty unit, and inventing one would claim a
scale nobody declared. fs says the sampling when it is
even; an uneven record sends its time vector, which the
standard equally accepts. quantity rides where the
standard has a letter for what a series measures.
name defaults to the comment when the record carries one,
because a SEP 005 series must be named and the comment is the
nearest thing to a name an object holds — the project knows
what it called this record, the record does not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A name for the series. |
None
|
Returns:
| Type | Description |
|---|---|
list of dict
|
One SEP 005 timeseries mapping per channel. |
Source code in src/visualdynamics/core/data.py
2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 | |
TransientSpecification
¶
TransientSpecification(abscissa: ArrayLike, ordinate: ArrayLike, response_dof: str | Sequence[str], reference_dof: str | Sequence[str] | None = None, ordinate_dim: str | Sequence[str] | None = None, comment: str | Sequence[str] | None = None, ordinate_unit: str | Sequence[str | None] | None = None, reference_unit: str | Sequence[str | None] | None = None, dimension_hint: str | Sequence[str | None] | None = None, block: str | Sequence[str] | None = None)
Bases: TimeHistory
What a transient test was controlled to: a target time history.
A different thing from a ShockSpecification, and the difference is
worth keeping straight because the two get called by each other's
names. A shock specification is an SRS: a required response
spectrum, and a controller meets it by producing some transient
whose spectrum lands inside the band. A transient specification
is a waveform: this acceleration, sample by sample, and the
controller inverts the structure's transfer function to reproduce
it. Rattlesnake can run the second today; the first it cannot.
So this is a time history that happens to be a target, and the comparison it invites is against another time history — what the article actually did — rather than against a band. It carries no limits for that reason: a tolerance on a waveform is not a settled idea the way a tolerance on a spectrum is, and inventing one here would be inventing a convention rather than reading one.
Its derived spectra stay targets. A PSD of this is a
Specification and an SRS of it is a ShockSpecification — both
without limits, for the reason above — because the spectrum of a
waveform the article was required to see is the spectrum it was
required to see. Left as plain objects they would be
indistinguishable from the response's own spectra but for a name,
and the comparison between them would have to be made by hand
instead of by type, which is the one thing this whole arrangement
exists to avoid.
Methods:
| Name | Description |
|---|---|
psd_type |
What a PSD of this record is: still a specification. |
srs_type |
What an SRS of this record is: a shock specification, for |
Source code in src/visualdynamics/core/data.py
Methods:¶
psd_type
¶
psd_type() -> type[Psd]
What a PSD of this record is: still a specification.
The spectrum of a required waveform is itself a requirement,
so it comes back as Specification rather than a plain Psd.
Source code in src/visualdynamics/core/data.py
Geometry
¶
Geometry(node_id: Ids, node_xyz: ArrayLike, node_def_cs: ArrayLike | None = None, node_disp_cs: ArrayLike | None = None, node_color: ArrayLike | None = None, cs_id: Ids | None = None, cs_name: Sequence[str] | None = None, cs_type: ArrayLike | None = None, cs_matrix: ArrayLike | None = None, traceline_id: Ids | None = None, traceline_color: ArrayLike | None = None, traceline_desc: Sequence[str] | None = None, traceline_conn: Sequence[ArrayLike] | None = None, elem_id: Ids | None = None, elem_type: ArrayLike | None = None, elem_color: ArrayLike | None = None, elem_conn: Sequence[ArrayLike] | None = None, elem_block: Ids | None = None, block_id: Ids | None = None, block_name: Sequence[str] | None = None, length_unit: str | None = None)
Nodes, coordinate systems, tracelines, elements and blocks.
Parameters are array-likes; connectivity lists contain one integer array of node ids per traceline/element. All coordinates in SI meters.
The arrays below are the storage; nodes, coordinate_systems,
tracelines, elements and blocks are views onto them, which
is how the tree lists a geometry and how a script should usually
reach one. A view is not a copy — writing through a row writes here.
Attributes:
node_id: Every node's id. Unique, because connectivity and
placement refer to nodes by id.
node_xyz: (nodes, 3) coordinates, in meters once
length_unit is declared and the file's raw numbers before
that.
node_def_cs: The coordinate system each node is placed in.
node_disp_cs: The system each node is measured in — the frame a
shape's values at that node are expressed in.
node_color: Palette index per node.
cs_id: Coordinate system ids. Unique, for the same reason as
nodes.
cs_name: A name per system, often empty.
cs_type: 0 cartesian, 1 cylindrical, 2 spherical (CS_TYPES).
cs_matrix: (systems, 4, 3) — three direction rows then the
origin, so cs_matrix[i, 3] is where system i sits.
traceline_id: Ids of the display polylines. Not unique: a UNV
trace line that lifts the pen arrives as several runs under
one id, and deleting that id removes all of them.
traceline_color: Palette index per line.
traceline_desc: Free text per line.
traceline_conn: One array of node ids per line, in drawing order.
elem_id: Element ids. Labels — nothing refers to them.
elem_type: UFF dataset 2412 descriptor code per element
(ELEMENT_TYPES names them and says how each is drawn).
elem_color: Palette index per element.
elem_conn: One array of node ids per element.
elem_block: Which block each element belongs to, by block id.
block_id: The declared blocks. Unique, since elements name them.
block_name: A name per block — 'wing', 'arm front left'. This
is where a mesh records that a region is a different part
from its neighbor, and fem.Model.from_geometry reads a
member's section from it.
length_unit: What the coordinates are in, or None while that
has not been declared — in which case they are the file's own
numbers and nothing has been scaled.
Methods:
| Name | Description |
|---|---|
validate |
Check the geometry hangs together — every element's nodes |
node_index |
Positions of the given node ids in the node arrays. |
contains_nodes |
Boolean array: which of |
missing_dofs |
The DOF strings whose node this geometry does not define. |
suggest_mass_properties |
The centroid of the nodes as the reference point, and no |
define_units |
Declare what the coordinates are in, converting them to SI. |
undefine_units |
Take the declaration back, restoring the file's raw coordinates. |
dof_direction |
A DOF's direction in global coordinates, through the frame |
add_node |
Append a node at |
add_coordinate_system |
Append a coordinate system. Returns its id. |
add_traceline |
Append a traceline through the given nodes. Returns its index. |
block_of |
The name of the block an element belongs to, or ''. |
elements_in |
The ids of the elements in a block, named or numbered. |
add_block |
Declare an element block. Returns its id. |
add_element |
Append an element. Type defaults to whatever fits the node count: |
renumber_node |
Give a node a new id, carrying its tracelines and elements over. |
renumber_block |
Give a block a new id, carrying its elements over. |
renumber_coordinate_system |
Give a coordinate system a new id, repointing the nodes using it. |
delete_nodes |
Remove nodes, and anything that referenced them. |
delete_coordinate_systems |
Remove coordinate systems, reassigning any node that used them. |
delete_tracelines |
Remove tracelines by id. Ids that are not there are ignored. |
delete_blocks |
Remove blocks, moving anything in them into the first one left. |
delete_elements |
Remove elements by id. Ids that are not there are ignored. |
save |
Write the geometry to a file of its own. |
plot |
Draw the geometry: nodes, elements and tracelines. |
plot_dofs |
This geometry with labeled arrows at every DOF |
Attributes:
| Name | Type | Description |
|---|---|---|
num_nodes |
int
|
How many nodes the geometry defines. |
nodes |
EntityView
|
Every node: |
coordinate_systems |
EntityView
|
Every coordinate system: |
tracelines |
EntityView
|
Every traceline: |
elements |
EntityView
|
Every element: |
blocks |
EntityView
|
Every element block: |
extent |
tuple[ndarray, ndarray]
|
(min_xyz, max_xyz), in meters once units are defined. |
units_defined |
bool
|
Whether the geometry knows what its coordinates mean. |
Source code in src/visualdynamics/core/geometry.py
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 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 | |
Attributes¶
coordinate_systems
property
¶
coordinate_systems: EntityView
Every coordinate system: ids, names, types, matrices.
blocks
property
¶
blocks: EntityView
Every element block: ids and names.
A block groups elements rather than holding them — which elements
are in one is read off elem_block (elements_in), so moving an
element between blocks is an edit to the element.
extent
property
¶
(min_xyz, max_xyz), in meters once units are defined.
units_defined
property
¶
Whether the geometry knows what its coordinates mean.
False until define_units names the length unit.
Methods:¶
validate
¶
Check the geometry hangs together — every element's nodes present, every identifier unique — and report what does not.
Source code in src/visualdynamics/core/geometry.py
node_index
¶
Positions of the given node ids in the node arrays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_ids
|
int or sequence of int
|
The identifiers, one or many. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Each identifier's row in the node arrays. |
Source code in src/visualdynamics/core/geometry.py
contains_nodes
¶
Boolean array: which of node_ids this geometry defines.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_ids
|
int or sequence of int
|
The identifiers, one or many. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
A boolean per identifier: whether the geometry has it. |
Source code in src/visualdynamics/core/geometry.py
missing_dofs
¶
The DOF strings whose node this geometry does not define.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dofs
|
sequence of str
|
The degrees of freedom to check, such as '101Z+'. |
required |
Returns:
| Type | Description |
|---|---|
list of str
|
Those the geometry has no node for — what makes a data object incompatible with it. |
Source code in src/visualdynamics/core/geometry.py
suggest_mass_properties
¶
suggest_mass_properties() -> MassProperties
The centroid of the nodes as the reference point, and no mass — unit rigid-body shapes about the middle of the model.
The seed the rigid-body pane opens with and what
generate_rigid_body_modes adopts when nothing was set: unlike
a whole-record truncation, this is a real answer, and the one
the virtual-point transformation wants most often.
Returns:
| Type | Description |
|---|---|
MassProperties
|
The centroid, unscaled. |
Source code in src/visualdynamics/core/geometry.py
define_units
¶
define_units(length_unit: str) -> Geometry
Declare what the coordinates are in, converting them to SI.
Re-declaring reinterprets the original file values rather than scaling twice, so a wrong guess can simply be corrected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length_unit
|
str
|
The unit the coordinates are in, such as 'm' or 'in'. |
required |
Returns:
| Type | Description |
|---|---|
Geometry
|
Self, converted to SI in place. |
Source code in src/visualdynamics/core/geometry.py
undefine_units
¶
undefine_units() -> Geometry
Take the declaration back, restoring the file's raw coordinates.
Source code in src/visualdynamics/core/geometry.py
dof_direction
¶
A DOF's direction in global coordinates, through the frame its node is measured in.
'101X+' at a node whose node_disp_cs is rotated is not global
X. The report's grid of control channels reads which global
axis a channel is nearest and how far off it sits (Brandon,
2026-09-19), and this is where that is answered. A rotational
DOF points along its axis, as the DOF arrows draw it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dof
|
str
|
The DOF, such as '101X+' or '1313RZ-'. |
required |
Returns:
| Type | Description |
|---|---|
ndarray or None
|
A unit vector, or None when the DOF names no node this geometry has, no axis (a node number alone), or a node measured in a cylindrical or spherical frame — whose local axes turn with the node's position, a reading this does not attempt rather than half-answer. |
Source code in src/visualdynamics/core/geometry.py
add_node
¶
add_node(xyz: ArrayLike, node_id: int | None = None, color: int = 1, def_cs: int | None = None, disp_cs: int | None = None) -> int
Append a node at xyz (SI). Returns its id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xyz
|
array_like
|
The node's coordinates. |
required |
node_id
|
int
|
Its identifier. The next free one when omitted. |
None
|
color
|
int
|
Its display color index. |
1
|
def_cs
|
int
|
The coordinate system the position is given in. |
None
|
disp_cs
|
int
|
The coordinate system displacements are measured in. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
The node's identifier. |
Source code in src/visualdynamics/core/geometry.py
add_coordinate_system
¶
add_coordinate_system(origin: ArrayLike = (0.0, 0.0, 0.0), rotation: ArrayLike | None = None, cs_id: int | None = None, name: str = '', cs_type: int = 0) -> int
Append a coordinate system. Returns its id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
origin
|
array_like
|
The system's origin. |
(0, 0, 0)
|
rotation
|
array_like
|
A 3x3 rotation matrix. Identity when omitted. |
None
|
cs_id
|
int
|
Its identifier. The next free one when omitted. |
None
|
name
|
str
|
What to call it. |
''
|
cs_type
|
int
|
0 cartesian, 1 cylindrical, 2 spherical. |
0
|
Returns:
| Type | Description |
|---|---|
int
|
The coordinate system's identifier. |
Source code in src/visualdynamics/core/geometry.py
add_traceline
¶
Append a traceline through the given nodes. Returns its index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_ids
|
int or sequence of int
|
The nodes the line passes through, in order. |
required |
color
|
int
|
Its display color index. |
1
|
description
|
str
|
A label for it. |
''
|
Returns:
| Type | Description |
|---|---|
int
|
The traceline's identifier. |
Source code in src/visualdynamics/core/geometry.py
block_of
¶
The name of the block an element belongs to, or ''.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
elem_id
|
int
|
Which element. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The name of the block it belongs to. |
Source code in src/visualdynamics/core/geometry.py
elements_in
¶
The ids of the elements in a block, named or numbered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
block
|
int or str
|
A block, by identifier or by name. |
required |
Returns:
| Type | Description |
|---|---|
list of int
|
The identifiers of the elements it holds. |
Source code in src/visualdynamics/core/geometry.py
add_block
¶
Declare an element block. Returns its id.
A block with nothing in it is legitimate — exodus files carry empty ones, and a block has to exist before an element can be put in it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
What to call the block. |
''
|
block_id
|
int
|
Its identifier. The next free one when omitted. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
The block's identifier. |
Source code in src/visualdynamics/core/geometry.py
add_element
¶
add_element(node_ids: Ids, elem_type: int | None = None, color: int = 1, block: int | None = None) -> int
Append an element. Type defaults to whatever fits the node count: 2 nodes a beam, 3 a triangle, 4 a quadrilateral. Returns its index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_ids
|
int or sequence of int
|
The nodes the element connects, in order. |
required |
elem_type
|
int
|
The element type code. Inferred from the node count when omitted. |
None
|
color
|
int
|
Its display color index. |
1
|
block
|
int
|
Which block it belongs to. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
The element's identifier. |
Source code in src/visualdynamics/core/geometry.py
renumber_node
¶
Give a node a new id, carrying its tracelines and elements over.
Connectivity names nodes by id, so a rename that left it alone would orphan every line and face touching the node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
int
|
Which node, by row. |
required |
node_id
|
int
|
Its new identifier. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/geometry.py
renumber_block
¶
Give a block a new id, carrying its elements over.
An element names its block by id, so a renumber that left them
alone would put every element of the block in a block that is no
longer there — which validate refuses, after the damage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
int
|
Which block, by row. |
required |
block_id
|
int
|
Its new identifier. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/geometry.py
renumber_coordinate_system
¶
Give a coordinate system a new id, repointing the nodes using it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
row
|
int
|
Which system, by row. |
required |
cs_id
|
int
|
Its new identifier. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/geometry.py
delete_nodes
¶
Remove nodes, and anything that referenced them.
A traceline or element naming a deleted node cannot survive, so it goes too. Returns what was removed, for reporting.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_ids
|
int or sequence of int
|
The identifiers, one or many. |
required |
Returns:
| Type | Description |
|---|---|
dict of str to int
|
How many of each kind were removed, including the dependents that went with them. |
Source code in src/visualdynamics/core/geometry.py
delete_coordinate_systems
¶
Remove coordinate systems, reassigning any node that used them.
The last coordinate system is never removed — nodes must reference something.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cs_ids
|
int or sequence of int
|
The identifiers, one or many. |
required |
Returns:
| Type | Description |
|---|---|
dict of str to int
|
How many of each kind were removed, including the dependents that went with them. |
Source code in src/visualdynamics/core/geometry.py
delete_tracelines
¶
Remove tracelines by id. Ids that are not there are ignored.
One id can name several polylines — a UNV trace line that lifts the pen arrives split into its drawn runs, all still that one trace line — and deleting it removes all of them, which is what deleting that trace line means.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
traceline_ids
|
int or sequence of int
|
The identifiers, one or many. |
required |
Returns:
| Type | Description |
|---|---|
dict of str to int
|
How many of each kind were removed, including the dependents that went with them. |
Source code in src/visualdynamics/core/geometry.py
delete_blocks
¶
Remove blocks, moving anything in them into the first one left.
Deleting the grouping must not delete what was grouped — an element is a piece of the mesh and a block is a label on it — so the elements move rather than go, the way a node whose coordinate system is deleted is reassigned. The last block cannot go while any element names one; with no elements at all there is nothing to hold and the geometry may have none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
block_ids
|
int or sequence of int
|
The identifiers, one or many. |
required |
Returns:
| Type | Description |
|---|---|
dict of str to int
|
How many of each kind were removed, including the dependents that went with them. |
Source code in src/visualdynamics/core/geometry.py
delete_elements
¶
Remove elements by id. Ids that are not there are ignored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
elem_ids
|
int or sequence of int
|
The identifiers, one or many. |
required |
Returns:
| Type | Description |
|---|---|
dict of str to int
|
How many of each kind were removed, including the dependents that went with them. |
Source code in src/visualdynamics/core/geometry.py
save
¶
Write the geometry to a file of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
Where to write it. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/geometry.py
plot
¶
plot(unit_system: UnitSystem | None = None, **kwargs: Any) -> Any
Draw the geometry: nodes, elements and tracelines.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit_system
|
UnitSystem
|
Units to draw in. |
None
|
**kwargs
|
Any
|
Passed through to the scene. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/geometry.py
plot_dofs
¶
plot_dofs(source: Any, quantity: str, unit_system: UnitSystem | None = None, **kwargs: Any) -> Any
This geometry with labeled arrows at every DOF source
measures as quantity — the GUI's DOF arrows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
DataArray
|
The object whose degrees of freedom are drawn. |
required |
quantity
|
str
|
Which quantity's DOFs to show, such as 'acceleration'. |
required |
unit_system
|
UnitSystem
|
Units to draw in. |
None
|
**kwargs
|
Any
|
Passed through to the scene. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plotter the scene is in. |
Source code in src/visualdynamics/core/geometry.py
ShapeSet
¶
ShapeSet(frequency: ArrayLike, damping: ArrayLike, coordinate: Sequence[str], shape_matrix: ArrayLike, modal_mass: ArrayLike | None = None, comment: str | Sequence[str] | None = None, mass_unit: str | None = None, description: Sequence[str] | None = None, unscaled: bool = False, modal_damping: ArrayLike | None = None)
Mode shapes over a shared set of DOFs.
shape_matrix is (modes, dofs). coordinate lists the DOF strings the
columns correspond to ('101X+').
A fitted set is also the record of the fit: reopening one in the app (Edit Fit) reconstructs the session that produced it, which is why the description and the scaling flag ride along with the numbers.
Attributes:
frequency: Hz per mode. A rigid-body mode is exactly 0.0 — the
FRF synthesis cancels its 0/0 by testing for that, so 'very
small' is not the same thing.
damping: Fraction of critical per mode, so 2% is 0.02.
coordinate: The DOF string of each column of shape_matrix.
shape_matrix: (modes, dofs). Complex for a complex mode; the
overlay and MAC machinery handles either.
modal_mass: Per mode. 1.0 throughout for a mass-normalized set,
which is what an eigensolution here produces. Complex when
an imported source carried complex modal mass — kept as
measured, never squeezed real.
modal_damping: Complex modal damping per mode where a source
carried one (I-DEAS ADFs do), or None. Distinct from
damping, the viscous fraction of critical: this is the
complex-mode estimate as the identifying tool reported it.
mass_unit: What modal_mass is in, or None when undeclared.
description: Free text per mode — what the shape is, filled in
while reading the table ('first torsion').
comment: One line about the set as a whole.
unscaled: True when the fit had no drive point to pin the
mass-normalized scale. Shapes and MACs are unaffected;
modal masses are then a convention rather than physics, and
comparisons refuse to read a scale factor out of them.
Methods:
| Name | Description |
|---|---|
auto_mac |
MAC of every mode against every other; the diagonal is 1. |
covers |
Does the shape set have a coefficient at this DOF? |
synthesize_frf |
FRFs from the modal model, one row per DOF pair. |
delete_modes |
Remove the given modes in place; the last one is refused. |
define_units |
Declare the mass unit the shapes were normalized against. |
undefine_units |
Take the declaration back, restoring the file's raw coefficients. |
display_shapes |
Coefficients in the display system's 1/sqrt(mass); undefined pass |
unit_label |
'1/√kg' for the stored unit, or the display system's. |
mode_label |
'Mode 3 — 12.4 Hz, 2.0% damping'. |
save |
Write the shape set to a file of its own. |
plot_mac |
The MAC grid: this set against itself, or against |
animate |
This mode moving on a geometry, as the GUI animates it. |
plot |
The set's own reading: its auto-MAC, or one mode animated |
Attributes:
| Name | Type | Description |
|---|---|---|
num_shapes |
int
|
How many mode shapes the set holds. |
num_dofs |
int
|
How many degrees of freedom each shape covers. |
is_complex |
bool
|
Whether these are complex modes. Real normal modes move |
units_defined |
bool
|
Whether the shapes carry a mass unit, without which a |
Source code in src/visualdynamics/core/shapes.py
Attributes¶
is_complex
property
¶
Whether these are complex modes. Real normal modes move every DOF in phase; complex ones do not, which is what a damped or non-proportionally damped structure produces.
units_defined
property
¶
Whether the shapes carry a mass unit, without which a modal mass is a number with no scale behind it.
Methods:¶
auto_mac
¶
covers
¶
Does the shape set have a coefficient at this DOF?
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dof
|
str
|
A degree of freedom, such as '101Z+'. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Whether the shapes include it. |
Source code in src/visualdynamics/core/shapes.py
synthesize_frf
¶
synthesize_frf(frequencies: ArrayLike, response_dof: Sequence[str], reference_dof: Sequence[str], modes: Sequence[int] | None = None, power: int = 0) -> ndarray
FRFs from the modal model, one row per DOF pair.
H_jk(f) = sum_r (iw)^power phi_jr phi_kr
/ (m_r (w_r^2 - w^2 + 2i z_r w_r w))
with w = 2pif — the residue form for mass-normalized shapes, with
modal_mass carrying any other scaling. power picks the response
quantity: 0 displacement per force, 1 velocity, 2 acceleration. It
applies inside the sum because a rigid-body mode's denominator is
exactly -w^2: at w = 0 its accelerance cancels to the finite
residue, where an after-the-fact multiply is 0/0 and a screenful
of warnings. Its displacement and velocity there are genuinely
unbounded and come back as nan.
modes restricts the sum; a truncated synthesis beside the
measurement is what shows which modes the measurement actually
contains. Raises ValueError for a DOF the shapes do not cover;
covers says so in advance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequencies
|
array_like
|
The lines to synthesize at, in Hz. |
required |
response_dof
|
sequence of str
|
The response degrees of freedom. |
required |
reference_dof
|
sequence of str
|
The drive degrees of freedom. |
required |
modes
|
sequence of int
|
Which modes to include. All of them when omitted. |
None
|
power
|
int
|
0 receptance, 1 mobility, 2 accelerance. |
0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
The synthesized FRFs, one row per response and drive pair. |
Source code in src/visualdynamics/core/shapes.py
delete_modes
¶
Remove the given modes in place; the last one is refused.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indices
|
sequence of int
|
Which modes to remove. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/shapes.py
define_units
¶
define_units(mass_unit: str) -> ShapeSet
Declare the mass unit the shapes were normalized against.
Re-declaring reinterprets the file's values rather than scaling twice, so a wrong guess can be corrected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mass_unit
|
str
|
The unit modal mass is in. |
required |
Returns:
| Type | Description |
|---|---|
ShapeSet
|
Self, converted to SI in place. |
Source code in src/visualdynamics/core/shapes.py
undefine_units
¶
undefine_units() -> ShapeSet
Take the declaration back, restoring the file's raw coefficients.
Source code in src/visualdynamics/core/shapes.py
display_shapes
¶
display_shapes(unit_system: UnitSystem) -> ndarray
Coefficients in the display system's 1/sqrt(mass); undefined pass through unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit_system
|
UnitSystem
|
The units to present in. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The shape matrix in display units. |
Source code in src/visualdynamics/core/shapes.py
unit_label
¶
unit_label(unit_system: UnitSystem | None = None) -> str
'1/√kg' for the stored unit, or the display system's.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unit_system
|
UnitSystem
|
Units to label in. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
How the shapes' own unit reads. |
Source code in src/visualdynamics/core/shapes.py
mode_label
¶
'Mode 3 — 12.4 Hz, 2.0% damping'.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i
|
int
|
Which mode. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A short label: its frequency, and its damping when known. |
Source code in src/visualdynamics/core/shapes.py
save
¶
Write the shape set to a file of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str or PathLike
|
Where to write it. |
required |
Returns:
| Type | Description |
|---|---|
None
|
|
Source code in src/visualdynamics/core/shapes.py
plot_mac
¶
plot_mac(other: ShapeSet | None = None, **kwargs: Any) -> Any
The MAC grid: this set against itself, or against other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
ShapeSet
|
The set to compare against. This set against itself when omitted, which is how repeated modes show up. |
None
|
**kwargs
|
Any
|
Passed through to the plotting layer. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget. |
Source code in src/visualdynamics/core/shapes.py
animate
¶
animate(geometry: Geometry, mode: int = 0, **kwargs: Any) -> Any
This mode moving on a geometry, as the GUI animates it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry
|
Geometry
|
The geometry to move. |
required |
mode
|
int
|
Which mode, by index. |
0
|
**kwargs
|
Any
|
Passed through to the scene. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |
Source code in src/visualdynamics/core/shapes.py
plot
¶
plot(geometry: Geometry | None = None, mode: int = 0, **kwargs: Any) -> Any
The set's own reading: its auto-MAC, or one mode animated when a geometry says where to put it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry
|
Geometry
|
The geometry to draw on. |
None
|
mode
|
int
|
Which mode. |
0
|
**kwargs
|
Any
|
Passed through to the scene. |
{}
|
Returns:
| Type | Description |
|---|---|
object
|
The plot widget or plotter. |