Coverage for src / sdynpy / modal / sdynpy_polypy.py: 10%

662 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-11 16:22 +0000

1# -*- coding: utf-8 -*- 

2""" 

3Implementation of a multi-reference polynomial curve fitter for Python 

4""" 

5""" 

6Copyright 2022 National Technology & Engineering Solutions of Sandia, 

7LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. 

8Government retains certain rights in this software. 

9 

10This program is free software: you can redistribute it and/or modify 

11it under the terms of the GNU General Public License as published by 

12the Free Software Foundation, either version 3 of the License, or 

13(at your option) any later version. 

14 

15This program is distributed in the hope that it will be useful, 

16but WITHOUT ANY WARRANTY; without even the implied warranty of 

17MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18GNU General Public License for more details. 

19 

20You should have received a copy of the GNU General Public License 

21along with this program. If not, see <https://www.gnu.org/licenses/>. 

22""" 

23 

24import os 

25 

26import numpy as np 

27from ..core.sdynpy_data import TransferFunctionArray, GUIPlot 

28from ..signal_processing.sdynpy_complex import collapse_complex_to_real 

29from ..core.sdynpy_geometry import Geometry 

30from ..core.sdynpy_shape import ShapeArray 

31from .sdynpy_modeshape import compute_shapes_multireference 

32import matplotlib.pyplot as plt 

33import matplotlib.cm as cm 

34import traceback 

35 

36from qtpy import QtWidgets, uic 

37from qtpy.QtWidgets import QMainWindow, QTableWidgetItem, QWidget 

38import pyqtgraph 

39pyqtgraph.setConfigOption('background', 'w') 

40pyqtgraph.setConfigOption('foreground', 'k') 

41 

42 

43class PolyPy: 

44 

45 def __init__(self, frfs: TransferFunctionArray, min_frequency=None, 

46 max_frequency=None, displacement_derivative=2): 

47 self.frfs = frfs.reshape_to_matrix() 

48 self.min_frequency = min_frequency 

49 self.max_frequency = max_frequency 

50 self.displacement_derivative = displacement_derivative 

51 abscissa_indices = np.ones(self.frequencies.shape, dtype=bool) 

52 if min_frequency is not None: 

53 abscissa_indices &= (self.frequencies >= min_frequency) 

54 if max_frequency is not None: 

55 abscissa_indices &= (self.frequencies <= max_frequency) 

56 abscissa = self.frequencies[abscissa_indices] 

57 freq_range = np.array((np.min(abscissa), np.max(abscissa))) 

58 index_range = np.argmin(np.abs(self.frequencies - freq_range[:, np.newaxis]), axis=1) 

59 self.frequency_slice = slice(index_range[0], index_range[1] + 1) 

60 

61 def compute_poles(self, polynomial_orders, weighting=None, 

62 frequency_stability_threshold=0.01, 

63 damping_stability_threshold=0.1, 

64 modal_participation_threshold=0.15): 

65 H = self.frfs.ordinate[..., self.frequency_slice] 

66 num_output, num_input, num_freq = H.shape 

67 num_poles = int(polynomial_orders[-1]) 

68 omegas = self.angular_frequencies[self.frequency_slice] 

69 

70 # Convert frf to acceleration for pole fitting 

71 H *= (1j * omegas)**(2 - self.displacement_derivative) 

72 

73 omega0 = omegas[0] 

74 sample_time = np.pi / (omegas[-1] - omega0) 

75 omegas = omegas - omega0 

76 if weighting is None: 

77 weighting = np.ones((num_freq, num_output)) 

78 else: 

79 weighting = weighting[self.frequency_slice] 

80 

81 if isinstance(polynomial_orders, (int, np.integer)): 

82 polynomial_orders = (np.arange(np.floor(polynomial_orders / 

83 num_input).astype(int)) + 1)[1:] * num_input 

84 

85 self.polynomial_orders = np.flip(polynomial_orders) 

86 self.weighting = weighting 

87 self.frequency_stability_threshold = frequency_stability_threshold 

88 self.damping_stability_threshold = damping_stability_threshold 

89 self.modal_participation_threshold = modal_participation_threshold 

90 

91 pole_dtype = np.dtype([('omega', float), ('zeta', float), ('Lr_real', float, (num_input,)), 

92 ('Lr_complex', complex, (num_input,)), ('freq_stable', bool), ('damp_stable', bool), 

93 ('part_stable', bool)]) 

94 

95 self.pole_list = [] 

96 

97 Omega = np.exp(-1j * omegas[:, np.newaxis] * sample_time * np.arange(num_poles + 1)) 

98 

99 RTot = np.zeros([num_poles + 1, num_poles + 1, num_output]) 

100 STot = np.zeros([num_poles + 1, num_input * (num_poles + 1), num_output]) 

101 TTot = np.zeros([num_input * (num_poles + 1), num_input * (num_poles + 1), num_output]) 

102 

103 for j_output in range(num_output): 

104 print('Accumulating Data: Progress = {:0.2f}%'.format(j_output / num_output * 100.0)) 

105 Xj = weighting[:, j_output][..., np.newaxis] * Omega 

106 Hj = H[j_output, ...] 

107 Yj = np.array([-np.kron(Xk, Hk) for Xk, Hk in zip(Xj, Hj.transpose())]) 

108 Rj = np.real(Xj.conjugate().transpose() @ Xj) 

109 Sj = np.real(Xj.conjugate().transpose() @ Yj) 

110 Tj = np.real(Yj.conjugate().transpose() @ Yj) 

111 RTot[:, :, j_output] = Rj 

112 STot[:, :, j_output] = Sj 

113 TTot[:, :, j_output] = Tj 

114 

115 for i_order, order in enumerate(self.polynomial_orders): 

116 

117 print('Solving for {:} roots ({:} of {:})'.format( 

118 order, i_order + 1, len(polynomial_orders))) 

119 

120 RTot = RTot[0:order + 1, 0:order + 1, :] 

