Skip to content

Fix PoSER merging: mode pairing, missing modes and mode-shape scaling - #58

Merged
dfm88 merged 1 commit into
mainfrom
fix/poser-merge
Sep 22, 2026
Merged

dfm88 merged 1 commit into
mainfrom
fix/poser-merge

Conversation

@dfm88

@dfm88 dfm88 commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

MultiSetup_PoSER.merge_results() had three problems in the way it combines the single-setup results. This PR fixes them:

  1. modes were matched across setups by their position in the list, not by being the same physical mode;
  2. a mode missing from some setups made the merge fail;
  3. the mode-shape rescaling was inverted, so the roving parts of the merged mode shapes had the wrong amplitude.

Nothing changes in the simple case: if every setup finds the same modes in the same order, the merged frequencies and damping ratios are identical to before. Mode shapes change only through the scaling fix in point 3. The call is also unchanged: msp.merge_results().

As agreed, using the single-setup SSI uncertainties (calc_unc=True) in the merge is not part of this PR; it is tracked in #59. The merge still ignores them, as before.


1. Modes were paired by position

Before: the k-th mode of every setup was averaged together. This only works if every setup finds every mode and sel_freq is picked in the same order everywhere. For example, if ss1 gives [2.0, 5.0, 9.0] Hz and ss2 gives [2.1, 9.1, 5.1] Hz, the 5.0 Hz mode was averaged with the 9.1 Hz one.

Now: before averaging, each mode of each setup is matched to "the same mode" in the other setups, using the two checks you would do by hand:

  • frequency: within 10 % (default);
  • mode shape on the reference sensors: MAC ≥ 0.5 (default). The reference sensors are the only DOFs measured in every setup, so they are the only place where mode shapes from different setups can be compared.

Both checks are needed. With only three reference sensors, the 2.63 Hz and 8.29 Hz modes of Example 3 have MAC 0.99 on the references, so the shape alone cannot tell them apart; the frequency can.

Among the admissible pairs, the program chooses the best overall one-to-one matching (small frequency difference and high MAC both count). Two close modes (e.g. 2.63 / 2.69 Hz) are therefore told apart by their shape, even when the frequency scatter between setups makes them cross.

2. Missing modes

Before: if a setup identified fewer modes, the merge failed.

Now:

  • A mode is merged from the setups that identified it.
  • A mode found only in a later setup is added at the end of the list.
  • In the merged mode shape, the roving sensors of a setup that missed the mode are NaN ("not measured"). They are not set to zero, because zero would look like a node.
  • The new attribute setups_used says which setups contributed to each merged mode.
  • Fn, Xi and their standard deviations are computed only over the setups that identified the mode. Fn_std is still the scatter between those setups, so it is 0 for a mode found in one setup only.

3. Mode-shape rescaling was inverted

In PoSER, each setup's mode shape has an arbitrary scale. To glue the roving parts together, each setup j is rescaled with the Modal Scale Factor αj, chosen so that its reference sensors match those of the first setup (αj φref,j ≈ φref,1). The roving part of setup j is then multiplied by αj.

The old code computed the factor in the opposite direction (the one that maps setup 1 onto setup j). It multiplied the roving part by roughly 1/αj instead of αj. Example:

reference sensors roving sensors
setup 1 1, 2 …
setup 2 10, 20 30, 40
setup 2 rescaled, old 300, 400
setup 2 rescaled, new 3, 4

The bug goes unnoticed when α ≈ ±1, which is common because SSI normalises every mode shape to its largest component. It is not always the case, though. On the Example 3 data (3SL building, settings in the next section), the roving part of each setup was off by these factors (old amplitude ÷ correct amplitude):

Mode (Hz) ss2 ss3
2.63 0.99 0.98
2.69 1.14 1.13
3.43 2.41 2.43
8.30 0.79 0.73
8.42 1.12 0.89
10.60 2.70 0.88
17.4 2.10 0.78

