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)