121 STot = STot[0:order + 1, 0:num_input * (order + 1), :] 

122 TTot = TTot[0:num_input * (order + 1), 0:num_input * (order + 1), :] 

123 

124 M = np.zeros(np.shape(TTot)[0:2]) 

125 for Rj, Sj, Tj in zip(RTot.transpose([2, 0, 1]), STot.transpose([2, 0, 1]), TTot.transpose([2, 0, 1])): 

126 M = M + Tj - Sj.transpose() @ np.linalg.solve(Rj, Sj) 

127 

128 A = M[:order * num_input, :order * num_input] 

129 B = -M[:order * num_input, order * num_input:] 

130 

131 alpha = np.linalg.solve(A, B) 

132 Ac_top_left = np.zeros([(order - 1) * num_input, num_input]) 

133 Ac_top_right = np.eye((order - 1) * num_input) 

134 Ac_bottom = -alpha.transpose() 

135 

136 Ac = np.concatenate((np.concatenate((Ac_top_left, Ac_top_right), axis=1), 

137 Ac_bottom), axis=0) 

138 

139 zpoles, V = np.linalg.eig(Ac) 

140 spoles = -1 / sample_time * np.log(zpoles) 

141 Lr = V[-num_input:] 

142 keep_poles = (np.imag(spoles) > 0) & (np.real(spoles) < 0) 

143 poles = spoles[keep_poles] 

144 Lr = Lr[:, keep_poles] 

145 omgr = abs(poles) + omega0 

146 zetar = -np.real(poles) / omgr 

147 isort = np.argsort(omgr) 

148 omgr = omgr[isort] 

149 zetar = zetar[isort] 

150 Lr = Lr[:, isort] 

151 Lr_real = collapse_complex_to_real(Lr, axis=0, preserve_magnitude=True) 

152 

153 # Now check stability 

154 pole_list_i = np.zeros(omgr.size, pole_dtype) 

155 pole_list_i['omega'] = omgr 

156 pole_list_i['zeta'] = zetar 

157 pole_list_i['Lr_complex'] = (Lr.T) 

158 pole_list_i['Lr_real'] = (Lr_real.T) 

159 

160 self.pole_list.append(pole_list_i) 

161 

162 # Now analyze stability 

163 last_poles = None 

164 for i_order, (order, poles) in enumerate(zip(self.polynomial_orders[::-1], self.pole_list[::-1])): 

165 if i_order > 0: 

166 for i_pole, pole in enumerate(poles): 

167 previous_omegas = last_poles['omega'] 

168 if previous_omegas.size == 0: 

169 continue 

170 # Check frequency 

171 frequency_differences = np.abs( 

172 pole['omega'] - previous_omegas) / previous_omegas 

173 closest_freq_index = np.argmin(frequency_differences) 

174 if frequency_differences[closest_freq_index] < frequency_stability_threshold: 

175 poles['freq_stable'][i_pole] = True 

176 # Now check damping 

177 previous_zeta = last_poles['zeta'][closest_freq_index] 

178 if abs(pole['zeta'] - previous_zeta) / previous_zeta < damping_stability_threshold: 

179 poles['damp_stable'][i_pole] = True 

180 # Now check participation factor 

181 previous_Lr = last_poles['Lr_complex'][closest_freq_index] 

182 if np.all( 

183 (np.abs(np.abs(pole['Lr_complex']) - np.abs(previous_Lr)) / np.abs(previous_Lr) < modal_participation_threshold) | 

184 (np.abs(pole['Lr_complex']) < np.abs(previous_Lr).max()*1e-4)): # Added this to remove divide by zero issues 

185 poles['part_stable'][i_pole] = True 

186 last_poles = poles.copy() 

187 

188 def plot_stability(self, no_converge_marger='rx', 

189 freq_converge_marker='b^', 

190 damp_converge_marker='bs', 

191 full_converge_marker='go', 

192 label_poles=False, order_range=None, 

193 mif_type='cmif', *mif_args, **mif_kwargs): 

194 cmif = self.frfs.compute_mif(mif_type, *mif_args, **mif_kwargs) 

195 

196 ax = cmif.plot() 

197 

198 ax.set_yscale('log') 

199 ax_poles = ax.twinx() 

200 

201 for j, (order, poles) in enumerate(zip(self.polynomial_orders, self.pole_list)): 

202 if order_range is not None: 

203 if order < order_range[0]: 

204 continue 

205 if order > order_range[1]: 

206 continue 

207 frequencies = poles['omega'] / (2 * np.pi) 

208 freq_convergence = poles['freq_stable'] 

209 damp_convergence = poles['damp_stable'] 

210 part_convergence = poles['part_stable'] 

211 for i, (freq, fc, dc, pc) in enumerate(zip(frequencies, freq_convergence, damp_convergence, part_convergence)): 

212 if pc: 

213 marker = full_converge_marker 

214 elif dc: 

215 marker = damp_converge_marker 

216 elif fc: 

217 marker = freq_converge_marker 

218 else: 

219 marker = no_converge_marger 

220 ax_poles.plot(freq, order, marker, markerfacecolor='none') 

221 if label_poles: 

222 ax_poles.text(freq, order, '{:},{:}'.format(j, i)) 

223 freq = self.frequencies[self.frequency_slice] 

224 ax.set_xlim(freq.min(), freq.max()) 

225 return ax, ax_poles 

226 

227 def pole_list_from_indices(self, indices): 

228 index_list = [self.pole_list[order_index][pole_index, np.newaxis] 

229 for order_index, pole_index in indices] 

230 if len(index_list) == 0: 

231 return np.zeros((0,), dtype=self.pole_list[0].dtype) 

232 return np.concatenate(index_list) 

233 

234 def analyze_pole_convergence(self, pole_or_index, 

235 frequency_stability_threshold=0.01, 

236 damping_stability_threshold=0.1, 

237 modal_participation_threshold=0.15, 

238 subplots_kwargs={}, 

239 label_order=True, 

240 no_converge_marger='rx', 

241 freq_converge_marker='b^', 

242 damp_converge_marker='bs', 

243 full_converge_marker='go'): 

