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)