pyOMA – Multi-Setup OMA with PoSER merging#
Interactive Jupyter companion to scripts/multi_setup_analysis.py.
Each sub-directory matching tests/files/measurement_* is treated as one
measurement setup. Stabilisation diagrams are computed for all setups and
shown in a tabbed widget for interactive pole selection. After selection the
results are merged across setups using PoSER and the mode shapes can be
inspected interactively.
Step |
Cell |
Description |
|---|---|---|
1 |
Geometry |
Load nodes, lines, parent–child assignments |
2–4 |
Per-setup loop |
Pre-process signals, identify modal parameters, compute stabilisation masks |
5a |
StabilGUIWeb |
Interactive tabbed stabilisation diagram — select poles manually |
5b |
Auto-select |
Headless alternative when interactive GUI is not needed |
6 |
Merge |
PoSER merge across setups |
7 |
Results |
Print merged frequencies and damping ratios |
8 |
PlotMSHWeb |
Animate merged mode shapes interactively |
Requirements:
pip install "pyOMA[jupyter]"Run cells top-to-bottom with Shift+Enter. After selecting poles in the stabilisation tabs (Step 5a), continue from Step 6 onwards.
0 Imports and display backend#
from pathlib import Path
import numpy as np
import ipympl # noqa: F401 – activates the widget backend
%matplotlib widget
from IPython.display import display
from pyOMA.core import (
GeometryProcessor,
PreProcessSignals,
BRSSICovRef,
SSIData,
PLSCF,
PRCE,
VarSSIRef,
StabilCluster,
StabilPlot,
ModeShapePlot,
)
from pyOMA.core.PostProcessingTools import MergePoSER
from pyOMA.GUI.JupyterGUI import StabilGUIWeb, PlotMSHWeb
print('pyOMA imported successfully')
1 Configuration#
Adjust the paths and parameters below to match your project.
import pyOMA
# Root of the repository — works whether pyOMA is installed or run in-place
REPO_ROOT = Path(pyOMA.__file__).parent.parent
EXAMPLE_DATA = REPO_ROOT / 'tests' / 'files'
# OMA method – change to SSIData, PLSCF, VarSSIRef, etc.
METHOD = BRSSICovRef
_CONF_FILES = {
BRSSICovRef: 'ssi_config.txt',
SSIData: 'ssi_config.txt',
PLSCF: 'plscf_config.txt',
PRCE: 'prce_config.txt',
VarSSIRef: 'varssi_config.txt',
}
CONF_FILE = EXAMPLE_DATA / _CONF_FILES[METHOD]
# Stabilisation thresholds
STABIL_KWARGS = dict(
d_range=(0, 0.5),
df_max=0.01,
dd_max=0.05,
dmac_max=0.05,
)
SKIP_EXISTING = False # load saved intermediate results when True
SAVE_RESULTS = False # persist intermediate results to disk when True
PreProcessSignals.load_measurement_file = np.load
assert EXAMPLE_DATA.exists(), f'Example data not found at {EXAMPLE_DATA}'
print(f'Data root : {EXAMPLE_DATA}')
print(f'Method : {METHOD.__name__}')
2 Structural geometry#
geometry_data = GeometryProcessor.load_geometry(
nodes_file=EXAMPLE_DATA / 'grid.txt',
lines_file=EXAMPLE_DATA / 'lines.txt',
parent_childs_file=EXAMPLE_DATA / 'parent_child_assignments.txt',
)
print(f'{len(geometry_data.nodes)} nodes, {len(geometry_data.lines)} lines loaded')
3 Per-setup loop: signal processing → modal ID → stabilisation#
All setups are processed here. Results are collected in lists so the interactive GUI (Step 4) and the merge step (Step 5) can use them.
setup_dirs = sorted(
EXAMPLE_DATA.glob('measurement_*'),
key=lambda p: int(p.name.split('_')[1]),
)
prep_signals_list = []
modal_datas = []
stabil_calcs = []
stabil_plots = []
setup_names = []
for setup_dir in setup_dirs:
meas_name = setup_dir.name
print(f'\n── {meas_name} ──')
# ── Signal pre-processing ────────────────────────────────────────────────
_prep_state = setup_dir / 'prep_data.npz'
if _prep_state.exists() and SKIP_EXISTING:
prep_signals = PreProcessSignals.load_state(_prep_state)
else:
prep_signals = PreProcessSignals.init_from_config(
conf_file=setup_dir / 'setup_info.txt',
meas_file=setup_dir / f'{meas_name}.npy',
chan_dofs_file=setup_dir / 'channel_dofs.txt',
)
prep_signals.decimate_signals(3)
prep_signals.decimate_signals(3)
prep_signals.correlation(m_lags=200)
prep_signals.psd()
if SAVE_RESULTS:
prep_signals.save_state(_prep_state)
# ── System identification ────────────────────────────────────────────────
_modal_state = setup_dir / 'modal_data.npz'
if _modal_state.exists() and SKIP_EXISTING:
modal_data = METHOD.load_state(_modal_state, prep_signals)
else:
modal_data = METHOD.init_from_config(CONF_FILE, prep_signals)
if SAVE_RESULTS:
modal_data.save_state(_modal_state)
# ── Stabilisation masks ──────────────────────────────────────────────────
_stabil_state = setup_dir / 'stabil_data.npz'
if _stabil_state.exists() and SKIP_EXISTING:
from pyOMA.core import StabilCalc
stabil_calc = StabilCalc.load_state(_stabil_state, modal_data)
else:
stabil_calc = StabilCluster(modal_data)
stabil_calc.calculate_stabilization_masks(**STABIL_KWARGS)
if SAVE_RESULTS:
stabil_calc.save_state(_stabil_state)
prep_signals_list.append(prep_signals)
modal_datas.append(modal_data)
stabil_calcs.append(stabil_calc)
stabil_plots.append(StabilPlot(stabil_calc))
setup_names.append(meas_name)
print(f'\n{len(stabil_calcs)} setup(s) ready.')
4a Interactive pole selection#
Each setup has its own tab. Switch the Cursor radio button to Stable, hover over a stable pole cluster and click to select it. Click again to deselect. Adjust the soft/hard criteria sliders to refine what counts as “stable” before selecting.
Once you are satisfied with the selection in all tabs, continue to Step 5 (Merge).
Tip: If you prefer fully automatic selection, skip this cell and run Step 4b instead.
stabil_widget, cursors = StabilGUIWeb(stabil_plots, setup_names=setup_names)
display(stabil_widget)
4b Automatic pole selection (alternative to 4a)#
Run this cell instead of 4a for a fully headless workflow. Poles that were already selected interactively are left unchanged.
for sc, name in zip(stabil_calcs, setup_names):
if not sc.select_modes:
sc.automatic_selection()
print(f'{name}: {len(sc.select_modes)} mode(s) selected')
5 Merge setups (PoSER)#
merger = MergePoSER()
for prep_signals, modal_data, stabil_calc in zip(prep_signals_list, modal_datas, stabil_calcs):
merger.add_setup(prep_signals, modal_data, stabil_calc)
merger.merge()
if SAVE_RESULTS:
_merged_state = EXAMPLE_DATA / 'measurement_15' / 'merged_setups.npz'
merger.save_state(_merged_state)
print('Merge complete.')
6 Merged results#
freqs = merger.mean_frequencies[:, 0]
damps = merger.mean_damping[:, 0]
std_f = merger.std_frequencies[:, 0]
std_d = merger.std_damping[:, 0]
n_modes = len(freqs)
n_setups = len(merger.setups)
print(f'\nMerged PoSER results ({n_setups} setup(s), {n_modes} mode(s))')
print('\u2500' * 56)
print(f' {"#":>3} {"Freq [Hz]":>10} {"Damp [%]":>10} {"\u03c3_f [Hz]":>10} {"\u03c3_d [%]":>10}')
print('\u2500' * 56)
for i, (f, d, sf, sd) in enumerate(zip(freqs, damps, std_f, std_d), 1):
print(f' {i:>3} {f:>10.3f} {d:>10.3f} {sf:>10.4f} {sd:>10.4f}')
print('\u2500' * 56)
7 Mode shape visualisation#
Displays the merged mode shapes on the 3-D geometry. Use the Mode dropdown to switch between modes and the play button to animate.
msp = ModeShapePlot(
geometry_data=geometry_data,
merged_data=merger,
amplitude=20,
)
msh_widget = PlotMSHWeb(msp)
display(msh_widget)