244 try: 

245 omega = pole_or_index['omega'] 

246 zeta = pole_or_index['zeta'] 

247 part = pole_or_index['Lr_complex'] 

248 except (IndexError, TypeError): 

249 pole_or_index = self.pole_list[pole_or_index[0]][pole_or_index[1]] 

250 omega = pole_or_index['omega'] 

251 zeta = pole_or_index['zeta'] 

252 part = pole_or_index['Lr_complex'] 

253 fig, ax = plt.subplots(2, 2, **subplots_kwargs) 

254 ax = ax.flatten() 

255 for poles, order in zip(self.pole_list, self.polynomial_orders): 

256 if len(poles) < 1: 

257 continue 

258 frequency_differences = np.abs(poles['omega'] - omega) / omega 

259 closest_freq_index = np.argmin(frequency_differences) 

260 pole = poles[closest_freq_index] 

261 if pole['part_stable']: 

262 marker = full_converge_marker 

263 elif pole['damp_stable']: 

264 marker = damp_converge_marker 

265 elif pole['freq_stable']: 

266 marker = freq_converge_marker 

267 else: 

268 marker = no_converge_marger 

269 for a, index in zip(ax[:3], ['omega', 'zeta', 'Lr_real']): 

270 a.plot(order * np.ones(pole[index].shape), 

271 pole[index], marker, markerfacecolor='none') 

272 if label_order: 

273 try: 

274 for val in pole[index]: 

275 a.text(order, val, '{:}'.format(order)) 

276 except TypeError: 

277 a.text(order, pole[index], '{:}'.format(order)) 

278 ax[-1].plot(pole['Lr_complex'].real, pole['Lr_complex'].imag, 

279 marker, markerfacecolor='none') 

280 if label_order: 

281 for val in pole['Lr_complex']: 

282 ax[-1].text(val.real, val.imag, '{:}'.format(order)) 

283 for a, title in zip(ax, ['Angular Frequency', 'Damping', 'Participation Factor (real)', 'Participation Factor (complex)']): 

284 a.set_title(title) 

285 

286 def compute_shapes(self, poles, 

287 low_residuals=True, high_residuals = True, real_modes=False, 

288 frequency_lines_at_resonance: int = None, 

289 frequency_lines_for_residuals: int = None 

290 ): 

291 self.natural_frequencies = poles['omega'] / (2 * np.pi) 

292 self.damping_ratios = poles['zeta'] 

293 self.participation_factors = poles['Lr_complex'] 

294 self.shapes, self.frfs_synth, self.frfs_residuals, self.frfs_kernel = ( 

295 compute_shapes_multireference( 

296 self.frfs, self.natural_frequencies, self.damping_ratios, 

297 self.participation_factors, real_modes, 

298 low_residuals, high_residuals, 

299 self.min_frequency, self.max_frequency, 

300 self.displacement_derivative, 

301 frequency_lines_at_resonance, 

302 frequency_lines_for_residuals)) 

303 

304 @property 

305 def frequencies(self): 

306 return self.frfs[0, 0].abscissa 

307 

308 @property 

309 def angular_frequencies(self): 

310 return 2 * np.pi * self.frequencies 

311 

312 @property 

313 def frequency_spacing(self): 

314 return np.mean(np.diff(self.frequencies)) 

315 

316 

317class PolyPy_Stability(QWidget): 

318 def __init__(self, polypy_gui, polypy_tabwidget, 

319 frfs, frequency_region, poles, displacement_derivative): 

320 # Load in the GUI 

321 super(PolyPy_Stability, self).__init__(polypy_tabwidget) 

322 uic.loadUi(os.path.join(os.path.abspath(os.path.dirname( 

323 os.path.abspath(__file__))), 'polypy_stability_diagram.ui'), self) 

324 self.polypy_gui = polypy_gui 

325 self.stability_tabwidget = polypy_tabwidget 

326 self.tabwidget_index = self.stability_tabwidget.count() 

327 self.stability_tabwidget.addTab(self, "{:0.2f}--{:0.2f} Hz".format(*frequency_region)) 

328 

329 # Set up colormaps 

330 self.cm = cm.Dark2 

331 self.cm_mod = 8 

332 self.compare_cm = cm.Paired 

333 self.compare_cm_mod = 12 

334 

335 self.frfs = frfs 

336 self.pole_markers = [] 

337 self.pole_positions = np.zeros((0, 2), dtype=float) 

338 self.pole_indices = np.zeros((0, 2), dtype=int) 

339 self.previous_closest_marker_index = None 

340 self.selected_poles = set([]) 

341 

342 # Set up the second axis for the stabilizaton plot 

343 self.pole_plot = self.stabilization_diagram.getPlotItem() 

344 self.pole_plot.setLabels(left='Polynomial Order') 

345 self.stabilization_plot = pyqtgraph.ViewBox() 

346 self.pole_plot.setAxisItems( 

347 {'right': pyqtgraph.AxisItem(orientation='right', showValues=False)}) 

348 self.pole_plot.showAxis('right') 

349 self.pole_plot.scene().addItem(self.stabilization_plot) 

350 self.pole_plot.getAxis('right').linkToView(self.stabilization_plot) 

351 self.stabilization_plot.setXLink(self.pole_plot) 

352 self.pole_plot.getAxis('right').setLabel('Mode Indicator Function') 

353 self.update_stability_plot_views() 

354 self.update_stabilization_plot() 

355 

356 self.connect_callbacks() 

357 

358 self.polypy = PolyPy(frfs, *frequency_region, displacement_derivative) 

359 self.polypy.compute_poles(poles) 

360 

361 self.plot_poles() 

362 

363 def connect_callbacks(self): 

364 self.stabilization_cmif_selection.clicked.connect(self.update_stabilization_plot) 

365 self.stabilization_qmif_selection.clicked.connect(self.update_stabilization_plot) 

