pyOMA – Multi-Setup OMA with PoGER merging#

Interactive Jupyter companion to scripts/multi_setup_analysis_poger.py.

PoGER (Post Global Estimation Re-scaling) merges setups before modal identification: correlation functions from all setups are stacked into a joint Hankel matrix, and a single SSI run yields global frequencies, damping ratios, and re-scaled mode shapes directly — no separate merge step is required. Compare with multi_setup_analysis.ipynb (PoSER), where SSI is run per setup and modal parameters are merged afterwards.

Step

Cell

Description

1

Geometry

Load nodes, lines, parent–child assignments

2

Per-setup loop

Pre-process signals and add each setup to the PoGER object

3

Joint ID

Pair channels, build joint Hankel matrix, estimate modal parameters

4a

StabilGUIWeb

Interactive stabilisation diagram — select poles manually

4b

Auto-select

Headless alternative when interactive GUI is not needed

5

Results

Print identified frequencies and damping ratios

6

PlotMSHWeb

Animate mode shapes interactively

Requirements: pip install "pyOMA[jupyter]"

Run cells top-to-bottom with Shift+Enter. After selecting poles in the stabilisation diagram (Step 4a), continue from Step 5 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,
    PogerSSICovRef,
    StabilCluster,
    StabilPlot,
    ModeShapePlot,
)
from pyOMA.core.Helpers import ConfigFile
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'

# PoGER uses SSI-cov/ref; the config provides num_block_columns and max_model_order
CONF_FILE = EXAMPLE_DATA / 'ssi_config.txt'

# 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'Config    : {CONF_FILE.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 pre-processing#

Each setup is pre-processed independently (decimation, correlation functions) and then added to the shared PogerSSICovRef object. The actual modal identification happens in the next cell using the jointly stacked data.

setup_dirs = sorted(
    EXAMPLE_DATA.glob('measurement_*'),
    key=lambda p: int(p.name.split('_')[1]),
)

poger = PogerSSICovRef()

for setup_dir in setup_dirs:
    meas_name = setup_dir.name
    print(f'\n── {meas_name} ──')

    _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=401)
        prep_signals.psd()
        if SAVE_RESULTS:
            prep_signals.save_state(_prep_state)

    poger.add_setup(prep_signals)

print(f'\n{len(poger.setups)} setup(s) added.')

4 Joint PoGER identification#

Channels common to all setups are paired as SSI reference channels. The correlation matrices are stacked into a joint block-Hankel matrix and decomposed via SVD. A single SSI run extracts global modal parameters and re-scales partial mode shapes to the reference of the first setup.

poger.pair_channels()

cfg = ConfigFile(CONF_FILE)
num_block_columns = cfg.int('Number of Block-Columns')
max_model_order   = cfg.int('Maximum Model Order')

_poger_state = EXAMPLE_DATA / 'poger_modal_data.npz'
if _poger_state.exists() and SKIP_EXISTING:
    poger = PogerSSICovRef.load_state(_poger_state)
else:
    poger.build_merged_subspace_matrix(num_block_columns)
    poger.compute_modal_params(max_model_order)
    if SAVE_RESULTS:
        poger.save_state(_poger_state)

stabil_calc = StabilCluster(poger)
stabil_calc.calculate_stabilization_masks(**STABIL_KWARGS)
stabil_plot = StabilPlot(stabil_calc)

print('Joint identification complete.')
print(f'  Block columns : {num_block_columns}')
print(f'  Max order     : {max_model_order}')

5a Interactive pole selection#

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 satisfied, continue to Step 5 (Results).

Tip: For fully automatic selection, skip this cell and run Step 4b instead.

stabil_widget, cursor = StabilGUIWeb(stabil_plot)
display(stabil_widget)

5b Automatic pole selection (alternative to 5a)#

Run this cell instead of 5a for a fully headless workflow. Poles that were already selected interactively are left unchanged.

if not stabil_calc.select_modes:
    stabil_calc.automatic_selection()
print(f'{len(stabil_calc.select_modes)} mode(s) selected')

6 Results#

selected_freqs = [poger.modal_frequencies[i] for i in stabil_calc.select_modes]
selected_damps = [poger.modal_damping[i]      for i in stabil_calc.select_modes]
n_modes  = len(selected_freqs)
n_setups = len(poger.setups)

print(f'\nPoGER results  ({n_setups} setup(s), {n_modes} mode(s))')
print('\u2500' * 46)
print(f'  {"#":>3}  {"Freq [Hz]":>10}  {"Damp [%]":>10}')
print('\u2500' * 46)
for i, (f, d) in enumerate(zip(selected_freqs, selected_damps), 1):
    print(f'  {i:>3}  {f:>10.3f}  {d:>10.3f}')
print('\u2500' * 46)

7 Mode shape visualisation#

Displays the identified 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,
    stabil_calc=stabil_calc,
    modal_data=poger,
    prep_signals=poger.prep_signals,
    amplitude=20,
)
msh_widget = PlotMSHWeb(msp)
display(msh_widget)