'''
pyOMA - A toolbox for Operational Modal Analysis
Copyright (C) 2015 - 2025 Simon Marwitz, Volkmar Zabel, Andrei Udrea et al.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Created on Apr 20, 2017
@author: womo1998
Multi-Setup Merging PoSER
for each setup provide:
prep_signals -> PreProcessSignals: chan_dofs, ref_channels, roving_channels
modal_data -> modal_frequencies, modal_damping, mode_shapes
stabil_data -> select_modes
changed/new variables:
- chan_dofs
- modal_frequencies
- modal_damping
- std_frequencies
- std_damping
- mode_shapes
- select_modes -> actually a dummy
- ref_channels, roving_channels
in PoGer/PreGer merging
modal_data -> modal_frequencies, modal_damping, mode_shapes, chan_dofs, ref_channels, roving_channels
stabil_data -> select_modes
PlotMSH (or other postprocessing routines) have to distinguish these three cases:
single-setup (prep_signals, modal_data, stabil_data)
poger/preger multi-setup (modal_data, stabil_data)
poser multi-setup (merged_data)
'''
import numpy as np
import datetime
from .PreProcessingTools import PreProcessSignals
from .ModalBase import ModalBase
from .StabilDiagram import StabilCalc
from .Helpers import calculateMAC, calculateMPC, calculateMPD
import os
import logging
logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)
[docs]
class MergePoSER(object):
'''
classdocs
'''
[docs]
def __init__(self,):
'''
Constructor
'''
self.setups = []
self.merged_chan_dofs = []
self.merged_num_channels = None
self.mean_frequencies = None
self.mean_damping = None
self.merged_mode_shapes = None
self.std_frequencies = None
self.std_damping = None
self.setup_name = 'merged_poser'
self.start_time = datetime.datetime.now()
self.state = [False, False]
def add_setup(
self,
prep_signals,
modal_data,
stabil_data,
override_ref_channels=None):
# does not check, if same method was used for each setup, also anaylsis
# parameters should be similar
if override_ref_channels:
raise RuntimeWarning('This function is not implemented yet!')
assert isinstance(prep_signals, PreProcessSignals)
assert isinstance(modal_data, ModalBase)
assert isinstance(stabil_data, StabilCalc)
# assure objects belong to the same setup
assert prep_signals.setup_name == modal_data.setup_name
assert modal_data.setup_name == stabil_data.setup_name
# assure chan_dofs were assigned
assert prep_signals.chan_dofs
# assure modes were selected
assert stabil_data.select_modes
# extract needed information and store them in a dictionary
self.setups.append({'setup_name': prep_signals.setup_name,
'chan_dofs': prep_signals.chan_dofs,
'num_channels': prep_signals.num_analised_channels,
'ref_channels': prep_signals.ref_channels,
'modal_frequencies': [modal_data.modal_frequencies[index] for index in stabil_data.select_modes],
'modal_damping': [modal_data.modal_damping[index] for index in stabil_data.select_modes],
'mode_shapes': [modal_data.mode_shapes[:, index[1], index[0]] for index in stabil_data.select_modes]
})
self.start_time = min(self.start_time, prep_signals.start_time)
print(
'Added setup "{}" with {} channels and {} selected modes.'.format(
prep_signals.setup_name, prep_signals.num_analised_channels, len(
stabil_data.select_modes)))
self.state[0] = True
[docs]
def merge(self, base_setup_num=0, mode_pairing=None):
'''
generate new_chan_dofs
assign modes from each setup
::
for each mode:
for each setup:
rescale
merge
.. TODO::
* rescale w.r.t to the average solution from all setups rather than specifying a base setup
* compute scaling factors for each setup with each setup and average them for each setup before rescaling
* corresponding standard deviations can be used to asses the quality of fit
'''
def pair_modes(frequencies_1, frequencies_2):
delta_matrix = np.ma.array(
np.zeros((len(frequencies_1), len(frequencies_2))))
for index, frequency in enumerate(frequencies_1):
delta_matrix[index,:] = np.abs(
(frequencies_2 - frequency) / frequency)
mode_pairs = []
while True:
row, col = np.unravel_index(
np.argmin(delta_matrix), delta_matrix.shape)
# TODO:: this code is useless: it always continues to the end and sets del_col to True
for col_ind in range(delta_matrix.shape[1]):
if col_ind == col:
continue
if np.argmin(delta_matrix[:, col_ind]) == row:
del_col = False
else:
del_col = True
# TODO:: this code is useless: it always continues to the end and sets del_row to True
for row_ind in range(delta_matrix.shape[0]):
if row_ind == row:
continue
if np.argmin(delta_matrix[row_ind,:]) == col:
del_row = False
else:
del_row = True
if del_col and del_row:
delta_matrix[row,:] = np.ma.masked
delta_matrix[:, col] = np.ma.masked
mode_pairs.append((row, col))
if len(mode_pairs) == len(frequencies_1):
break
if len(mode_pairs) == len(frequencies_2):
break
return mode_pairs
setups = self.setups
# get values from base instance
chan_dofs_base = setups[base_setup_num]['chan_dofs']
num_channels_base = setups[base_setup_num]['num_channels']
mode_shapes_base = setups[base_setup_num]['mode_shapes']
frequencies_base = setups[base_setup_num]['modal_frequencies']
damping_base = setups[base_setup_num]['modal_damping']
del setups[base_setup_num]
# pair channels and modes of each instance with base instance
channel_pairing = []
if mode_pairing is None:
auto_pairing = True
mode_pairing = []
else:
auto_pairing = False
print('The provided mode pairs will be applied without any further checks.')
total_dofs = 0
total_dofs += num_channels_base
for setup in setups:
# calculate the common reference dofs, which may be different channels
# furthermore reference channels for covariances need not be the reference channels for mode merging
# channel dof assignments have to be present in each of the
# instances
chan_dofs_this = setup['chan_dofs']
num_channels_this = setup['num_channels']
these_pairs = []
for chan_dof_base in chan_dofs_base:
chan_base, node_base, az_base, elev_base = chan_dof_base[0:4]
for chan_dof_this in chan_dofs_this:
chan_this, node_this, az_this, elev_this = chan_dof_this[0:4]
if node_this == node_base and az_this == az_base and elev_this == elev_base:
these_pairs.append((chan_base, chan_this))
channel_pairing.append(these_pairs)
total_dofs += num_channels_this - len(these_pairs)
# calculate the mode pairing by minimal frequency difference
# check that number of modes is equal in all instances (not necessarily)
# assert len(self.selected_modes_indices) == len(instance.selected_modes_indices)
if auto_pairing:
frequencies_this = setup['modal_frequencies']
mode_pairs = pair_modes(frequencies_base, frequencies_this)
mode_pairing.append(mode_pairs)
# delete modes not common to all instance from mode pairing
for mode_num in range(len(frequencies_base) - 1, -1, -1):
in_all = True
for mode_pairs in mode_pairing:
for mode_pair in mode_pairs:
if mode_pair[0] == mode_num:
break
else:
in_all = False
break
if in_all:
continue
for mode_pairs in mode_pairing:
while True:
for index, mode_pair in enumerate(mode_pairs):
if mode_pair[0] == mode_num:
del mode_pairs[index]
break
else:
break
lengths = [len(mode_pairs) for mode_pairs in mode_pairing]
common_modes = min(lengths)
new_mode_nums = [mode_num[0] for mode_num in mode_pairing[0]]
# allocate output objects
mode_shapes = np.zeros((total_dofs, 1, common_modes), dtype=complex)
f_list = np.zeros((len(setups) + 1, common_modes))
d_list = np.zeros((len(setups) + 1, common_modes))
scale_factors = np.zeros((len(setups), common_modes), dtype=complex)
start_dof = 0
# copy modal values from base instance first
for mode_num_base, mode_num_this in mode_pairing[0]:
# for mode_num_base in range(common_modes):
mode_index = new_mode_nums.index(mode_num_base)
mode_base = mode_shapes_base[mode_num_base]
mode_shapes[start_dof:start_dof +
num_channels_base, 0, mode_index, ] = mode_base
f_list[0, mode_index] = frequencies_base[mode_num_base]
d_list[0, mode_index] = damping_base[mode_num_base]
start_dof += num_channels_base
# iterate over instances and assemble output objects (mode_shapes,
# chan_dofs)
for setup_num, setup in enumerate(setups):
chan_dofs_this = setup['chan_dofs']
num_channels_this = setup['num_channels']
mode_shapes_this = setup['mode_shapes']
these_pairs = channel_pairing[setup_num]
num_ref_channels = len(these_pairs)
num_remain_channels = num_channels_this - num_ref_channels
ref_channels_base = [pair[0] for pair in these_pairs]
ref_channels_this = [pair[1] for pair in these_pairs]
print('Next Instance', ref_channels_base, ref_channels_this)
# create 0,1 matrices to extract and reorder channels from base
# instance and this instance
split_mat_refs_base = np.zeros(
(num_ref_channels, num_channels_base))
split_mat_refs_this = np.zeros(
(num_ref_channels, num_channels_this))
split_mat_rovs_this = np.zeros(
(num_remain_channels, num_channels_this))
row_ref = 0
for channel in range(num_channels_base):
if channel in ref_channels_base:
split_mat_refs_base[row_ref, channel] = 1
row_ref += 1
row_ref = 0
row_rov = 0
# print(instance)
for channel in range(num_channels_this):
if channel in ref_channels_this:
split_mat_refs_this[row_ref, channel] = 1
row_ref += 1
else:
split_mat_rovs_this[row_rov, channel] = 1
for chan_dof_this in chan_dofs_this:
chan, node, az, elev = chan_dof_this[0:4]
if chan == channel:
chan = int(start_dof + row_rov)
chan_dofs_base.append([chan, node, az, elev])
row_rov += 1
# loop over modes and rescale them and merge with the other
# instances
for mode_num_base, mode_num_this in mode_pairing[setup_num]:
mode_index = new_mode_nums.index(mode_num_base)
mode_base = mode_shapes_base[mode_num_base]
mode_refs_base = np.dot(split_mat_refs_base, mode_base)
mode_this = mode_shapes_this[mode_num_this]
mode_refs_this = np.dot(split_mat_refs_this, mode_this)
mode_rovs_this = np.dot(split_mat_rovs_this, mode_this)
numer = np.dot(
np.transpose(
np.conjugate(mode_refs_this)),
mode_refs_base)
denom = np.dot(
np.transpose(
np.conjugate(mode_refs_this)),
mode_refs_this)
scale_fact = numer / denom
scale_factors[setup_num, mode_index] = (scale_fact)
mode_shapes[start_dof:start_dof + num_remain_channels,
0, mode_index] = scale_fact * mode_rovs_this
f_list[setup_num + 1,
mode_index] = setup['modal_frequencies'][mode_num_this]
d_list[setup_num + 1,
mode_index] = setup['modal_damping'][mode_num_this]
start_dof += num_remain_channels
mean_frequencies = np.zeros((common_modes,))
std_frequencies = np.zeros((common_modes,))
mean_damping = np.zeros((common_modes,))
std_damping = np.zeros((common_modes,))
for mode_num_base, mode_num_this in mode_pairing[0]:
mode_index = new_mode_nums.index(mode_num_base)
# rescaling of mode shape
mode_tmp = mode_shapes[:, 0, mode_index]
abs_mode_tmp = np.abs(mode_tmp)
index_max = np.argmax(abs_mode_tmp)
this_max = mode_tmp[index_max]
mode_tmp = mode_tmp / this_max
mode_shapes[:, 0, mode_index] = mode_tmp
mean_frequencies[mode_index, ] = np.mean(
f_list[:, mode_index], axis=0)
std_frequencies[mode_index, ] = np.std(
f_list[:, mode_index], axis=0)
mean_damping[mode_index, ] = np.mean(d_list[:, mode_index], axis=0)
std_damping[mode_index, ] = np.std(d_list[:, mode_index], axis=0)
self.merged_chan_dofs = chan_dofs_base
self.merged_num_channels = total_dofs
self.merged_mode_shapes = mode_shapes
self.mean_frequencies = np.expand_dims(mean_frequencies, axis=1)
self.std_frequencies = np.expand_dims(std_frequencies, axis=1)
self.mean_damping = np.expand_dims(mean_damping, axis=1)
self.std_damping = np.expand_dims(std_damping, axis=1)
self.state[1] = True
def save_state(self, fname):
dirname, _ = os.path.split(fname)
if not os.path.isdir(dirname):
os.makedirs(dirname)
out_dict = {}
out_dict['self.state'] = self.state
out_dict['self.setup_name'] = self.setup_name
out_dict['self.start_time'] = self.start_time
if self.state[0]:
out_dict['self.setups'] = self.setups
if self.state[1]:
out_dict['self.merged_chan_dofs'] = self.merged_chan_dofs
out_dict['self.merged_num_channels'] = self.merged_num_channels
out_dict['self.merged_mode_shapes'] = self.merged_mode_shapes
out_dict['self.mean_frequencies'] = self.mean_frequencies
out_dict['self.std_frequencies'] = self.std_frequencies
out_dict['self.mean_damping'] = self.mean_damping
out_dict['self.std_damping'] = self.std_damping
np.savez_compressed(fname, **out_dict)
@classmethod
def load_state(cls, fname):
print('Now loading previous results from {}'.format(fname))
in_dict = np.load(fname, allow_pickle=True)
if 'self.state' in in_dict:
state = list(in_dict['self.state'])
else:
return
for this_state, state_string in zip(state, ['Setups added',
'Setups merged',
]):
if this_state:
print(state_string)
postprocessor = cls()
setup_name = str(in_dict['self.setup_name'].item())
start_time = in_dict['self.start_time'].item()
postprocessor.setup_name = setup_name
postprocessor.start_time = start_time
if state[0]:
postprocessor.setups = list(in_dict['self.setups'])
if state[1]:
postprocessor.merged_chan_dofs = [[int(float(chan_dof[0])),
str(chan_dof[1]),
float(chan_dof[2]),
float(chan_dof[3]),
str(chan_dof[-1])] for chan_dof in in_dict['self.merged_chan_dofs']]
postprocessor.merged_num_channels = in_dict['self.merged_num_channels']
postprocessor.merged_mode_shapes = in_dict['self.merged_mode_shapes']
postprocessor.mean_frequencies = in_dict['self.mean_frequencies']
postprocessor.std_frequencies = in_dict['self.std_frequencies']
postprocessor.mean_damping = in_dict['self.mean_damping']
postprocessor.std_damping = in_dict['self.std_damping']
return postprocessor
def export_results(self, fname, binary=False):
selected_freq = self.mean_frequencies
selected_damp = self.mean_damping
num_modes = len(selected_freq)
selected_MPC = calculateMPC(
self.merged_mode_shapes[:, 0,:])
selected_MP, selected_MPD = calculateMPD(
self.merged_mode_shapes[:, 0,:])
selected_stdf = self.std_frequencies
selected_stdd = self.std_damping
selected_modes = self.merged_mode_shapes
freq_str = ''
damp_str = ''
ord_str = ''
msh_str = ''
mpc_str = ''
mp_str = ''
mpd_str = ''
std_freq_str = ''
std_damp_str = ''
for col in range(num_modes):
freq_str += '{:3.3f} \t\t'.format(selected_freq[col, 0])
damp_str += '{:3.3f} \t\t'.format(selected_damp[col, 0])
mpc_str += '{:3.3f}\t \t'.format(selected_MPC[col])
mp_str += '{:3.2f} \t\t'.format(selected_MP[col])
mpd_str += '{:3.2f} \t\t'.format(selected_MPD[col])
std_damp_str += '{:3.3e} \t\t'.format(selected_stdd[col, 0])
std_freq_str += '{:3.3e} \t\t'.format(selected_stdf[col, 0])
for row in range(num_modes):
msh_str += '\n \t\t'
for col in range(self.merged_num_channels):
msh_str += '{:+3.4f} \t'.format(selected_modes[col, 0, row])
export_modes = 'MANUAL MODAL ANALYSIS\n'
export_modes += '=======================\n'
export_modes += 'Frequencies [Hz]:\t' + freq_str + '\n'
export_modes += 'Standard deviations of the Frequencies [Hz]:\t' + \
std_freq_str + '\n'
export_modes += 'Damping [%]:\t\t' + damp_str + '\n'
export_modes += 'Standard deviations of the Damping [%]:\t' + \
std_damp_str + '\n'
export_modes += 'Mode shapes:\t\t' + msh_str + '\n'
export_modes += 'Model order:\t\t' + ord_str + '\n'
export_modes += 'MPC [-]:\t\t' + mpc_str + '\n'
export_modes += 'MP [\u00b0]:\t\t' + mp_str + '\n'
export_modes += 'MPD [-]:\t\t' + mpd_str + '\n\n'
dirname, _ = os.path.split(fname)
if not os.path.isdir(dirname):
os.makedirs(dirname)
if binary:
out_dict = {'selected_freq': selected_freq,
'selected_damp': selected_damp}
out_dict['selected_MPC'] = selected_MPC
out_dict['selected_MP'] = selected_MP
out_dict['selected_MPD'] = selected_MPD
out_dict['selected_modes'] = selected_modes
out_dict['selected_stdf'] = selected_stdf
out_dict['selected_stdd'] = selected_stdd
np.savez_compressed(fname, **out_dict)
else:
f = open(fname, 'w')
f.write(export_modes)
f.close()
[docs]
def pair_modes(freq_a, freq_b,
shapes_a, shapes_b,
freq_thresh=0.2, mac_thresh=0.8):
'''
A function to pair two sets of modes (here: a and b) based on frequency
differences and mode shape similarity. The number of modes in both sets may
be different and relative complements of both arrays may be non-empty.
The threshold where pairing stops is based on normalized frequency differences
AND modal assurance criteria.
Parameters
----------
f_a, f_b: np.ndarray
Arrays holding the natural frequencies of both sets of modes. The
dimension (number of modes) of both sets can be different.
d_a, d_b: np.ndarray
Arrays holding the damping ratios of both sets of modes. The dimension
(number of modes) of both sets can be different.
phi_a, phi_b: np.ndarray
Arrays holding the mode shapes of both sets of modes. The first
dimension is the number of channels, that must match in both arrays.
Other Parameters
----------------
kwargs :
Additional kwargs are passed to pair_modes
Returns
-------
inds_a, inds_b: np.ndarray,
Arrays holding the indices of paired modes sorted by ascending
frequencies (set a). Length represents the number of common modes.
unp_a, unp_b: np.ndarray
Arrays holding the indices of modes that could not be paired
'''
shape = (len(freq_a), len(freq_b))
delta_matrix = np.ma.array(np.zeros(shape), mask=np.zeros(shape))
for index, frequency in enumerate(freq_a):
delta_matrix[index,:] = np.abs(
(freq_b - frequency) / (0.5 * (freq_b + frequency)))
# mask all nan values, to reduce number of checks later
delta_matrix.mask = np.isnan(delta_matrix)
mac_matrix = calculateMAC(shapes_a, shapes_b)
# indices and sizes of delta_matrix and mac_matrix should be equal
indices_a = []
indices_b = []
delta_values = []
mac_values = []
while ~np.all(delta_matrix.mask):
# find index of smallest frequency difference
row, col = np.unravel_index(
np.nanargmin(delta_matrix), delta_matrix.shape)
# if another column contains a minimal value in the same row
# do not mask the column
for col_ind in range(delta_matrix.shape[1]):
if col_ind == col:
continue
if delta_matrix[:, col_ind].mask.all():
continue
if np.nanargmin(delta_matrix[:, col_ind]) == row:
del_col = False
break
else:
del_col = True
col_ind = col
# if another row contains a minimal value in the same column
# do not mask the row
for row_ind in range(delta_matrix.shape[0]):
if row_ind == row:
continue
if delta_matrix[row_ind,:].mask.all():
continue
if np.nanargmin(delta_matrix[row_ind,:]) == col:
del_row = False
break
else:
del_row = True
row_ind = row
if not logger.isEnabledFor(logging.DEBUG):
debug_str = ''
else:
debug_str = f"Current Minimum at {row}:{col}, "
# in the case, where we might discard a candidate for a good match for another mode
# we use the modal assurance criterion to decide which mode to discard
# counter-intuitively that should be the best matching mode of several candidates
if not (del_row and del_col): # a or b must be false
# both members of the selected pair also have another close match
# which of the three candidates has the best MAC value?
best = np.nanargmax([mac_matrix[row_ind, col], mac_matrix[row, col_ind], mac_matrix[row, col]])
if best == 0:
# another row (row_ind) contains a minimal value in the same column
row = row_ind
debug_str += f'Chose alternative match for "Mode A" at {row_ind}, '
elif best == 1:
# another column (col_ind) contains a minimal value in the same row
col = col_ind
debug_str += f'Chose alternative match for "Mode B" at {col_ind}, '
elif not del_row:
# initial mode is better candidate
debug_str += f'Reject alternative match for "Mode A" at {row_ind}, '
elif not del_col:
# initial mode is better candidate
debug_str += f'Reject alternative match for "Mode B" at {col_ind}, '
# no alternative candidates found for current pair
elif del_row and del_col:
pass
# this will never trigger. keep it in case of future modifications
else:
raise RuntimeError('Caught in a loop')
if logger.isEnabledFor(logging.DEBUG):
if delta_matrix[row, col] < freq_thresh or mac_matrix[row, col] > mac_thresh:
debug_str += "Thresholds are within limits for: "
if delta_matrix[row, col] < freq_thresh:
debug_str += "freq, "
if mac_matrix[row, col] > mac_thresh:
debug_str += "mac, "
else:
debug_str += "Thresholds are out of limits, "
# within threshold limits -> select modepair
if delta_matrix[row, col] < freq_thresh and mac_matrix[row, col] > mac_thresh:
if logger.isEnabledFor(logging.DEBUG):
debug_str += "Selecting candidate."
delta_values.append(delta_matrix[row, col])
mac_values.append(mac_matrix[row, col])
indices_a.append(row)
indices_b.append(col)
# threshold are out of limits -> reject modepair
elif logger.isEnabledFor(logging.DEBUG):
debug_str += "Rejecting candidate."
# in either case mask row/column to not get caught in a loop
delta_matrix[row,:] = np.ma.masked
delta_matrix[:, col] = np.ma.masked
logger.debug(debug_str)
# now sort according to ascending numerical frequencies
sort_inds = np.argsort(freq_a[indices_a])
indices_a = np.array(indices_a)[sort_inds]
indices_b = np.array(indices_b)[sort_inds]
unp_a = [i for i in range(len(freq_a)) if i not in indices_a]
unp_b = [i for i in range(len(freq_b)) if i not in indices_b]
return indices_a, indices_b, unp_a, unp_b
[docs]
def compare_modes(f_a, d_a, phi_a, f_b, d_b, phi_b, **kwargs):
'''
Compares two sets of modes (set a and set b) by first pairing them and then displaying
statistics on the identified pairs and a full MAC matrix for manual assessment.
Parameters
----------
f_a, f_b: np.ndarray
Arrays holding the natural frequencies of both sets of modes. The
dimension (number of modes) of both sets can be different.
d_a, d_b: np.ndarray
Arrays holding the damping ratios in percent of both sets of modes. The dimension
(number of modes) of both sets can be different.
phi_a, phi_b: np.ndarray
Arrays holding the mode shapes of both sets of modes. The first
dimension is the number of channels, that must match in both arrays.
Other Parameters
----------------
kwargs :
Additional kwargs are passed to pair_modes
Returns
-------
inds_a, inds_b: np.ndarray
Arrays holding the indices of paired modes
unp_a, unp_b: np.ndarray
Arrays holding the indices of modes that could not be paired
'''
import matplotlib.pyplot as plt
inds_a, inds_b, unp_a, unp_b = pair_modes(f_a, f_b, phi_a, phi_b, **kwargs)
if inds_a.shape[0] == 0:
logger.warning('Could not match any modes. Consider raising freq_thresh or lowering mac_thresh.')
return inds_a, inds_b, unp_a, unp_b
if np.max(d_a) <= 1:
logger.warning('First set damping values do not seem to be given in percent.')
if np.max(d_b) <= 1:
logger.warning('Second set damping values do not seem to be given in percent.')
all_inds_b = np.concatenate((inds_b, unp_b))
corr_inds_a = np.ma.concatenate([np.ma.array(inds_a, mask=np.zeros_like(inds_a, dtype=bool)), np.ma.array(np.zeros_like(unp_b), mask=np.ones_like(unp_b, dtype=bool), dtype=int)])
# indices of "modes 1" in the order of "modes 2" (for each mode 2 the index of mode 1 of nan)
corr_inds_a_sort = corr_inds_a[np.argsort(all_inds_b)]
freqs_a_corr = np.ma.array(f_a[corr_inds_a_sort],
mask=corr_inds_a_sort.mask,
fill_value=np.nan
).filled()
damps_a_corr = np.ma.array(d_a[corr_inds_a_sort],
mask=corr_inds_a_sort.mask,
fill_value=np.nan
).filled()
msh_a_corr = np.ma.array(phi_a[:, corr_inds_a_sort],
mask=np.repeat(np.ma.getmaskarray(corr_inds_a_sort)[np.newaxis,:], phi_a.shape[0], axis=0),
fill_value=np.nan
).filled()
freq_diffs = freqs_a_corr - f_b
damp_diffs = damps_a_corr - d_b
mac_matrix = calculateMAC(msh_a_corr, phi_b)
macs = np.diag(mac_matrix)
# create the alpha mask: put 0.5 into every row corresponding to unp 1 and every column corresponding to unp 2
mac_matrix = calculateMAC(phi_a, phi_b)
alpha_mask = np.ones_like(mac_matrix)
alpha_mask[unp_a,:] = 0.25
alpha_mask[:, unp_b] = 0.25
plt.matshow(mac_matrix, alpha=alpha_mask, cmap='viridis_r', vmin=0, vmax=1)
plt.yticks(ticks=np.arange(f_a.shape[0]), labels=[f"{v:1.2f} Hz" for v in f_a])
plt.xticks(ticks=np.arange(f_b.shape[0]), labels=[f"{v:1.2f} Hz" for v in f_b], rotation=90)
plt.scatter(inds_b, inds_a, color='r', marker='+')
logger.info(f'''Statistics on identification:
Δf = {np.nanmean(freq_diffs):1.3f}± {np.nanstd(freq_diffs):1.3f},
Δd = {np.nanmean(damp_diffs):1.3f}± {np.nanstd(damp_diffs):1.3f},
MAC: mean = {np.nanmean(macs):1.3f}, min= {np.nanmin(macs):1.3f},
Number of unmatched modes: "a" {len(unp_a)}, "b" {len(unp_b)}''')
plt.figure()
plt.plot(f_a, d_a, marker='x', color='black', ls='none')
plt.plot(f_b, d_b, marker='+', color='black', ls='none')
for ind_a, ind_b in zip(inds_a, inds_b):
fs = (f_a[ind_a], f_b[ind_b])
ds = (d_a[ind_a], d_b[ind_b])
plt.plot(fs, ds, color='red')
plt.annotate(f'''Statistics on identification:
Δf = {np.nanmean(freq_diffs):1.3f}± {np.nanstd(freq_diffs):1.3f},
Δd = {np.nanmean(damp_diffs):1.3f}± {np.nanstd(damp_diffs):1.3f},
MAC: mean = {np.nanmean(macs):1.3f}, min= {np.nanmin(macs):1.3f},
Number of unmatched modes: "a" {len(unp_a)}, "b" {len(unp_b)}''',
(0.55, 0.7), xycoords='figure fraction')
return inds_a, inds_b, unp_a, unp_b
[docs]
def main():
from pyOMA.core.PreProcessingTools import GeometryProcessor
from pyOMA.core.SSICovRef import BRSSICovRef
from pyOMA.core.PlotMSH import ModeShapePlot
from pyOMA.GUI.PlotMSHGUI import start_msh_gui
working_dir = '/home/womo1998/Projects/2017_modal_merging_test_files/'
interactive = False
merger = MergePoSER()
geometry_data = GeometryProcessor.load_geometry(
nodes_file=working_dir + 'macec/grid_full.asc',
lines_file=working_dir + 'macec/beam_full.asc')
setups = ['meas_1', 'meas_2']
for setup in setups:
result_folder = working_dir + setup
prep_signals = PreProcessSignals.load_state(result_folder + 'prep_signals.npz')
modal_data = BRSSICovRef.load_state(
result_folder + 'modal_data.npz', prep_signals)
stabil_data = StabilCalc.load_state(
result_folder + 'stabi_data.npz', modal_data)
merger.add_setup(prep_signals, modal_data, stabil_data)
merger.merge()
if interactive:
mode_shape_plot = ModeShapePlot(merger, geometry_data)
start_msh_gui(mode_shape_plot)
merger.save_state(result_folder + 'merged_setups.npz')
merger.export_results(result_folder + 'merged_results.txt', binary=False)
if __name__ == '__main__':
main()