Skip to content

Modal Analysis Module

Modal analysis tools for mode fitting and modal parameter extraction.

modal

Created on Mon Aug 19 17:29:30 2019

@author: tb267

Attributes

MESSAGE module-attribute
MESSAGE = ''

Functions:

f_3dB
f_3dB(f, G0)
modal_fit_single_channel
modal_fit_single_channel(tf_data, freq_range=None, channel=0, measurement_type='acc')

Fit modal parameters for a single mode to data within specified freq_range

f_TF
f_TF(x, f, measurement_type)
f_residual
f_residual(x, f, G0, measurement_type)
f_TF_all_channels
f_TF_all_channels(x, f, measurement_type)
f_residual_all_channels
f_residual_all_channels(x, f, G0, measurement_type)
modal_fit_all_channels
modal_fit_all_channels(tf_data_list, freq_range=None, measurement_type='acc')

Fit modal parameters for a single mode to data within specified freq_range.

Assumes all tf_data in tf_data_list have same frequency axes

unpack
unpack(x)
unpack_matrix
unpack_matrix(X)

Unpack a stacked modal matrix X (one packed mode per row, [fn, zn, an*N, pn*N, rk*N, rm*N]) into per-parameter arrays.

Robust to an EMPTY model: a (0, 2+4*N) matrix (every mode deleted) returns zero-length fn/zn and (0, N) an/pn/rk/rm instead of raising. The channel count is read from the column count (X.shape[1]), NOT by indexing row 0 — indexing X[0, :] on an emptied (0, 6) matrix is exactly what crashed ModalData.delete_mode when the last mode was removed (the round-4 "Fit -> Reject" IndexError, also on Qt's Reject path).

pack
pack(fn, zn, an, pn, rk, rm)
reconstruct_transfer_function
reconstruct_transfer_function(modal_data, f, measurement_type='acc')

Reconstructs transfer functions from modal_data and returns TfData object. Includes the per-channel local residual terms (rk, rm). Does not modify modal_data.

_measurement_power
_measurement_power(measurement_type)

The (jw)^p exponent for 'dsp' (0) / 'vel' (1) / 'acc' (2).

_modes_band
_modes_band(M, f_axis)

Default estimation/refinement band: the modes' fn span padded so each peak's half-power skirts are included (the modal_refine convention), clamped to the measured axis. Returns [lo, hi] in Hz.

_measured_columns
_measured_columns(tf_data_list, sel)

Stack every measured (non-reconstruction) TF column over the sel frequency indices, cal-scaled — the modal_fit_all_channels / modal_refine G0 assembly, shared. Returns (len(sel), n_cols).

NB the shared-pole machinery assumes every set shares ONE frequency axis (tf_data_list[0].freq_axis) — the same assumption the fitters make.

estimate_global_constants
estimate_global_constants(fn, zn, f, G0, measurement_type='acc')

Re-estimate the COMPLEX modal constants and per-channel GLOBAL residual terms for FIXED poles — the linear half of the global fit (round-7g).

With the poles {fn, zn} held fixed, the modal model is LINEAR in the remaining parameters, so they solve in one least-squares with no convergence risk. In receptance space (measured columns divided by (jw)^p):

``G0_c/(jw)^p  ~=  sum_n A_nc * phi_n(w)  +  RH_c  -  RL_c/w^2``

with phi_n = 1/(wn^2 + 2j*wn*zn*w - w^2). RH (stiffness-like constant) and RL/w^2 (mass-like) are ONE pair of GLOBAL residues per channel, standing for above-band and below-band modes respectively — unlike the local fits' per-mode rk/rm, which are mutually redundant in a joint model (each mode's residues can impersonate a neighbour's tail: flat cost directions that let refinement drag poles without penalty). The constants that emerge are the GLOBALLY consistent ones: neighbour interaction is explained by the neighbours themselves, so any remaining phase in A is genuine mode complexity, not circle-rotation leakage from nearby modes.

Parameters:

  • fn (array_like) –

    pole natural frequencies (Hz), length N — held fixed during the solve

  • zn (array_like) –

    pole damping ratios, length N — held fixed

  • f (ndarray) –

    frequency axis (Hz); rows with f <= 0 are excluded (the 1/w^2 term and the (jw)^p division are singular at DC)

  • G0 (ndarray) –

    measured complex columns, shape (len(f), n_cols), cal-scaled

  • measurement_type (str, default: 'acc' ) –

    'acc' | 'vel' | 'dsp' (the (jw)^p convention)

Returns (A, RH, RL, cost): A complex (N, n_cols), RH and RL complex (n_cols,), and cost = 0.5*sum(|model - G0|^2) over the used rows in MEASURED space (identical to the stacked real/imag least-squares cost convention used by modal_refine).

_evaluate_global_model
_evaluate_global_model(fn, zn, A, RH, RL, f, measurement_type='acc')

Evaluate the re-estimated global model on axis f (any axis — the reconstruction axis need not match the estimation axis). Guards the w = 0 singularity the way f_TF_all_channels does (first bin borrows the second bin's frequency).

reconstruct_transfer_function_global
reconstruct_transfer_function_global(modal_data, f, measurement_type='acc', tf_data_list=None)

Reconstructs the GLOBAL (whole-model) transfer functions from modal_data and returns a TfData object. Does not modify modal_data.

With tf_data_list (the measured TFs) the reconstruction uses the round-7g GLOBAL RE-ESTIMATION: the modal constants (amplitude AND phase, per channel) plus one pair of global residues per channel (RH const + RL/w^2) are re-solved linearly against the measured data over the modes' padded band, with the stored poles held fixed (see estimate_global_constants — this removes the double-counting of neighbour interactions that each mode's LOCALLY-fitted phase absorbs).

Without tf_data_list (e.g. a ModalData loaded on its own) the legacy behaviour is kept: the stored per-mode rows are summed with their local residual terms (rk, rm) zeroed.

modal_refine
modal_refine(modal_data, tf_data_list, freq_range=None, measurement_type='acc')

Simultaneously refine ALL modes in modal_data against the measured transfer functions, seeded from the current fit.

Round-7g VARIABLE-PROJECTION rebuild: the nonlinear search runs over the POLES ONLY ([fn, zn] per mode — 2N parameters), and at every candidate pole set the modal constants + per-channel GLOBAL residues are re-solved linearly (estimate_global_constants). The previous refine optimised the whole packed parameter set including every mode's LOCAL residual terms (rk, rm) — which are mutually redundant in a joint model (one mode's residues can impersonate a neighbour's tail), creating flat directions in the cost surface along which a pole could drift far while the residues compensated, "improving" the residual as it went. Projecting the linear parameters out removes those flat directions and makes the pole search far stiffer on overlapping-mode data (e.g. instrument bodies).

Like modal_fit_all_channels, the measured TFs are cal-scaled when building the target G0, so the seed and the refined result live in the SAME parameter space. freq_range defaults to the modes' padded band (see _modes_band), clamped to the measured axis.

The refined M rows carry the poles plus the GLOBALLY re-estimated constants (an = |A|, pn = arg A) with the per-mode local residues ZEROED — after a joint refine the local-window residues are meaningless, and the global reconstruction re-estimates its own residues from the measured data anyway (reconstruct_transfer_function_global).

Convergence / non-convergence is REPORTED, never enforced. Returns (ModalData, info) with info = {'converged': bool, 'cost_before': float, 'cost_after': float}; both costs are the SAME projected-model cost (linear solve at the seed poles vs at the refined poles), so directly comparable. The refined model and info are returned even when not converged — the CALLER decides whether to keep or revert (the webui auto-reverts on converged == False). On a pathological failure the seed model is handed back unchanged with converged == False.

Mac-runnable, no hardware. Mirrors modal_fit_all_channels for settings / id_link / test_name provenance.