366 self.stabilization_mmif_selection.clicked.connect(self.update_stabilization_plot) 

367 self.stabilization_nmif_selection.clicked.connect(self.update_stabilization_plot) 

368 self.pole_plot.vb.sigResized.connect(self.update_stability_plot_views) 

369 self.pole_plot.scene().sigMouseMoved.connect(self.mouseMoved) 

370 self.pole_plot.scene().sigMouseClicked.connect(self.mouseClicked) 

371 self.discard_button.clicked.connect(self.discard) 

372 

373 def discard(self): 

374 self.polypy_gui.stability_diagrams.pop(self.tabwidget_index) 

375 for i in range(len(self.polypy_gui.stability_diagrams)): 

376 self.polypy_gui.stability_diagrams[i].tabwidget_index = i 

377 self.stability_tabwidget.removeTab(self.tabwidget_index) 

378 self.polypy_gui.pole_selection_changed() 

379 

380 def plot_poles(self): 

381 self.pole_markers = [] 

382 self.pole_positions = [] 

383 self.pole_indices = [] 

384 for j, (order, poles) in enumerate(zip(self.polypy.polynomial_orders, self.polypy.pole_list)): 

385 frequencies = poles['omega'] / (2 * np.pi) 

386 freq_convergence = poles['freq_stable'] 

387 damp_convergence = poles['damp_stable'] 

388 part_convergence = poles['part_stable'] 

389 for i, (freq, fc, dc, pc) in enumerate(zip(frequencies, freq_convergence, damp_convergence, part_convergence)): 

390 if pc: 

391 pen = pyqtgraph.mkPen(color=(0, 128, 0)) 

392 symbol = 'o' 

393 elif dc: 

394 pen = pyqtgraph.mkPen(color='b') 

395 symbol = 's' 

396 elif fc: 

397 pen = pyqtgraph.mkPen(color='b') 

398 symbol = 't1' 

399 else: 

400 pen = pyqtgraph.mkPen(color='r') 

401 symbol = 'x' 

402 item = pyqtgraph.ScatterPlotItem( 

403 [freq], [order], pen=pen, symbol=symbol, symbolPen=pen, brush=(0, 0, 0, 0)) 

404 self.pole_markers.append(item) 

405 self.pole_indices.append((j, i)) 

406 self.pole_positions.append((freq, order)) 

407 self.pole_plot.addItem(item) 

408 freq = self.polypy.frequencies[self.polypy.frequency_slice] 

409 self.pole_plot.setRange(xRange=(freq.min(), freq.max()), 

410 yRange=(self.polypy.polynomial_orders.min(), 

411 self.polypy.polynomial_orders.max()), padding=0.1) 

412 self.pole_indices = np.array(self.pole_indices) 

413 self.pole_positions = np.array(self.pole_positions) 

414 

415 def update_stabilization_plot(self): 

416 self.stabilization_plot.clear() 

417 if self.stabilization_cmif_selection.isChecked(): 

418 cmif = self.frfs.compute_cmif() 

419 for i, curve in enumerate(cmif): 

420 pen = pyqtgraph.mkPen(color=[int(255 * v) for v in self.cm(i % self.cm_mod)]) 

421 self.stabilization_plot.addItem(pyqtgraph.PlotCurveItem( 

422 curve.abscissa, np.log10(curve.ordinate), pen=pen)) 

423 elif self.stabilization_qmif_selection.isChecked(): 

424 qmif = self.frfs.compute_cmif( 

425 part='real' if self.polypy_gui.datatype_selector.currentIndex() == 1 else 'imag') 

426 for i, curve in enumerate(qmif): 

427 pen = pyqtgraph.mkPen(color=[int(255 * v) for v in self.cm(i % self.cm_mod)]) 

428 self.stabilization_plot.addItem(pyqtgraph.PlotCurveItem( 

429 curve.abscissa, np.log10(curve.ordinate), pen=pen)) 

430 elif self.stabilization_mmif_selection.isChecked(): 

431 mmif = self.frfs.compute_mmif() 

432 for i, curve in enumerate(mmif): 

433 pen = pyqtgraph.mkPen(color=[int(255 * v) for v in self.cm(i % self.cm_mod)]) 

434 self.stabilization_plot.addItem(pyqtgraph.PlotCurveItem( 

435 curve.abscissa, curve.ordinate, pen=pen)) 

436 elif self.stabilization_nmif_selection.isChecked(): 

437 nmif = self.frfs.compute_nmif() 

438 pen = pyqtgraph.mkPen(color=[int(255 * v) for v in self.cm(0 % self.cm_mod)]) 

439 self.stabilization_plot.addItem(pyqtgraph.PlotCurveItem( 

440 nmif.abscissa, nmif.ordinate), pen=pen) 

441 

442 def update_stability_plot_views(self): 

443 self.stabilization_plot.setGeometry(self.pole_plot.vb.sceneBoundingRect()) 

444 self.stabilization_plot.linkedViewChanged(self.pole_plot.vb, self.stabilization_plot.XAxis) 

445 

446 def mouseMoved(self, position): 

447 if self.pole_plot.vb.sceneBoundingRect().contains(position): 

448 # Get aspect ratio of the box 

449 ar = np.array([self.pole_plot.vb.getAspectRatio(), 1]) 

450 # Get the point in scene coordinates 

451 mouse_point = self.pole_plot.vb.mapSceneToView(position) 

452 # print('Mouse Point: {:}'.format((mouse_point.x(), mouse_point.y()))) 

453 # Find closest point to the mouse point 

454 try: 

455 closest_marker = np.argmin(np.linalg.norm( 

456 (self.pole_positions - np.array([mouse_point.x(), mouse_point.y()])) * ar, axis=-1)) 

457 # print('Closest Marker {:}'.format(self.pole_positions[closest_marker])) 

458 except ValueError: 

459 return 

460 if self.previous_closest_marker_index is not None: 

461 if self.previous_closest_marker_index in self.selected_poles: 

462 order_index, pole_index = self.pole_indices[self.previous_closest_marker_index] 