For example, in the merged 3.43 Hz mode the roving sensors of setups 2 and 3 were about 2.4 times too large compared with those of setup 1.


Effect on the Example 3 dataset

Settings: the ones of the Example 3 docs page, which are also the ms_poser fixture in tests/conftest.py. Each setup is decimated with q=2 and analysed with SSI(method="cov", br=50, ordmax=80), with the sel_freq below and order_in = 50 / 40 / 40 for ss1 / ss2 / ss3, and ref_ind = [[0, 1, 2]] * 3. (The docs page imports SSIcov, which no longer exists; the script below uses SSI.) In setups_used, 0 = ss1, 1 = ss2, 2 = ss3.

Script to reproduce the table
import numpy as np
from pyoma2.algorithms import SSI
from pyoma2.setup import MultiSetup_PoSER, SingleSetup
from pyoma2.support.utils.sample_data import get_sample_data

sel_freq = [2.63, 2.69, 3.43, 8.29, 8.42, 10.62, 14.00, 14.09, 17.57]
setups = []
for i, order_in in [(1, 50), (2, 40), (3, 40)]:
    data = np.load(get_sample_data(filename=f"set{i}.npy", folder="3SL"), allow_pickle=True)
    ss = SingleSetup(data, fs=100)
    ss.decimate_data(q=2)
    ss.add_algorithms(SSI(name=f"SSIcov_s{i}", method="cov", br=50, ordmax=80))
    ss.run_all()
    ss.mpe(f"SSIcov_s{i}", sel_freq=sel_freq, order_in=order_in)
    setups.append(ss)

msp = MultiSetup_PoSER(ref_ind=[[0, 1, 2]] * 3, single_setups=setups, names=["SSIcov"])
res = msp.merge_results()["SSIcov"]
print(np.round(res.Fn, 3))
print(res.setups_used)
print(len(msp.merge_results(mac_min=0.0)["SSIcov"].Fn), "modes with mac_min=0.0")
Before (9 modes) After (10 modes) setups_used
2.632 2.632 ss1, ss2, ss3
2.691 2.691 ss1, ss2, ss3
3.425 3.425 ss1, ss2, ss3
8.294 8.294 ss1, ss2, ss3
8.430 8.430 ss1, ss2, ss3
10.607 10.607 ss1, ss2, ss3
14.004 13.953 ss1 only
14.086 14.037 ss1, ss2, ss3
17.429 17.429 ss1, ss2, ss3
14.103 ss2, ss3

Frequencies are unchanged except around 14 Hz, where the setups do not agree:

  • ss1 at 13.953 Hz moves mainly on the 1st and 3rd reference channels (reference values ≈ [-0.40, 0.08, -0.45]). All the other ~14 Hz modes move mainly on the 2nd reference channel (e.g. ss1 at 14.050 Hz ≈ [0.06, -0.51, 0.01]). Its MAC on the references with the ss2/ss3 modes it used to be averaged with is only 0.05 – 0.08, and at most 0.20 with any other ~14 Hz mode, so it is a different mode and is now kept separate.
    The old average (14.004 Hz) happens to be very close to the FE value (14.0053 Hz), but its merged mode shape mixed two different modes.
  • ss2 picks the same pole twice. With sel_freq=[..., 14.00, 14.09, ...], ss2 returns 14.060 Hz for both, with identical shapes. So ss2 did not really identify the second mode of the pair. Its duplicate is now merged with ss3's 14.147 Hz mode (MAC 0.76) into the new 14.103 Hz mode.
    This is a picking issue in the example rather than in the merge. I did not change the example in this PR: the output printed on the Example 3 docs page already differs from the current output, so the page needs a refresh anyway.

The poles picked around 14 Hz depend on the SSI settings. For example, without decimation and with br = 45 / 30 / 30, step=2 and order_in=50, it is ss1 and ss3 that pick the same pole twice, and the extra merged mode is 13.971 Hz from ss2 only. The frequencies differ, but the conclusion is the same: the setups do not agree around 14 Hz, and the new merge keeps the modes that do not match separate instead of averaging them.

