{ "cells": [ { "cell_type": "markdown", "id": "title", "metadata": {}, "source": [ "# pyOMA – Multi-Setup OMA with PoGER merging\n", "\n", "Interactive Jupyter companion to `scripts/multi_setup_analysis_poger.py`.\n", "\n", "PoGER (Post Global Estimation Re-scaling) merges setups **before** modal\n", "identification: correlation functions from all setups are stacked into a\n", "joint Hankel matrix, and a single SSI run yields global frequencies,\n", "damping ratios, and re-scaled mode shapes directly — no separate merge\n", "step is required. Compare with `multi_setup_analysis.ipynb` (PoSER),\n", "where SSI is run per setup and modal parameters are merged afterwards.\n", "\n", "| Step | Cell | Description |\n", "|------|------|-------------|\n", "| 1 | Geometry | Load nodes, lines, parent–child assignments |\n", "| 2 | Per-setup loop | Pre-process signals and add each setup to the PoGER object |\n", "| 3 | Joint ID | Pair channels, build joint Hankel matrix, estimate modal parameters |\n", "| 4a | StabilGUIWeb | Interactive stabilisation diagram — select poles manually |\n", "| 4b | Auto-select | Headless alternative when interactive GUI is not needed |\n", "| 5 | Results | Print identified frequencies and damping ratios |\n", "| 6 | PlotMSHWeb | Animate mode shapes interactively |\n", "\n", "> **Requirements:** `pip install \"pyOMA[jupyter]\"`\n", ">\n", "> Run cells top-to-bottom with **Shift+Enter**. After selecting poles in\n", "> the stabilisation diagram (Step 4a), continue from Step 5 onwards." ] }, { "cell_type": "markdown", "id": "imports-header", "metadata": {}, "source": [ "## 0 Imports and display backend" ] }, { "cell_type": "code", "execution_count": null, "id": "imports", "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "import numpy as np\n", "import ipympl # noqa: F401 – activates the widget backend\n", "%matplotlib widget\n", "import matplotlib.pyplot as plt\n", "\n", "from IPython.display import display\n", "\n", "from pyOMA.core import (\n", " GeometryProcessor,\n", " SignalPlot,\n", " PreProcessSignals,\n", " PogerSSICovRef,\n", " StabilCluster,\n", " StabilPlot,\n", " ModeShapePlot,\n", ")\n", "from pyOMA.core.Helpers import ConfigFile\n", "from pyOMA.GUI.JupyterGUI import StabilGUIWeb, PlotMSHWeb\n", "\n", "print('pyOMA imported successfully')" ] }, { "cell_type": "markdown", "id": "config-header", "metadata": {}, "source": [ "## 1 Configuration\n", "\n", "Adjust the paths and parameters below to match your project." ] }, { "cell_type": "code", "execution_count": null, "id": "config", "metadata": {}, "outputs": [], "source": [ "import pyOMA\n", "\n", "# Root of the repository — works whether pyOMA is installed or run in-place\n", "REPO_ROOT = Path(pyOMA.__file__).parent.parent\n", "EXAMPLE_DATA = REPO_ROOT / 'tests' / 'files'\n", "\n", "# PoGER uses SSI-cov/ref; the config provides num_block_columns and max_model_order\n", "CONF_FILE = EXAMPLE_DATA / 'ssi_config.txt'\n", "\n", "# Stabilisation thresholds\n", "STABIL_KWARGS = dict(\n", " d_range=(0, 0.5),\n", " df_max=0.01,\n", " dd_max=0.05,\n", " dmac_max=0.05,\n", ")\n", "\n", "SKIP_EXISTING = False # load saved intermediate results when True\n", "SAVE_RESULTS = False # persist intermediate results to disk when True\n", "\n", "PreProcessSignals.load_measurement_file = np.load\n", "\n", "assert EXAMPLE_DATA.exists(), f'Example data not found at {EXAMPLE_DATA}'\n", "print(f'Data root : {EXAMPLE_DATA}')\n", "print(f'Config : {CONF_FILE.name}')" ] }, { "cell_type": "markdown", "id": "geometry-header", "metadata": {}, "source": [ "## 2 Structural geometry" ] }, { "cell_type": "code", "execution_count": null, "id": "geometry", "metadata": {}, "outputs": [], "source": [ "geometry_data = GeometryProcessor.load_geometry(\n", " nodes_file=EXAMPLE_DATA / 'grid.txt',\n", " lines_file=EXAMPLE_DATA / 'lines.txt',\n", " parent_childs_file=EXAMPLE_DATA / 'parent_child_assignments.txt',\n", ")\n", "print(f'{len(geometry_data.nodes)} nodes, {len(geometry_data.lines)} lines loaded')" ] }, { "cell_type": "markdown", "id": "loop-header", "metadata": {}, "source": [ "## 3 Per-setup loop: signal pre-processing\n", "\n", "Each setup is pre-processed independently (decimation, correlation functions)\n", "and then added to the shared `PogerSSICovRef` object. The actual modal\n", "identification happens in the next cell using the jointly stacked data." ] }, { "cell_type": "code", "execution_count": null, "id": "loop", "metadata": {}, "outputs": [], "source": [ "setup_dirs = sorted(\n", " EXAMPLE_DATA.glob('measurement_*'),\n", " key=lambda p: int(p.name.split('_')[1]),\n", ")\n", "\n", "poger = PogerSSICovRef()\n", "\n", "for setup_dir in setup_dirs:\n", " meas_name = setup_dir.name\n", " print(f'\\n── {meas_name} ──')\n", "\n", " _prep_state = setup_dir / 'prep_data.npz'\n", " if _prep_state.exists() and SKIP_EXISTING:\n", " prep_signals = PreProcessSignals.load_state(_prep_state)\n", " else:\n", " prep_signals = PreProcessSignals.init_from_config(\n", " conf_file=setup_dir / 'setup_info.txt',\n", " meas_file=setup_dir / f'{meas_name}.npy',\n", " chan_dofs_file=setup_dir / 'channel_dofs.txt',\n", " )\n", " prep_signals.decimate_signals(3)\n", " prep_signals.decimate_signals(3)\n", " prep_signals.correlation(m_lags=401)\n", " prep_signals.psd()\n", " if SAVE_RESULTS:\n", " prep_signals.save_state(_prep_state)\n", "\n", " poger.add_setup(prep_signals)\n", "\n", "print(f'\\n{len(poger.setups)} setup(s) added.')" ] }, { "cell_type": "markdown", "id": "ms-poger-geo-viz-header", "metadata": {}, "source": [ "### Geometry with sensor positions\n", "\n", "The plot below shows the structural nodes and lines together with channel-DOF arrows for the *last* pre-processed setup. Arrows indicate each sensor's measurement direction." ] }, { "cell_type": "code", "execution_count": null, "id": "ms-poger-geo-viz-code", "metadata": {}, "outputs": [], "source": [ "setup_dir = setup_dirs[7]\n", "meas_name = setup_dir.name\n", "tmp_prep_signals = PreProcessSignals.init_from_config(\n", " conf_file=setup_dir / 'setup_info.txt',\n", " meas_file=setup_dir / f'{meas_name}.npy',\n", " chan_dofs_file=setup_dir / 'channel_dofs.txt',)\n", "\n", "geo_plot = ModeShapePlot(geometry_data=geometry_data, prep_signals=tmp_prep_signals)\n", "geo_plot.reset_view()\n", "geo_plot.draw_nodes()\n", "geo_plot.draw_lines()\n", "geo_plot.draw_chan_dofs()\n", "geo_plot.refresh_parent_childs(False)\n", "geo_plot.subplot.view_init(elev=20, azim=110)\n", "display(geo_plot.fig)" ] }, { "cell_type": "markdown", "id": "ms-poger-sig-header", "metadata": {}, "source": [ "### Signal inspection\n", "\n", "Inspect the time-series and power spectral densities for the last pre-processed setup before running identification." ] }, { "cell_type": "code", "execution_count": null, "id": "ms-poger-sig-code", "metadata": {}, "outputs": [], "source": [ "sig_plot = SignalPlot(prep_signals)\n", "sig_plot.plot_signals(per_channel_axes=True, psd_scale='db')\n", "plt.suptitle(f'Signals — {prep_signals.setup_name}', fontsize=12)\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "id-header", "metadata": {}, "source": [ "## 4 Joint PoGER identification\n", "\n", "Channels common to all setups are paired as SSI reference channels.\n", "The correlation matrices are stacked into a joint block-Hankel matrix\n", "and decomposed via SVD. A single SSI run extracts global modal parameters\n", "and re-scales partial mode shapes to the reference of the first setup." ] }, { "cell_type": "code", "execution_count": null, "id": "joint-id", "metadata": {}, "outputs": [], "source": [ "poger.pair_channels()\n", "\n", "cfg = ConfigFile(CONF_FILE)\n", "num_block_columns = cfg.int('Number of Block-Columns')\n", "max_model_order = cfg.int('Maximum Model Order')\n", "\n", "_poger_state = EXAMPLE_DATA / 'poger_modal_data.npz'\n", "if _poger_state.exists() and SKIP_EXISTING:\n", " poger = PogerSSICovRef.load_state(_poger_state)\n", "else:\n", " poger.build_merged_subspace_matrix(num_block_columns)\n", " poger.compute_modal_params(max_model_order)\n", " if SAVE_RESULTS:\n", " poger.save_state(_poger_state)\n", "\n", "stabil_calc = StabilCluster(poger)\n", "stabil_calc.calculate_stabilization_masks(**STABIL_KWARGS)\n", "stabil_plot = StabilPlot(stabil_calc)\n", "\n", "print('Joint identification complete.')\n", "print(f' Block columns : {num_block_columns}')\n", "print(f' Max order : {max_model_order}')" ] }, { "cell_type": "markdown", "id": "stabil-header", "metadata": {}, "source": [ "## 5a Interactive pole selection\n", "\n", "Switch the **Cursor** radio button to *Stable*, hover over a stable pole\n", "cluster and **click** to select it. Click again to deselect. Adjust the\n", "soft/hard criteria sliders to refine what counts as “stable” before selecting.\n", "\n", "Once satisfied, continue to **Step 5 (Results)**.\n", "\n", "> **Tip:** For fully automatic selection, skip this cell and run **Step 4b** instead." ] }, { "cell_type": "code", "execution_count": null, "id": "stabil-gui", "metadata": {}, "outputs": [], "source": [ "stabil_widget, cursor = StabilGUIWeb(stabil_plot)\n", "display(stabil_widget)" ] }, { "cell_type": "code", "execution_count": null, "id": "auto-select", "metadata": {}, "outputs": [], "source": [ "if not stabil_calc.select_modes:\n", " stabil_calc.automatic_selection()\n", "print(f'{len(stabil_calc.select_modes)} mode(s) selected')" ] }, { "cell_type": "markdown", "id": "results-header", "metadata": {}, "source": [ "## 6 Results" ] }, { "cell_type": "code", "execution_count": null, "id": "results", "metadata": {}, "outputs": [], "source": [ "selected_freqs = [poger.modal_frequencies[i] for i in stabil_calc.select_modes]\n", "selected_damps = [poger.modal_damping[i] for i in stabil_calc.select_modes]\n", "n_modes = len(selected_freqs)\n", "n_setups = len(poger.setups)\n", "\n", "print(f'\\nPoGER results ({n_setups} setup(s), {n_modes} mode(s))')\n", "print('\\u2500' * 46)\n", "print(f' {\"#\":>3} {\"Freq [Hz]\":>10} {\"Damp [%]\":>10}')\n", "print('\\u2500' * 46)\n", "for i, (f, d) in enumerate(zip(selected_freqs, selected_damps), 1):\n", " print(f' {i:>3} {f:>10.3f} {d:>10.3f}')\n", "print('\\u2500' * 46)" ] }, { "cell_type": "markdown", "id": "msh-header", "metadata": {}, "source": [ "## 7 Mode shape visualisation\n", "\n", "Displays the identified mode shapes on the 3-D geometry. Use the **Mode**\n", "dropdown to switch between modes and the **play** button to animate." ] }, { "cell_type": "code", "execution_count": null, "id": "msh-gui", "metadata": {}, "outputs": [], "source": [ "msp = ModeShapePlot(\n", " geometry_data=geometry_data,\n", " stabil_calc=stabil_calc,\n", " modal_data=poger,\n", " prep_signals=poger.prep_signals,\n", " amplitude=20,\n", ")\n", "msh_widget = PlotMSHWeb(msp)\n", "display(msh_widget)" ] }, { "cell_type": "code", "execution_count": null, "id": "726e1747-8835-4e52-b816-24c8530e1da6", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.6" } }, "nbformat": 4, "nbformat_minor": 5 }