463 pole = self.polypy.pole_list[order_index][pole_index] 

464 if pole['part_stable']: 

465 brush = (0, 128, 0) 

466 elif pole['damp_stable'] or pole['freq_stable']: 

467 brush = 'b' 

468 else: 

469 brush = 'r' 

470 self.pole_markers[self.previous_closest_marker_index].setBrush(brush) 

471 else: 

472 self.pole_markers[self.previous_closest_marker_index].setBrush((0, 0, 0, 0)) 

473 self.previous_closest_marker_index = closest_marker 

474 order_index, pole_index = self.pole_indices[self.previous_closest_marker_index] 

475 pole = self.polypy.pole_list[order_index][pole_index] 

476 self.selected_pole_display.setText('Highlighted Pole: {:0.2f} Hz, {:0.3f}% Damping'.format( 

477 pole['omega'] / (2 * np.pi), pole['zeta'] * 100)) 

478 self.pole_markers[self.previous_closest_marker_index].setBrush((0, 0, 0, 255)) 

479 

480 def mouseClicked(self, event): 

481 view_position = self.pole_plot.vb.mapSceneToView(event.scenePos()) 

482 clickx = view_position.x() 

483 clicky = view_position.y() 

484 [[xmin, xmax], [ymin, ymax]] = self.pole_plot.vb.viewRange() 

485 if (clickx < xmin) or (clickx > xmax) or (clicky > ymax) or (clicky < ymin): 

486 return 

487 if self.previous_closest_marker_index is None: 

488 return 

489 if self.previous_closest_marker_index in self.selected_poles: 

490 self.selected_poles.remove(self.previous_closest_marker_index) 

491 self.pole_markers[self.previous_closest_marker_index].setBrush((0, 0, 0, 0)) 

492 else: 

493 order_index, pole_index = self.pole_indices[self.previous_closest_marker_index] 

494 pole = self.polypy.pole_list[order_index][pole_index] 

495 if pole['part_stable']: 

496 brush = (0, 128, 0) 

497 elif pole['damp_stable'] or pole['freq_stable']: 

498 brush = 'b' 

499 else: 

500 brush = 'r' 

501 self.selected_poles.add(self.previous_closest_marker_index) 

502 self.pole_markers[self.previous_closest_marker_index].setBrush(brush) 

503 self.previous_closest_marker_index = None 

504 self.selected_pole_display.setText('Highlighted Pole:') 

505 # Now update the pole list 

506 self.pole_table.clearContents() 

507 self.pole_table.setRowCount(len(self.selected_poles)) 

508 selected_pole_list = [] 

509 for index in self.selected_poles: 

510 order_index, pole_index = self.pole_indices[index] 

511 pole = self.polypy.pole_list[order_index][pole_index] 

512 if pole['part_stable']: 

513 stable = 'All' 

514 elif pole['damp_stable']: 

515 stable = 'Damp' 

516 elif pole['freq_stable']: 

517 stable = 'Freq' 

518 else: 

519 stable = 'None' 

520 selected_pole_list.append((pole['omega'] / (2 * np.pi), 

521 pole['zeta'], stable)) 

522 for i, (frequency, damping, stable) in enumerate(sorted(selected_pole_list)): 

523 freq_label = QTableWidgetItem('{:0.2f}'.format(frequency)) 

524 damp_label = QTableWidgetItem('{:0.2f}%'.format(100 * damping)) 

525 stab_label = QTableWidgetItem(stable) 

526 self.pole_table.setItem(i, 0, freq_label) 

527 self.pole_table.setItem(i, 1, damp_label) 

528 self.pole_table.setItem(i, 2, stab_label) 

529 # Send a notification to the main gui that the pole selection has been 

530 # updated 

531 self.polypy_gui.pole_selection_changed() 

532 

533 

534class PolyPy_GUI(QMainWindow): 

535 

536 def __init__(self, frf_data: TransferFunctionArray): 

537 super(PolyPy_GUI, self).__init__() 

538 uic.loadUi(os.path.join(os.path.abspath(os.path.dirname( 

539 os.path.abspath(__file__))), 'polypy.ui'), self) 

540 # Store the FRF 

541 # TODO Make it so we can load data if none is passed in 

542 self.frfs = frf_data.reshape_to_matrix() 

543 self.resynthesis_plots = [] 

544 self.resynthesized_frfs = self.frfs.copy() 

545 self.resynthesized_frfs.ordinate = 0 

546 self.frfs_synth_residual = self.resynthesized_frfs.copy() 

547 self.frfs_synth_kernel = self.resynthesized_frfs.copy() 

548 self.shapes = ShapeArray((0,), self.frfs.shape[0]) 

549 self.first_stability = True 

550 

551 # Set up colormaps 

552 self.cm = cm.Dark2 

553 self.cm_mod = 8 

554 self.compare_cm = cm.Paired 

555 self.compare_cm_mod = 12 

556 

557 # Initialize the plots 

558 self.data_plot = self.data_display.getPlotItem() 

559 

560 # Create the frequency selector 

561 freq_range = (self.frfs.abscissa.min(), 

562 self.frfs.abscissa.max()) 

563 self.frequency_selector = pyqtgraph.LinearRegionItem(values=freq_range, 

564 orientation='vertical', 

565 bounds=freq_range, 

566 ) 

567 self.update_data_plot() 

568 

569 # Update max and mins of selectors 

570 self.min_frequency_selector.setRange(*freq_range) 

571 self.min_frequency_selector.setValue(freq_range[0]) 

572 self.max_frequency_selector.setRange(*freq_range) 

573 self.max_frequency_selector.setValue(freq_range[1]) 

574 

575 self.lines_at_resonance_spinbox.setMaximum(self.frfs.num_elements) 

576 self.lines_at_residuals_spinbox.setMaximum(self.frfs.num_elements) 

577 self.lines_at_residuals_spinbox.setValue(self.frfs.num_elements // 10) 

578 

579 self.connect_callbacks() 

580 self.num_responses_label.setText('{:} Responses'.format(self.frfs.shape[0])) 

581 self.num_references_label.setText('{:} References'.format(self.frfs.shape[1])) 

582 self.num_frequencies_label.setText('{:} Frequencies'.format(self.frfs.num_elements)) 

583 self.by_order_spinbox.setValue(self.frfs.shape[1]) 

584 self.stability_diagrams = [] 

585 self.geometry = None 

586 self.shapes = None 

587 self.setWindowTitle('PolyPy -- Multi-reference Polynomial Curve Fitter') 

588 self.show_line_selectors() 

589 self.show() 

590 

591 @property 

592 def min_frequency(self): 

593 min_freq = np.min([sd.polypy.min_frequency for sd in self.stability_diagrams]) 

594 if min_freq == 0: 

595 min_freq = self.frequencies[1] 

596 return min_freq 

597 

598 @property 

599 def max_frequency(self): 

600 return np.max([sd.polypy.max_frequency for sd in self.stability_diagrams]) 

601 

602 @property 

603 def frequencies(self): 

604 return self.frfs[0, 0].abscissa 

605 

606 @property 

607 def frequency_slice(self): 

608 abscissa_indices = np.ones(self.frequencies.shape, dtype=bool) 

609 if self.min_frequency is not None: 

610 abscissa_indices &= (self.frequencies >= self.min_frequency) 

611 if self.max_frequency is not None: 

612 abscissa_indices &= (self.frequencies <= self.max_frequency) 

613 abscissa = self.frequencies[abscissa_indices] 

614 if abscissa[0] == 0: 

615 abscissa = abscissa[1:] 

616 freq_range = np.array((np.min(abscissa), np.max(abscissa))) 

617 index_range = np.argmin(np.abs(self.frequencies - freq_range[:, np.newaxis]), axis=1) 

618 return slice(index_range[0], index_range[1] + 1) 

619 

620 def connect_callbacks(self): 

621 self.setup_cmif_selection.clicked.connect(self.update_data_plot) 

622 self.setup_qmif_selection.clicked.connect(self.update_data_plot) 

623 self.setup_mmif_selection.clicked.connect(self.update_data_plot) 

624 self.setup_nmif_selection.clicked.connect(self.update_data_plot) 

625 self.frequency_selector.sigRegionChanged.connect(self.update_frequency_from_region) 

626 self.min_frequency_selector.valueChanged.connect(self.update_min_frequency) 

627 self.max_frequency_selector.valueChanged.connect(self.update_max_frequency) 

628 self.compute_stabilization_button.clicked.connect(self.compute_stabilization) 

629 self.compute_shapes_button.clicked.connect(self.compute_shapes) 

630 self.export_pole_list_button.clicked.connect(self.export_pole_list) 

631 self.load_geometry_button.clicked.connect(self.load_geometry) 

632 self.plot_shapes_button.clicked.connect(self.plot_shapes) 

633 self.export_shapes_button.clicked.connect(self.save_shapes) 

634 self.create_frf_window_button.clicked.connect(self.create_frf_window) 

635 self.create_cmif_window_button.clicked.connect(self.create_cmif_window) 

636 self.create_qmif_window_button.clicked.connect(self.create_qmif_window) 

637 self.create_mmif_window_button.clicked.connect(self.create_mmif_window) 

638 self.create_nmif_window_button.clicked.connect(self.create_nmif_window) 

639 self.complex_modes_checkbox.stateChanged.connect(self.pole_selection_changed) 

640 self.autoresynthesize_checkbox.stateChanged.connect(self.pole_selection_changed) 

641 self.all_frequency_lines_checkbox.stateChanged.connect(self.pole_selection_changed) 

642 self.lines_at_resonance_spinbox.valueChanged.connect(self.pole_selection_changed) 

643 self.lines_at_residuals_spinbox.valueChanged.connect(self.pole_selection_changed) 

644 self.use_low_residuals_checkbox.stateChanged.connect(self.pole_selection_changed) 

645 self.use_high_residuals_checkbox.stateChanged.connect(self.pole_selection_changed) 

646 self.mode_display_selector.currentIndexChanged.connect(self.update_resynthesis) 

647 self.reconstruction_mode_selector.currentIndexChanged.connect(self.update_resynthesis) 

648 self.export_fit_data_button.clicked.connect(self.export_fit_data) 

649 self.all_frequency_lines_checkbox.stateChanged.connect(self.show_line_selectors) 

650 

651 def show_line_selectors(self): 

652 if self.all_frequency_lines_checkbox.isChecked(): 

653 for widget in [self.lines_at_resonance_label, self.lines_at_resonance_spinbox, 

654 self.lines_at_residuals_label, self.lines_at_residuals_spinbox]: 

655 widget.hide() 

656 else: 

657 for widget in [self.lines_at_resonance_label, self.lines_at_resonance_spinbox, 

658 self.lines_at_residuals_label, self.lines_at_residuals_spinbox]: 

659 widget.show() 

660 

661 def load_geometry(self): 

662 filename, file_filter = QtWidgets.QFileDialog.getOpenFileName( 

663 self, 'Open Geometry', filter='Numpy Files (*.npz);;Universal Files (*.unv *.uff)') 

664 if filename == '': 

665 return 

666 self.geometry = Geometry.load(filename) 

667 

668 def set_geometry(self, geometry): 

669 self.geometry = geometry 

670 

671 def save_shapes(self): 

672 if self.shapes is None: 

673 QtWidgets.QMessageBox.warning(self, 'PyPoly', 'Shapes not created!') 

674 return 

675 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

676 self, 'Save Shapes', filter='Numpy Files (*.npy)') 

677 if filename == '': 

678 return 

679 np.sort(self.shapes).save(filename) 

680 

681 def export_fit_data(self): 

682 if self.shapes is None: 

683 QtWidgets.QMessageBox.warning(self, 'PyPoly', 'Shapes not created!') 

684 return 

685 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

686 self, 'Save Fit Data', filter='Numpy Files (*.npz)') 

687 if filename == '': 

688 return 

689 shapes = np.sort(self.shapes) 

690 np.savez(filename, shapes=shapes.view(np.ndarray), 

691 frfs=self.frfs.view(np.ndarray), 

692 frfs_resynth=self.resynthesized_frfs.view(np.ndarray), 

693 frfs_residual=self.frfs_synth_residual.view(np.ndarray)) 

694 

695 def export_pole_list(self): 

696 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

697 self, "Save Pole List Data", filter="Numpy Files (*.npy)" 

698 ) 

699 if filename == "": 

700 return 

701 poles = [] 

702 for sd in self.stability_diagrams: 

703 pole_indices = [sd.pole_indices[val] for val in sd.selected_poles] 

704 poles.append(sd.polypy.pole_list_from_indices(pole_indices)) 

705 poles = np.concatenate(poles) 

706 np.save(filename, poles) 

707 

708 def plot_shapes(self): 

709 if self.geometry is None: 

710 self.load_geometry() 

711 if self.geometry is None: 

712 QtWidgets.QMessageBox.warning(self, 'PyPoly', 'No Geometry Loaded!') 

713 return 

714 if self.shapes is None: 

715 QtWidgets.QMessageBox.warning(self, 'PyPoly', 'Shapes not created!') 

716 return 

717 self.geometry.plot_shape(np.sort(self.shapes)) 

718 

719 def pole_selection_changed(self): 

720 if self.autoresynthesize_checkbox.isChecked(): 

721 self.compute_shapes() 

722 

723 def create_frf_window(self): 

724 self.resynthesis_plots.append( 

725 ('frf', GUIPlot(Data=self.frfs, Fit=self.resynthesized_frfs))) 

726 self.update_resynthesis() 

727 

728 def create_cmif_window(self): 

729 self.resynthesis_plots.append( 

730 ('cmif', GUIPlot(Data=self.frfs.compute_cmif(), Fit=self.resynthesized_frfs.compute_cmif()))) 

731 self.resynthesis_plots[-1][-1].ordinate_log = True 

732 self.resynthesis_plots[-1][-1].actionOrdinate_Log.setChecked(True) 

733 self.update_resynthesis() 

734 

735 def create_qmif_window(self): 

736 self.resynthesis_plots.append( 

737 ('qmif', GUIPlot(Data=self.frfs.compute_cmif(part='real' if self.datatype_selector.currentIndex() == 1 else 'imag'), 

738 Fit=self.resynthesized_frfs.compute_cmif(part='imag')))) 

739 self.resynthesis_plots[-1][-1].ordinate_log = True 

740 self.resynthesis_plots[-1][-1].actionOrdinate_Log.setChecked(True) 

741 self.update_resynthesis() 

742 

743 def create_mmif_window(self): 

744 test_mmif = self.frfs.compute_mmif() 

745 try: 

746 mmif = self.resynthesized_frfs.compute_mmif() 

747 except np.linalg.LinAlgError: 

748 mmif = test_mmif.copy() 

749 mmif.ordinate = 1 

750 self.resynthesis_plots.append( 

751 ('mmif', GUIPlot(Data=test_mmif, Fit=mmif))) 

752 self.update_resynthesis() 

753 

754 def create_nmif_window(self): 

755 self.resynthesis_plots.append( 

756 ('nmif', GUIPlot(Data=self.frfs.compute_nmif(), Fit=self.resynthesized_frfs.compute_nmif()))) 

757 self.update_resynthesis() 

758 

759 def update_resynthesis(self): 

760 # First go through and remove any closed windows 

761 self.resynthesis_plots = [(function_type, window) for function_type, 

762 window in self.resynthesis_plots if window.isVisible()] 

763 # Now go and update the resynthesized FRFs 

764 if self.reconstruction_mode_selector.currentIndex() == 0: 

765 data_computed = {'frf': self.resynthesized_frfs.flatten()} 

766 elif self.reconstruction_mode_selector.currentIndex() == 1: 

767 data_computed = {'frf': self.frfs_synth_kernel.flatten()} 

768 elif self.reconstruction_mode_selector.currentIndex() == 2: 

769 data_computed = {'frf': self.frfs_synth_residual.flatten()} 

770 for function_type, window in self.resynthesis_plots: 

771 if function_type not in data_computed: 

772 if function_type == 'cmif': 

773 data_computed[function_type] = data_computed["frf"].compute_cmif().flatten() 

774 elif function_type == 'qmif': 

775 data_computed[function_type] = data_computed['frf'].compute_cmif( 

776 part='real' if self.datatype_selector.currentIndex() == 1 else 'imag').flatten() 

777 elif function_type == 'mmif': 

778 try: 

779 data_computed[function_type] = data_computed['frf'].compute_mmif().flatten() 

780 except np.linalg.LinAlgError: 

781 test_mmif = self.frfs.compute_mmif().copy() 

782 test_mmif.ordinate = 1 

783 data_computed[function_type] = test_mmif 

784 elif function_type == 'nmif': 

785 data_computed[function_type] = data_computed['frf'].compute_nmif().flatten() 

786 window.data_dictionary['Fit'] = data_computed[function_type] 

787 if self.mode_display_selector.currentIndex() > 0: 

788 symbol = [None,'vline','o','x','+','star'][self.mode_display_selector.currentIndex()] 

789 window.marker_data['Fit'] = [self.shapes.frequency, '{index:}: {abscissa:0.2f}', symbol] 

790 else: 

791 window.marker_data['Fit'] = [None, None, None] 

792 window.update() 

793 

794 def compute_shapes(self): 

795 try: 

796 # Assemble pole list 

797 poles = [] 

798 for sd in self.stability_diagrams: 

799 pole_indices = [sd.pole_indices[val] for val in sd.selected_poles] 

800 poles.append(sd.polypy.pole_list_from_indices(pole_indices)) 

801 poles = np.concatenate(poles) 

802 if poles.size == 0: 

803 self.resynthesized_frfs = self.frfs.copy() 

804 self.resynthesized_frfs.ordinate = 0 

805 self.frfs_synth_residual = self.resynthesized_frfs.copy() 

806 self.frfs_synth_kernel = self.resynthesized_frfs.copy() 

807 self.shapes = ShapeArray((0,), self.frfs.shape[0]) 

808 self.negative_drive_points = np.zeros((0,), dtype=int) 

809 return 

810 natural_frequencies = poles['omega'] / (2 * np.pi) 

811 damping_ratios = poles['zeta'] 

812 participation_factors = poles['Lr_complex'] 

813 H = self.frfs.ordinate[..., self.frequency_slice] 

814 num_outputs, num_inputs, num_elements = H.shape 

815 self.shapes, self.resynthesized_frfs, self.frfs_synth_residual, self.frfs_synth_kernel = ( 

816 compute_shapes_multireference( 

817 self.frfs, natural_frequencies, damping_ratios, 

818 participation_factors, 

819 not self.complex_modes_checkbox.isChecked(), 

820 self.use_low_residuals_checkbox.isChecked(), 

821 self.use_high_residuals_checkbox.isChecked(), 

822 self.min_frequency, self.max_frequency, 

823 self.datatype_selector.currentIndex(), 

824 None if self.all_frequency_lines_checkbox.isChecked() else self.lines_at_resonance_spinbox.value(), 

825 self.lines_at_residuals_spinbox.value()) 

826 ) 

827 self.update_resynthesis() 

828 except Exception: 

829 print(traceback.format_exc()) 

830 

831 def compute_stabilization(self): 

832 self.compute_stabilization_button.setEnabled(False) 

833 poles = np.arange(self.min_order_spinbox.value(), 

834 self.max_order_spinbox.value() + 1, 

835 self.by_order_spinbox.value()) 

836 self.stability_diagrams.append( 

837 PolyPy_Stability(self, self.stability_tab_widget, 

838 self.frfs, self.frequency_selector.getRegion(), poles, self.datatype_selector.currentIndex())) 

839 self.compute_stabilization_button.setEnabled(True) 

840 if self.first_stability: 

841 self.first_stability = False 

842 num_lines = np.sum((self.frequencies >= self.min_frequency) & 

843 (self.frequencies <= self.max_frequency)) 

844 self.lines_at_residuals_spinbox.setValue(num_lines // 10) 

845 self.stability_tab_widget.setCurrentIndex(len(self.stability_diagrams) - 1) 

846 self.polypy_tab_widget.setCurrentIndex(1) 

847 

848 def update_frequency_from_region(self): 

849 self.min_frequency_selector.blockSignals(True) 

850 self.max_frequency_selector.blockSignals(True) 

851 region = self.frequency_selector.getRegion() 

852 self.min_frequency_selector.setValue(region[0]) 

853 self.max_frequency_selector.setValue(region[1]) 

854 self.min_frequency_selector.blockSignals(False) 

855 self.max_frequency_selector.blockSignals(False) 

856 

857 def update_min_frequency(self): 

858 self.frequency_selector.blockSignals(True) 

859 self.max_frequency_selector.blockSignals(True) 

860 if self.min_frequency_selector.value() > self.max_frequency_selector.value(): 

861 self.max_frequency_selector.setValue(self.min_frequency_selector.value()) 

862 self.frequency_selector.setRegion((self.min_frequency_selector.value(), 

863 self.max_frequency_selector.value())) 

864 self.frequency_selector.blockSignals(False) 

865 self.max_frequency_selector.blockSignals(False) 

866 

867 def update_max_frequency(self): 

868 self.frequency_selector.blockSignals(True) 

869 self.min_frequency_selector.blockSignals(True) 

870 if self.min_frequency_selector.value() > self.max_frequency_selector.value(): 

871 self.min_frequency_selector.setValue(self.max_frequency_selector.value()) 

872 self.frequency_selector.setRegion((self.min_frequency_selector.value(), 

873 self.max_frequency_selector.value())) 

874 self.frequency_selector.blockSignals(False) 

875 self.min_frequency_selector.blockSignals(False) 

876 

877 def update_data_plot(self): 

878 self.data_plot.clear() 

879 self.data_plot.addItem(self.frequency_selector) 

880 if self.setup_cmif_selection.isChecked(): 

881 self.data_plot.setLogMode(False, True) 

882 cmif = self.frfs.compute_cmif() 

883 for i, curve in enumerate(cmif): 

884 pen = pyqtgraph.mkPen(color=[int(255 * v) for v in self.cm(i % self.cm_mod)]) 

885 self.data_plot.plot(curve.abscissa, curve.ordinate, pen=pen) 

886 elif self.setup_qmif_selection.isChecked(): 

887 self.data_plot.setLogMode(False, True) 

888 qmif = self.frfs.compute_cmif( 

889 part='real' if self.datatype_selector.currentIndex() == 1 else 'imag') 

890 for i, curve in enumerate(qmif): 

891 pen = pyqtgraph.mkPen(color=[int(255 * v) for v in self.cm(i % self.cm_mod)]) 

892 self.data_plot.plot(curve.abscissa, curve.ordinate, pen=pen) 

893 elif self.setup_mmif_selection.isChecked(): 

894 self.data_plot.setLogMode(False, False) 

895 mmif = self.frfs.compute_mmif() 

896 for i, curve in enumerate(mmif): 

897 pen = pyqtgraph.mkPen(color=[int(255 * v) for v in self.cm(i % self.cm_mod)]) 

898 self.data_plot.plot(curve.abscissa, curve.ordinate, pen=pen) 

899 elif self.setup_nmif_selection.isChecked(): 

900 self.data_plot.setLogMode(False, False) 

901 nmif = self.frfs.compute_nmif() 

902 pen = pyqtgraph.mkPen(color=[int(255 * v) for v in self.cm(0 % self.cm_mod)]) 

903 self.data_plot.plot(nmif.abscissa, nmif.ordinate)