If you prefer the old behaviour for this dataset, merge_results(mac_min=0.0) gives back 9 merged modes. With that setting the shape no longer rejects a pair; it is only used to choose the best matching.

How to use

result = msp.merge_results()           # same call as before
res = result["SSIcov"]
res.Fn, res.Fn_std, res.Xi, res.Xi_std, res.Phi   # as before
res.setups_used                        # new: setups that identified each mode (0 = first setup)

# optional tuning of the pairing
msp.merge_results(freq_tol=0.05, mac_min=0.8)   # stricter

Other small changes

  • Merging FDD results used to crash, because FDD does not estimate damping. It now works, and Xi / Xi_std are None.
  • Invalid pairing tolerances (e.g. freq_tol=0, or mac_min outside 0 – 1) now raise a clear error.
  • New tests reproduce each of the three problems (reordered modes, close modes, missing modes, rescaling) and check that the simple case gives the same result as before; they all fail on the current code and pass with this PR. The whole test suite passes.
  • Three internal zip() calls in dfphi_map_func were made explicit because the linter required it. This does not change behaviour.

Where I'd like your opinion

  1. Default thresholds. Frequency within 10 % and MAC ≥ 0.5 on the reference sensors. In Example 3, with 3 reference sensors, the genuine matches have MAC between 0.79 and 1.0, while clearly different modes within 10 % in frequency (the 2.63/2.69 and 8.29/8.42 Hz pairs) have MAC ≤ 0.15. Would you choose different defaults?
  2. Order of the merged modes. The order of the first setup is kept, and modes found only in later setups are added at the end. The alternative is sorting everything by frequency, but that would change the order of existing results.
  3. Example 3. Should I update the example (e.g. the ss2 picking around 14 Hz) and the printed output in the docs, in this PR or in a separate one?
  4. Follow-up, not in this PR. The general MSF helper does not use the complex conjugate, so it assumes nearly real mode shapes. This is fine for normalised SSI shapes, but I would fix it separately if you agree.

@dfm88
dfm88 requested a review from dagghe September 21, 2026 12:56
…ults

MultiSetup_PoSER.merge_results() averaged the single-setup results by mode index,
scaled the roving sensors by the inverse Modal Scale Factor and failed when a mode
was missing in some setups.

Changes:
- Add gen.match_modes(): pairs the modes of each setup with the merged modes by linear
  assignment on relative frequency difference and reference-sensor MAC, gated by
  freq_tol and mac_min (ValueError if not positive / not in [0, 1]); unpaired modes
  are appended as new merged modes
- merge_mode_shapes() scales each setup onto the first setup's reference sensors
  (MSF arguments were swapped) and treats all-NaN columns as missing modes
- merge_results() averages each mode over the setups that identified it, accepts
  freq_tol/mac_min and records MsPoserResult.setups_used
- Xi/Xi_std are None for algorithms without damping (FDD) instead of raising
- Aligned modes still reproduce the previous mean/std merge
- Add tests for reordered, close, mismatched and missing modes, mode-shape scaling,
  tolerance validation and the backward-compatible aligned case
- Pass strict=True to the same-DataFrame zip() calls in dfphi_map_func (ruff B905,
  flagged by the pre-commit hook on the touched module)

Using the single-setup SSI uncertainties in the merge is left to a separate issue.
@dfm88 dfm88 changed the title Fix PoSER merging: mode pairing, missing modes, uncertainty weighting and mode-shape scaling Fix PoSER merging: mode pairing, missing modes and mode-shape scaling Sep 22, 2026
@dfm88
dfm88 merged commit dbaeaf6 into main Sep 22, 2026
18 checks passed
@dfm88
dfm88 deleted the fix/poser-merge branch September 22, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant