visualdynamics.core.modal_fit¶
modal_fit
¶
Fit real normal modes to measured FRFs, one mode at a time.
The session holds the measured FRF matrix and the modes confirmed so far. Everything it shows is computed on the residual, the measurement minus the synthesis of the confirmed modes, so a confirmed mode removes its own peak from the CMIF and the most prominent peak left is the next suggestion.
One mode is fitted from the residual alone:
- the SVD at the peak line gives the shape direction (its first left singular vector, rotated to the nearest real vector, since these are real normal modes);
- the frequency and damping are the values that leave the least
residual over a band around the peak, searched from a half-power
estimate (
search,search_damping); - a least-squares fit of the SDOF term over a band around the peak gives one real residue per reference, which scales the unit shape into a mass-normalized one. A drive point (a reference also measured as a response) sets the scale. Without one the scale comes from the residue magnitudes, the synthesis still reproduces the measured records, and the shape set is marked unscaled.
References
The single-degree-of-freedom residue fit is standard; these are where it and the indicator driving it are set out.
- Ewins, D. J. (2000). Modal Testing: Theory, Practice and Application, 2nd ed. Research Studies Press. The residue form fitted here, and what neighboring modes leave behind as residuals.
- Allemang, R. J., & Brown, D. L. (1998). "A Complete Review of the Complex Mode Indicator Function (CMIF) with Applications." Proceedings of ISMA. The indicator whose peaks suggest the next mode, and whose peak drops away once that mode is confirmed.
- Maia, N. M. M., & Silva, J. M. M. (1997). Theoretical and Experimental Modal Analysis. Research Studies Press. Residue to mode-shape scaling, including the unit-modal-mass convention.
The way of working, one mode at a time with the analyst choosing each pole, was inspired by SMAC; its computations are not the ones here.
- Klenke, S. E., & Mayes, R. L. (1999). "The SMAC Modal Parameter Extraction Package." International Modal Analysis Conference, Orlando, FL. Synthesize Modes And Correlate, which fits modes one at a time by correlating a trial root's synthesized response with the measured data.
Classes:
| Name | Description |
|---|---|
ModalFitSession |
An interactive fit of real normal modes to one FRF set. |
Functions:
| Name | Description |
|---|---|
prominence_window |
How many lines either side a peak's prominence is measured |
prominence |
How far each of |
Classes¶
ModalFitSession
¶
ModalFitSession(frf: Frf, records: Sequence[int] | None = None, coherence: Any | None = None)
An interactive fit of real normal modes to one FRF set.
frf is the measured FRF. records picks which of its records
take part, all of them when None. coherence, when given, weights
each response channel at each line in the least squares by
γ²/(1 - γ²), the inverse of the FRF estimate's relative variance,
with γ² capped at 0.999 and the weights scaled to a maximum of one.
A channel with low coherence counts for less, and a response the
coherence does not cover has weight one. A suggestion is made when
the session is
created, so pending already holds a frequency and damping.
Methods:
| Name | Description |
|---|---|
adopt |
Start the fit from an existing shape set's modes. |
synthesis_matrix |
The synthesis of the confirmed modes, (lines, responses, |
residual_matrix |
The measured matrix minus the synthesis of the confirmed modes, |
synthesis_singular_values |
The singular values of the synthesis CMIF, (k, lines), |
synthesis_records |
The synthesis of the confirmed modes, and of the previewed mode |
searched |
(low, high), the frequency range a suggestion may land in. |
suggest |
Move the pending mode to the most prominent residual CMIF peak |
move_to |
Move the pending mode to |
override_damping |
Hold the pending mode's damping at |
pending_curve |
The SDOF magnitude curve of the pending (frequency, damping), |
estimate_damping |
The half-power damping estimate at the line nearest |
half_power |
Read Q from the half-power width of the residual CMIF peak |
fit_pending |
Fit the pending mode where the cursor is, without adding it, |
confirm |
Add the pending mode to the confirmed modes, and return it. |
refine_residues |
Fit the residues of all confirmed modes again, together, with |
residual_records |
The residual in the source FRF's record layout, (records, |
search_damping |
The damping that leaves the least residual at |
search |
The (frequency, damping) near line |
shape_set |
The confirmed modes as a ShapeSet, ready for the project. |
Source code in src/visualdynamics/core/modal_fit.py
Methods:¶
adopt
¶
adopt(shapes: ShapeSet) -> None
Start the fit from an existing shape set's modes.
This is how a published fit is edited. A shape set holds the frequency, damping, shape and description of each mode, which is everything the session keeps for a confirmed mode. Shapes are matched to this session's DOFs by name: a DOF the set does not have gets zero, and the set's DOFs that are not in this FRF are dropped. A shape set does not store the order its modes were confirmed in, so the adopted modes are replayed in frequency order. A new suggestion is made afterward.
Source code in src/visualdynamics/core/modal_fit.py
synthesis_matrix
¶
The synthesis of the confirmed modes, (lines, responses,
references), in the layout of matrix.
Source code in src/visualdynamics/core/modal_fit.py
residual_matrix
¶
The measured matrix minus the synthesis of the confirmed modes, (lines, responses, references).
The result is cached against the number of confirmed modes. It does not depend on the cursor, and a drag asks for it on every mouse move.
Source code in src/visualdynamics/core/modal_fit.py
synthesis_singular_values
¶
The singular values of the synthesis CMIF, (k, lines), computed without building the synthesis matrix.
The modal synthesis of m modes has rank m: each mode adds
q_k(w) * outer(shape, participation), so the whole matrix is
U diag(q(w)) V^T with U and V constant over frequency.
Singular values do not change under a matrix with orthonormal
columns on the left or orthonormal rows on the right, so U and V
are each QR-factored once, and the SVD at each line is taken of
a core at most m by m built from the two R factors, in place of
the full (responses x references) matrix. The values are the
same as the full SVD gives, to rounding.
include_preview adds the previewed mode (fitted, not yet
confirmed). The result is padded with zero rows up to
min(responses, references), the number of rows the full SVD
returns: the extra singular values of a rank-m matrix are zero,
and the plot's floor test counts rows. None when there is no
mode to synthesize.
Source code in src/visualdynamics/core/modal_fit.py
synthesis_records
¶
The synthesis of the confirmed modes, and of the previewed mode
when include_preview is set, in the source FRF's record layout,
(records, lines). The dashed CMIF is drawn from this.
Source code in src/visualdynamics/core/modal_fit.py
searched
¶
(low, high), the frequency range a suggestion may land in.
within is the plot's view, (low, high) in Hz; zooming to a
region is how the user says the next mode is in there. Without
it the whole measured range is searched. The range is clamped to
the measured range, so a view scrolled past the end of the data
still searches data, and a view entirely outside the data gives
the whole range.
Source code in src/visualdynamics/core/modal_fit.py
suggest
¶
Move the pending mode to the most prominent residual CMIF peak and return its (frequency, damping).
within is the plot's view, as for searched; without it the
whole measured range is searched. Peaks are ranked by
prominence, not height: how far a peak stands above the median
of the log CMIF around it (at least 25 lines either side), which
is how a peak looks on the plot. The CMIF of accelerance grows
as frequency squared, so ranked by raw height the tallest
residual line is nearly always a shoulder of the highest dense
cluster.
Lines inside a confirmed mode's half-power width are skipped,
unless the residual there has a shape the confirmed modes do not
explain (_another_mode_at). Subtracting a fitted mode is exact
only when the mode is alone; in a cluster it leaves a ridge that
keeps the shape of the mode that left it, while the second mode
of a repeated pair, at the same frequency, has a shape of its
own. When no local peak qualifies, the tallest free line is
taken. The frequency and damping are then found by search, and
the pending mode's held damping is released. The cursor can
still be dragged anywhere by hand.
Source code in src/visualdynamics/core/modal_fit.py
move_to
¶
Move the pending mode to frequency, in Hz, where the cursor is.
With damping True (the default), and the damping not held by
hand, the damping is searched at the new frequency
(search_damping). The residual is cached against the confirmed
modes, so this costs one small SVD at the cursor line and a
one-dimensional search over a few dozen lines. A caller that
cannot afford even that, such as a drag that is already running
behind, passes False and the damping is left as it was. Any
preview is dropped.
Source code in src/visualdynamics/core/modal_fit.py
override_damping
¶
Hold the pending mode's damping at damping (a fraction of
critical), clipped to 1e-4 to 0.5.
The pending mode keeps this value until Find Mode suggests again or a confirm resets it. Where two solutions sit at nearly the same frequency they differ in damping, and the automatic search finds only one of them; holding the damping chooses which one the fit lands on. Any preview is dropped.
Source code in src/visualdynamics/core/modal_fit.py
pending_curve
¶
The SDOF magnitude curve of the pending (frequency, damping), as (frequencies, values).
This is the curve drawn under the cursor to show how sharp a mode is about to be fitted. Its peak is scaled to meet the residual CMIF at the cursor's line, and its width shows the damping, which is what lets a vertical drag set the damping. It covers max(6ζ, 0.015) times the frequency either side of the cursor, and at least two lines. It is closed form (no SVD, no least squares), so it can be redrawn on every mouse move.
unit_system converts the values the way the plotted CMIF is
converted: the session works in SI and the plot draws display
units. When no fit record has a defined dimension, the values
are returned unconverted.
Source code in src/visualdynamics/core/modal_fit.py
estimate_damping
¶
The half-power damping estimate at the line nearest
frequency, read from the residual CMIF.
A side that never falls to half power borrows the other side's width; with no crossing on either side the estimate is 0.01. The result is clipped to 1e-4 to 0.5. This is the seed the searches start from, not the damping a fit uses.
Source code in src/visualdynamics/core/modal_fit.py
half_power
¶
half_power(frequency: float | None = None, *, unit_system: Any | None = None, warn: bool = True) -> HalfPower
Read Q from the half-power width of the residual CMIF peak
under the cursor, or at frequency.
This is the readout beside the fit: the peak the cursor sits on,
its frequency, the two half-power crossings, Q and the damping
ratio they give, read off the residual CMIF the cursor works on
(core.half_power.read_half_power). For one FRF the CMIF is its
magnitude, so the reading is that FRF's peak. It is a reading of
the curve, not a fit, and it differs from the fitted damping
where the curve is not one clean mode: a close pair reads as one
wide peak, and the fit's search does not. Reading it costs a
climb along the cached CMIF, so a cursor can show it on every
move.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frequency
|
float
|
A frequency on the peak to read, in Hz; the pending mode's (the cursor's) when omitted. |
None
|
unit_system
|
UnitSystem
|
The height is converted to these units, as the plotted CMIF
is ( |
None
|
warn
|
bool
|
Warn a missing crossing or too few lines as a
|
True
|
Returns:
| Type | Description |
|---|---|
HalfPower
|
The reading. |
Source code in src/visualdynamics/core/modal_fit.py
fit_pending
¶
Fit the pending mode where the cursor is, without adding it, and return it.
Unless the damping is held by hand, it is searched at the cursor
first (search_damping, as move_to does), so the fit uses the
damping the cursor's curve showed. The shape is then computed and
the result held as preview: the synthesis and the MAC show it,
and Confirm adopts it. With the damping held by hand, the mode's
amplitude is scaled so its own CMIF at the cursor meets the
residual CMIF there, by a factor between 0.25 and 4.
The returned mode is a dict of frequency, damping, shape
(over coordinate), description and scaled (False when no
drive point set the scale).
Source code in src/visualdynamics/core/modal_fit.py
confirm
¶
confirm(frequency: float | None = None, damping: float | None = None, description: str | None = None) -> dict[str, Any]
Add the pending mode to the confirmed modes, and return it.
frequency, damping and description replace the pending
values when given. The preview is used when it was fitted at the
same frequency and damping; otherwise the mode is fitted here.
The modes are kept sorted by frequency, the confirm order is
recorded in confirmed, and the pending description and any
held damping are cleared.
Source code in src/visualdynamics/core/modal_fit.py
refine_residues
¶
Fit the residues of all confirmed modes again, together, with the frequencies and dampings held, and return how many modes were refined (0 with none confirmed).
Each mode was fit on the residual as it stood when it was confirmed, so the first of a close pair was fit on data that still held the second mode's tail, and its residues took up part of it. The sum of the pair matches the measurement; the error is in how it is split between them. Here the residues are fitted with all the SDOF terms present at once, over the lines near each mode (3ζf either side, at least two lines). Lines far from every mode are left out, because they hold only what was never fitted, and a full-band solve would spread that into the shapes.
Modes are solved together only when their bands overlap, and each such cluster is solved on the measurement minus the other modes at their current residues. Where close modes have similar shapes the least squares is badly conditioned and noise moves amplitude between the pair, so each cluster compares five candidates: the plain least-squares answer, three answers regularized toward the current residues (ridge weights 0.01, 0.05 and 0.2 of the mean diagonal), and the current residues themselves. The candidate whose synthesized CMIF is closest to the measured CMIF over the cluster's band is kept, so refining cannot make that score worse. A mode alone in its cluster keeps its shape. With coherence weights, each response channel's equations are weighted by its coherence weight.
The frequencies and dampings do not change, and the shapes do not change where the current residues won. Any preview is dropped.
Source code in src/visualdynamics/core/modal_fit.py
884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 | |
residual_records
¶
The residual in the source FRF's record layout, (records, lines). A plot of the residual draws this.
Source code in src/visualdynamics/core/modal_fit.py
search_damping
¶
search_damping(frequency: float, seed: float | None = None, refine: int = 4, steps: int = 5) -> float
The damping that leaves the least residual at frequency,
with the frequency held.
This is the one-dimensional form of search, for a cursor being
dragged: the user sets the frequency by hand, so only the damping
is searched. seed is where the search starts (the half-power
estimate when None or zero). Each of the refine passes tries
steps dampings across the current span, starting at the seed
plus or minus the seed, and fits a parabola through the best
and its neighbors; each pass then narrows the span to a fifth.
The result is clipped to 1e-4 to 0.5.
It is cheap enough to run on every mouse move: the residual is cached against the confirmed modes, so a call costs one small SVD at the cursor line and a few dozen SDOF evaluations.
Source code in src/visualdynamics/core/modal_fit.py
search
¶
search(index: int, damping: float | None = None, refine: int = 4, steps: int = 5) -> tuple[float, float]
The (frequency, damping) near line index that leave the least
residual.
The search runs coarse to fine. The first pass is a steps by
steps grid over half a line either side in frequency and the
seed plus or minus the seed in damping; each later pass repeats
the grid around the best point at a fifth of the span, refine
passes in all. Each pass ends with a parabola through the best
point and its neighbors in each direction. A pass whose best
frequency is on the edge of its grid moves the grid and looks
again (up to eight times) instead of narrowing, as long as the
best frequency stays within 1.5 lines of the starting line and
short of every confirmed mode's half-power width. A start inside
a confirmed mode's half-power width does not move the grid at
all. The damping is clipped to 1e-4 to 0.5.
This is used in place of reading the damping from two half-power crossings. Those are two points on a curve and assume the peak is one isolated mode; where two modes are close, the width they measure is the pair's, and the damping comes out too high with nothing to show it. A residual minimum does not assume either.
damping seeds the span when the user has typed one; otherwise
the half-power estimate seeds it, which is a good enough start
even where it is a poor answer.
Source code in src/visualdynamics/core/modal_fit.py
1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 | |
shape_set
¶
shape_set() -> ShapeSet
The confirmed modes as a ShapeSet, ready for the project.
The mass unit is kg when every fit record has a defined dimension, and None otherwise, since shapes fitted to raw values are raw. The set is marked unscaled when any mode had no drive point to set its scale.
Source code in src/visualdynamics/core/modal_fit.py
Functions:¶
prominence_window
¶
How many lines either side a peak's prominence is measured
over, for a search across lines lines: a twelfth of them, and at
least 25.
prominence
¶
How far each of peaks (line indices) stands above its
surroundings: its log value less the median log value of the
window lines either side of it.
This is how a peak looks on a plot, and it is the ranking the
fitting screen's suggestion uses. Ranked by raw height instead, the
tallest line of an accelerance (which grows as frequency squared) is
nearly always a shoulder of the highest dense cluster, never the
first mode. One rule, so the suggestion and the signature
comparison's mode finding (signature.find_modes) rank alike.