Coverage for src / sdynpy / modal / sdynpy_signal_processing_gui.py: 8%

901 statements  

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

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

2""" 

3Graphical Signal Processing tool for computing FRFs and CPSDs 

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 

26from ..core.sdynpy_data import TimeHistoryArray, data_array, FunctionTypes, GUIPlot 

27from ..core.sdynpy_coordinate import CoordinateArray, outer_product 

28from ..fileio.sdynpy_rattlesnake import read_rattlesnake_output 

29from ..core.sdynpy_geometry import Geometry 

30from .sdynpy_smac import SMAC_GUI 

31from .sdynpy_polypy import PolyPy_GUI 

32from qtpy import QtWidgets, uic, QtGui 

33from qtpy.QtGui import QIcon, QFont 

34from qtpy.QtCore import Qt, QCoreApplication, QRect 

35from qtpy.QtWidgets import (QToolTip, QLabel, QPushButton, QApplication, 

36 QGroupBox, QWidget, QMessageBox, QHBoxLayout, 

37 QVBoxLayout, QSizePolicy, QMainWindow, 

38 QFileDialog, QErrorMessage, QListWidget, QListWidgetItem, 

39 QLineEdit, 

40 QDockWidget, QGridLayout, QButtonGroup, QDialog, 

41 QCheckBox, QRadioButton, QMenuBar, QMenu) 

42try: 

43 from qtpy.QtGui import QAction 

44except ImportError: 

45 from qtpy.QtWidgets import QAction 

46import numpy as np 

47import pyqtgraph as pqtg 

48import matplotlib.cm as cm 

49from scipy.signal import get_window 

50import traceback 

51 

52 

53class SignalProcessingGUI(QMainWindow): 

54 """An iteractive window allowing users to compute FRFs""" 

55 

56 def __init__(self, time_history_array: TimeHistoryArray = None, 

57 geometry=None): 

58 """ 

59 Create a Signal Processing window to compute FRF and other spectral data. 

60 

61 A TimeHistoryArray can be passed as an argument, or data can be loaded 

62 from a file. 

63 

64 Parameters 

65 ---------- 

66 data_array : TimeHistoryArray 

67 Time history data to use to compute FRF and other spectral data. 

68 geometry : Geometry 

69 Geometry data used to plot transients or deflection shapes 

70 

71 Returns 

72 ------- 

73 None. 

74 

75 """ 

76 self.sample_rate = 1.0 

77 self.reference_indices = [] 

78 self.window_parameters = { 

79 'rectangle': (), 

80 'hann': (), 

81 'hamming': (), 

82 'flattop': (), 

83 'tukey': ('Cosine Fraction:'), 

84 'blackmanharris': (), 

85 'exponential': ('Center:', 'tau:'), 

86 'exponential+force': ('Pulse End:', 'Center:', 'tau:'), } 

87 self.cm = cm.Dark2 

88 self.cm_mod = 8 

89 self.time_selector_references = pqtg.LinearRegionItem(values=[0, 1], 

90 orientation='vertical', 

91 bounds=[0, 1], 

92 ) 

93 self.time_selector_responses = pqtg.LinearRegionItem(values=[0, 1], 

94 orientation='vertical', 

95 bounds=[0, 1], 

96 ) 

97 self.trigger_level_selector_references = pqtg.InfiniteLine(pos=0, 

98 angle=0, 

99 movable=True, 

100 label='Trigger ', 

101 pen='b', 

102 labelOpts={ 

103 'position': 1.0, 'color': 'b'} 

104 ) 

105 self.hysteresis_level_selector_references = pqtg.InfiniteLine(pos=0, 

106 angle=0, 

107 movable=True, 

108 label='Hysteresis ', 

109 pen='r', 

110 labelOpts={ 

111 'position': 1.0, 'color': 'r'} 

112 ) 

113 self.trigger_level_selector_responses = pqtg.InfiniteLine(pos=0, 

114 angle=0, 

115 movable=True, 

116 label='Trigger ', 

117 pen='b', 

118 labelOpts={ 

119 'position': 1.0, 'color': 'b'} 

120 ) 

121 self.hysteresis_level_selector_responses = pqtg.InfiniteLine(pos=0, 

122 angle=0, 

123 movable=True, 

124 label='Hysteresis ', 

125 pen='r', 

126 labelOpts={ 

127 'position': 1.0, 'color': 'r'} 

128 ) 

129 self.frame_start_indices = [] 

130 self.ignore_frames = [] 

131 self.response_rois = [] 

132 self.reference_rois = [] 

133 self.windowed_time_data = None 

134 self.autospectra_data = None 

135 self.crossspectra_data = None 

136 self.frf_data = None 

137 self.coherence_data = None 

138 self.geometry = geometry 

139 self.plots = {} 

140 self.deflection_actions = [] 

141 self.active_plots = [] 

142 super(SignalProcessingGUI, self).__init__() 

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

144 os.path.abspath(__file__))), 'signal_processing.ui'), self) 

145 self.responsesPlot.addItem(self.trigger_level_selector_responses) 

146 self.referencesPlot.addItem(self.trigger_level_selector_references) 

147 self.responsesPlot.addItem(self.hysteresis_level_selector_responses) 

148 self.referencesPlot.addItem(self.hysteresis_level_selector_references) 

149 self.responsesPlot.addItem(self.time_selector_responses) 

150 self.referencesPlot.addItem(self.time_selector_references) 

151 for widget in [self.trigger_level_selector_responses, 

152 self.trigger_level_selector_references, 

153 self.hysteresis_level_selector_responses, 

154 self.hysteresis_level_selector_references]: 

155 widget.setVisible(False) 

156 self.connect_callbacks() 

157 if time_history_array is not None: 

158 self.time_history_data = time_history_array.flatten() 

159 self.initialize_ui() 

160 self.setWindowTitle('Graphical Signal Processing Tool') 

161 self.show() 

162 

163 def connect_callbacks(self): 

164 self.overlapDoubleSpinBox.valueChanged.connect(self.overlapChanged) 

165 self.overlapSamplesSpinBox.valueChanged.connect(self.overlapSamplesChanged) 

166 self.framesSpinBox.valueChanged.connect(self.framesChanged) 

167 self.startTimeDoubleSpinBox.valueChanged.connect(self.startTimeChanged) 

168 self.endTimeDoubleSpinBox.valueChanged.connect(self.endTimeChanged) 

169 self.frameSizeSpinBox.valueChanged.connect(self.frameSizeChanged) 

170 self.frequencyLinesSpinBox.valueChanged.connect(self.frequencyLinesChanged) 

171 self.frameTimeDoubleSpinBox.valueChanged.connect(self.frameTimeChanged) 

172 self.frequencySpacingDoubleSpinBox.valueChanged.connect(self.frequencySpacingChanged) 

173 self.windowComboBox.currentIndexChanged.connect(self.windowChanged) 

174 self.typeComboBox.currentIndexChanged.connect(self.typeChanged) 

175 self.referencesSelector.itemSelectionChanged.connect(self.referencesChanged) 

176 self.responsesSelector.itemSelectionChanged.connect(self.responsesChanged) 

177 self.referencesSelector.itemDoubleClicked.connect(self.sendToResponse) 

178 self.responsesSelector.itemDoubleClicked.connect(self.sendToReference) 

179 self.time_selector_references.sigRegionChanged.connect(self.updateTimeFromReference) 

180 self.time_selector_responses.sigRegionChanged.connect(self.updateTimeFromResponse) 

181 self.trigger_level_selector_references.sigPositionChanged.connect( 

182 self.updateTriggerFromReference) 

183 self.trigger_level_selector_responses.sigPositionChanged.connect( 

184 self.updateTriggerFromResponse) 

185 self.hysteresis_level_selector_references.sigPositionChanged.connect( 

186 self.updateHysteresisFromReference) 

187 self.hysteresis_level_selector_responses.sigPositionChanged.connect( 

188 self.updateHysteresisFromResponse) 

189 self.levelDoubleSpinBox.valueChanged.connect(self.levelChanged) 

190 self.hysteresisLevelDoubleSpinBox.valueChanged.connect(self.hysteresisChanged) 

191 self.pretriggerDoubleSpinBox.valueChanged.connect(self.pretriggerChanged) 

192 self.acceptanceComboBox.currentIndexChanged.connect(self.acceptanceChanged) 

193 self.responsesPlot.getViewBox().sigYRangeChanged.connect(self.responseViewChanged) 

194 self.referencesPlot.getViewBox().sigYRangeChanged.connect(self.referenceViewChanged) 

195 self.computeButton.clicked.connect(self.compute) 

196 self.plotWindowedTimeHistoryButton.clicked.connect(self.showWindowedTimeHistory) 

197 self.saveWindowedTimeHistoryButton.clicked.connect(self.saveWindowedTimeHistory) 

198 self.plotFRFButton.clicked.connect(self.showFRF) 

199 self.saveFRFButton.clicked.connect(self.saveFRF) 

200 self.plotCoherenceButton.clicked.connect(self.showCoherence) 

201 self.saveCoherenceButton.clicked.connect(self.saveCoherence) 

202 self.plotAutospectraButton.clicked.connect(self.showAutospectra) 

203 self.saveAutospectraButton.clicked.connect(self.saveAutospectra) 

204 self.plotCrossspectraButton.clicked.connect(self.showCrossspectra) 

205 self.saveCrossspectraButton.clicked.connect(self.saveCrossspectra) 

206 self.actionSend_to_SMAC.triggered.connect(self.analyzeSMAC) 

207 self.actionSend_to_PolyPy.triggered.connect(self.analyzePolyPy) 

208 self.actionLoad_Geometry.triggered.connect(self.loadGeometry) 

209 self.actionVisualize_with_TransientPlotter.triggered.connect(self.plotTransient) 

210 self.actionLoad_Data.triggered.connect(self.loadData) 

211 

212 def block_averaging_signals(self, block: bool): 

213 for widget in [self.overlapDoubleSpinBox, 

214 self.framesSpinBox, 

215 self.overlapSamplesSpinBox, 

216 self.channelComboBox, 

217 self.slopeComboBox, 

218 self.levelDoubleSpinBox, 

219 self.hysteresisLevelDoubleSpinBox, 

220 self.pretriggerDoubleSpinBox]: 

221 widget.blockSignals(block) 

222 

223 def block_data_range_signals(self, block: bool): 

224 for widget in [self.startTimeDoubleSpinBox, 

225 self.endTimeDoubleSpinBox, 

226 self.time_selector_references, 

227 self.time_selector_responses]: 

228 widget.blockSignals(block) 

229 

230 def block_sampling_signals(self, block: bool): 

231 for widget in [self.frameSizeSpinBox, 

232 self.frequencyLinesSpinBox, 

233 self.frameTimeDoubleSpinBox, 

234 self.frequencySpacingDoubleSpinBox]: 

235 widget.blockSignals(block) 

236 

237 def reset_ui(self): 

238 self.sample_rate = 1.0 

239 self.reference_indices = [] 

240 self.signalsSpinBox.setValue(0) 

241 self.referencesSpinBox.setValue(0) 

242 self.responsesSpinBox.setValue(0) 

243 self.samplesSpinBox.setValue(0) 

244 self.sampleRateDoubleSpinBox.setValue(0) 

245 self.durationDoubleSpinBox.setValue(0) 

246 self.startTimeDoubleSpinBox.setalue(0) 

247 self.endTimeDoubleSpinBox.setValue(0) 

248 self.frameSizeSpinBox.setValue(0) 

249 self.windowComboBox.setCurrentIndex(0) 

250 self.typeComboBox.setCurrentIndex(0) 

251 self.channelComboBox.clear() 

252 

253 def initialize_ui(self): 

254 # Set the information 

255 if not self.time_history_data.validate_common_abscissa(): 

256 QMessageBox.critical(self, 'Invalid Abscissa', 

257 'Time histories must have identical abscissa') 

258 self.reset_ui() 

259 return 

260 dt = np.diff(self.time_history_data.abscissa) 

261 if not np.allclose(dt, dt[0]): 

262 QMessageBox.critical(self, 'Invalid Abscissa', 

263 'Time histories must have equally spaced timesteps') 

264 self.reset_ui() 

265 return 

266 self.block_averaging_signals(True) 

267 self.block_data_range_signals(True) 

268 self.block_sampling_signals(True) 

269 dt = np.mean(dt) 

270 self.sample_rate = 1 / dt 

271 self.signalsSpinBox.setValue(self.time_history_data.size) 

272 self.referencesSpinBox.setValue(0) 

273 self.responsesSpinBox.setValue(self.time_history_data.size) 

274 self.samplesSpinBox.setValue(self.time_history_data.num_elements) 

275 self.sampleRateDoubleSpinBox.setValue(self.sample_rate) 

276 self.durationDoubleSpinBox.setValue(dt * self.time_history_data.num_elements) 

277 self.time_selector_references.setBounds((0, dt * self.time_history_data.num_elements)) 

278 self.time_selector_responses.setBounds((0, dt * self.time_history_data.num_elements)) 

279 self.startTimeDoubleSpinBox.setMaximum(dt * self.time_history_data.num_elements) 

280 self.startTimeDoubleSpinBox.setValue(0) 

281 self.startTimeDoubleSpinBox.setSingleStep(dt) 

282 self.startTimeDoubleSpinBox.setDecimals(len(str(int(self.sample_rate))) + 2) 

283 self.endTimeDoubleSpinBox.setMaximum(dt * self.time_history_data.num_elements) 

284 self.endTimeDoubleSpinBox.setValue(dt * self.time_history_data.num_elements) 

285 self.endTimeDoubleSpinBox.setSingleStep(dt) 

286 self.endTimeDoubleSpinBox.setDecimals(len(str(int(self.sample_rate))) + 2) 

287 self.frameSizeSpinBox.setValue(int(self.sample_rate)) 

288 self.frameTimeDoubleSpinBox.setSingleStep(dt) 

289 self.frameTimeDoubleSpinBox.setDecimals(len(str(int(self.sample_rate))) + 2) 

290 self.frameSizeSpinBox.setMaximum(self.time_history_data.num_elements) 

291 self.frameTimeDoubleSpinBox.setMaximum(dt * self.time_history_data.num_elements) 

292 self.frequencyLinesSpinBox.setMaximum(self.time_history_data.num_elements // 2 + 1) 

293 self.frequencySpacingDoubleSpinBox.setMinimum( 

294 1 / (dt * self.time_history_data.num_elements)) 

295 self.windowComboBox.setCurrentIndex(0) 

296 self.windowParameter1DoubleSpinBox.setVisible(False) 

297 self.windowParameter2DoubleSpinBox.setVisible(False) 

298 self.windowParameter3DoubleSpinBox.setVisible(False) 

299 self.windowParameter1Label.setVisible(False) 

300 self.windowParameter2Label.setVisible(False) 

301 self.windowParameter3Label.setVisible(False) 

302 self.typeComboBox.setCurrentIndex(0) 

303 self.typeChanged() 

304 self.overlapDoubleSpinBox.setValue(0) 

305 # Set up the channel numbers 

306 self.channelComboBox.clear() 

307 for i, coordinate in enumerate(self.time_history_data.coordinate[:, 0]): 

308 self.channelComboBox.addItem('{:}: {:}'.format(i + 1, str(coordinate)), i) 

309 self.responsesSelector.clear() 

310 for i, coordinate in enumerate(self.time_history_data.coordinate[:, 0]): 

311 list_item = QListWidgetItem('{:}: {:}'.format(i + 1, str(coordinate))) 

312 list_item.setData(Qt.UserRole, i) 

313 self.responsesSelector.addItem(list_item) 

314 self.referencesSelector.clear() 

315 self.referencesPlot.clear() 

316 self.responsesPlot.clear() 

317 self.time_selector_references.setRegion((0, dt * self.time_history_data.num_elements)) 

318 self.time_selector_responses.setRegion((0, dt * self.time_history_data.num_elements)) 

319 self.referencesPlot.setXRange(0, dt * self.time_history_data.num_elements) 

320 self.responsesPlot.setXRange(0, dt * self.time_history_data.num_elements) 

321 self.block_averaging_signals(False) 

322 self.block_data_range_signals(False) 

323 self.block_sampling_signals(False) 

324 self.frameSizeChanged() 

325 self.overlapSamplesChanged() 

326 

327 def create_rois(self): 

328 xsize = self.frameSizeSpinBox.value() / self.sample_rate 

329 for roi in self.response_rois: 

330 self.responsesPlot.removeItem(roi) 

331 for roi in self.reference_rois: 

332 self.referencesPlot.removeItem(roi) 

333 self.response_rois.clear() 

334 self.reference_rois.clear() 

335 for roi_list, plot in zip([self.response_rois, self.reference_rois], 

336 [self.responsesPlot, self.referencesPlot]): 

337 yrange = plot.getViewBox().viewRange()[1] 

338 ysize = (yrange[1] - yrange[0]) * .9 

339 ystart = yrange[0] + 0.05 * (yrange[1] - yrange[0]) 

340 for frame_start in self.frame_start_indices: 

341 xstart = frame_start / self.sample_rate 

342 roi = pqtg.ROI((xstart, ystart), (xsize, ysize), 

343 movable=False, rotatable=False, 

344 resizable=False, removable=False, 

345 pen=pqtg.mkPen(color=(0, 125, 0), width=2.0), 

346 hoverPen=pqtg.mkPen(color=(0, 255, 0), width=4.0)) 

347 roi.setAcceptedMouseButtons(Qt.MouseButton.LeftButton) 

348 roi.sigClicked.connect(self.toggleROI) 

349 roi_list.append(roi) 

350 plot.addItem(roi, ignoreBounds=True) 

351 

352 def toggleROI(self, roi): 

353 print('Clicked ROI {:}'.format(roi)) 

354 if self.acceptanceComboBox.currentIndex() != 0: 

355 # Find the current index 

356 for index, (response_roi, reference_roi) in enumerate(zip(self.response_rois, 

357 self.reference_rois)): 

358 if roi is response_roi or roi is reference_roi: 

359 print('ROI found at index {:}'.format(index)) 

360 # Check if it is already ignored 

361 if index in self.ignore_frames: 

362 self.ignore_frames.pop(self.ignore_frames.index(index)) 

363 response_roi.setPen(pqtg.mkPen(color=(0, 125, 0), width=2.0)) 

364 reference_roi.setPen(pqtg.mkPen(color=(0, 125, 0), width=2.0)) 

365 self.block_averaging_signals(True) 

366 self.framesSpinBox.setValue(self.framesSpinBox.value() + 1) 

367 self.block_averaging_signals(False) 

368 else: 

369 self.ignore_frames.append(index) 

370 response_roi.setPen(pqtg.mkPen(color=(125, 0, 0), width=2.0)) 

371 reference_roi.setPen(pqtg.mkPen(color=(125, 0, 0), width=2.0)) 

372 self.block_averaging_signals(True) 

373 self.framesSpinBox.setValue(self.framesSpinBox.value() - 1) 

374 self.block_averaging_signals(False) 

375 

376 def acceptanceChanged(self): 

377 print('Acceptance Changed') 

378 if self.acceptanceComboBox.currentIndex() == 0: 

379 self.ignore_frames.clear() 

380 for index, (response_roi, reference_roi) in enumerate(zip(self.response_rois, 

381 self.reference_rois)): 

382 response_roi.setPen(pqtg.mkPen(color=(0, 125, 0), width=2.0)) 

383 reference_roi.setPen(pqtg.mkPen(color=(0, 125, 0), width=2.0)) 

384 

385 def referenceViewChanged(self): 

386 print('Reference View Changed') 

387 yrange = self.referencesPlot.getViewBox().viewRange()[1] 

388 print('YRange: {:}'.format(yrange)) 

389 ysize = (yrange[1] - yrange[0]) * .9 

390 ystart = yrange[0] + 0.05 * (yrange[1] - yrange[0]) 

391 print('YStart: {:}'.format(ystart)) 

392 print('YSize: {:}'.format(ysize)) 

393 print('YEnd: {:}'.format(ystart + ysize)) 

394 for roi in self.reference_rois: 

395 currentX, currentY = roi.pos() 

396 currentxsize, currentysize = roi.size() 

397 roi.setPos(currentX, ystart) 

398 roi.setSize((currentxsize, ysize)) 

399 

400 def responseViewChanged(self): 

401 print('Response View Changed') 

402 yrange = self.responsesPlot.getViewBox().viewRange()[1] 

403 ysize = (yrange[1] - yrange[0]) * .9 

404 ystart = yrange[0] + 0.05 * (yrange[1] - yrange[0]) 

405 for roi in self.response_rois: 

406 currentX, currentY = roi.pos() 

407 currentxsize, currentysize = roi.size() 

408 roi.setPos(currentX, ystart) 

409 roi.setSize((currentxsize, ysize)) 

410 

411 def get_abscissa_index_range(self): 

412 return (int(np.round(self.startTimeDoubleSpinBox.value() * self.sample_rate)), 

413 int(np.round(self.endTimeDoubleSpinBox.value() * self.sample_rate))) 

414 

415 def overlapChanged(self): 

416 print('Overlap Changed') 

417 self.block_averaging_signals(True) 

418 frame_size = self.frameSizeSpinBox.value() 

419 overlap_samples = int(np.round(self.overlapDoubleSpinBox.value() / 100 * frame_size)) 

420 self.overlapSamplesSpinBox.setValue(overlap_samples) 

421 self.overlapDoubleSpinBox.setValue(100 * overlap_samples / frame_size) 

422 # Compute the number of frames available 

423 min_index, max_index = self.get_abscissa_index_range() 

424 total_samples = max_index - min_index + 1 

425 self.framesSpinBox.setValue( 

426 int((total_samples - overlap_samples) / (frame_size - overlap_samples))) 

427 self.block_averaging_signals(False) 

428 self.frame_start_indices = [min_index + i * (frame_size - overlap_samples) 

429 for i in range(self.framesSpinBox.value())] 

430 self.ignore_frames = [] 

431 self.create_rois() 

432 

433 def overlapSamplesChanged(self): 

434 print('Overlap Samples Changed') 

435 self.block_averaging_signals(True) 

436 frame_size = self.frameSizeSpinBox.value() 

437 overlap_samples = self.overlapSamplesSpinBox.value() 

438 self.overlapDoubleSpinBox.setValue(100 * overlap_samples / frame_size) 

439 # Compute the number of frames available 

440 min_index, max_index = self.get_abscissa_index_range() 

441 total_samples = max_index - min_index + 1 

442 self.framesSpinBox.setValue( 

443 int((total_samples - overlap_samples) / (frame_size - overlap_samples))) 

444 self.block_averaging_signals(False) 

445 self.frame_start_indices = [min_index + i * (frame_size - overlap_samples) 

446 for i in range(self.framesSpinBox.value())] 

447 self.ignore_frames = [] 

448 self.create_rois() 

449 

450 def framesChanged(self): 

451 print('Number Frames Changed') 

452 self.block_averaging_signals(True) 

453 frame_size = self.frameSizeSpinBox.value() 

454 frames = self.framesSpinBox.value() 

455 min_index, max_index = self.get_abscissa_index_range() 

456 total_samples = max_index - min_index + 1 

457 try: 

458 overlap_samples = (frames * frame_size - total_samples) / (frames - 1) 

459 except ZeroDivisionError: 

460 overlap_samples = 0 

461 print('Overlap Samples {:}'.format(overlap_samples)) 

462 if overlap_samples < 0: 

463 overlap_samples = 0 

464 self.overlapSamplesSpinBox.setValue(overlap_samples) 

465 self.overlapDoubleSpinBox.setValue(100 * overlap_samples / frame_size) 

466 self.block_averaging_signals(False) 

467 self.frame_start_indices = [min_index + i * (frame_size - overlap_samples) 

468 for i in range(self.framesSpinBox.value())] 

469 self.ignore_frames = [] 

470 self.create_rois() 

471 

472 def startTimeChanged(self): 

473 print('Start Time Changed') 

474 self.block_data_range_signals(True) 

475 samples = np.round(self.startTimeDoubleSpinBox.value() * self.sample_rate) 

476 self.startTimeDoubleSpinBox.setValue(samples / self.sample_rate) 

477 if self.startTimeDoubleSpinBox.value() > self.endTimeDoubleSpinBox.value(): 

478 self.startTimeDoubleSpinBox.setValue(self.endTimeDoubleSpinBox.value()) 

479 self.time_selector_references.setRegion((self.startTimeDoubleSpinBox.value(), 

480 self.endTimeDoubleSpinBox.value())) 

481 self.time_selector_responses.setRegion((self.startTimeDoubleSpinBox.value(), 

482 self.endTimeDoubleSpinBox.value())) 

483 self.block_data_range_signals(False) 

484 # Now adjust averaging information 

485 if self.typeComboBox.currentIndex() == 0: 

486 self.overlapChanged() 

487 else: 

488 self.compute_triggers() 

489 # Adjust limits on the frame size 

490 dt = 1 / self.sample_rate 

491 min_index, max_index = self.get_abscissa_index_range() 

492 total_samples = max_index - min_index + 1 

493 self.frameSizeSpinBox.setMaximum(total_samples) 

494 self.frameTimeDoubleSpinBox.setMaximum(dt * total_samples) 

495 self.frequencyLinesSpinBox.setMaximum(total_samples // 2 + 1) 

496 self.frequencySpacingDoubleSpinBox.setMinimum(1 / (dt * total_samples)) 

497 

498 def endTimeChanged(self): 

499 print('End Time Changed') 

500 self.block_data_range_signals(True) 

501 samples = np.round(self.endTimeDoubleSpinBox.value() * self.sample_rate) 

502 self.endTimeDoubleSpinBox.setValue(samples / self.sample_rate) 

503 if self.startTimeDoubleSpinBox.value() > self.endTimeDoubleSpinBox.value(): 

504 self.endTimeDoubleSpinBox.setValue(self.startTimeDoubleSpinBox.value()) 

505 self.time_selector_references.setRegion((self.startTimeDoubleSpinBox.value(), 

506 self.endTimeDoubleSpinBox.value())) 

507 self.time_selector_responses.setRegion((self.startTimeDoubleSpinBox.value(), 

508 self.endTimeDoubleSpinBox.value())) 

509 self.block_data_range_signals(False) 

510 # Now adjust averaging information 

511 if self.typeComboBox.currentIndex() == 0: 

512 self.overlapChanged() 

513 else: 

514 self.compute_triggers() 

515 # Adjust limits on the frame size 

516 dt = 1 / self.sample_rate 

517 min_index, max_index = self.get_abscissa_index_range() 

518 total_samples = max_index - min_index + 1 

519 self.frameSizeSpinBox.setMaximum(total_samples) 

520 self.frameTimeDoubleSpinBox.setMaximum(dt * total_samples) 

521 self.frequencyLinesSpinBox.setMaximum(total_samples // 2 + 1) 

522 self.frequencySpacingDoubleSpinBox.setMinimum(1 / (dt * total_samples)) 

523 

524 def updateTimeFromReference(self): 

525 print('Time Changed from References') 

526 self.block_data_range_signals(True) 

527 min_time, max_time = self.time_selector_references.getRegion() 

528 samples_min = np.round(min_time * self.sample_rate) 

529 samples_max = np.round(max_time * self.sample_rate) 

530 min_time = samples_min / self.sample_rate 

531 max_time = samples_max / self.sample_rate 

532 self.startTimeDoubleSpinBox.setValue(min_time) 

533 self.endTimeDoubleSpinBox.setValue(max_time) 

534 self.time_selector_references.setRegion((self.startTimeDoubleSpinBox.value(), 

535 self.endTimeDoubleSpinBox.value())) 

536 self.time_selector_responses.setRegion((self.startTimeDoubleSpinBox.value(), 

537 self.endTimeDoubleSpinBox.value())) 

538 self.block_data_range_signals(False) 

539 # Now adjust averaging information 

540 if self.typeComboBox.currentIndex() == 0: 

541 self.overlapChanged() 

542 else: 

543 self.compute_triggers() 

544 # Adjust limits on the frame size 

545 dt = 1 / self.sample_rate 

546 min_index, max_index = self.get_abscissa_index_range() 

547 total_samples = max_index - min_index + 1 

548 self.frameSizeSpinBox.setMaximum(total_samples) 

549 self.frameTimeDoubleSpinBox.setMaximum(dt * total_samples) 

550 self.frequencyLinesSpinBox.setMaximum(total_samples // 2 + 1) 

551 self.frequencySpacingDoubleSpinBox.setMinimum(1 / (dt * total_samples)) 

552 

553 def updateTimeFromResponse(self): 

554 print('Time Changed from Responses') 

555 self.block_data_range_signals(True) 

556 min_time, max_time = self.time_selector_responses.getRegion() 

557 samples_min = np.round(min_time * self.sample_rate) 

558 samples_max = np.round(max_time * self.sample_rate) 

559 min_time = samples_min / self.sample_rate 

560 max_time = samples_max / self.sample_rate 

561 self.startTimeDoubleSpinBox.setValue(min_time) 

562 self.endTimeDoubleSpinBox.setValue(max_time) 

563 self.time_selector_references.setRegion((self.startTimeDoubleSpinBox.value(), 

564 self.endTimeDoubleSpinBox.value())) 

565 self.time_selector_responses.setRegion((self.startTimeDoubleSpinBox.value(), 

566 self.endTimeDoubleSpinBox.value())) 

567 self.block_data_range_signals(False) 

568 # Now adjust averaging information 

569 if self.typeComboBox.currentIndex() == 0: 

570 self.overlapChanged() 

571 else: 

572 self.compute_triggers() 

573 # Adjust limits on the frame size 

574 dt = 1 / self.sample_rate 

575 min_index, max_index = self.get_abscissa_index_range() 

576 total_samples = max_index - min_index + 1 

577 self.frameSizeSpinBox.setMaximum(total_samples) 

578 self.frameTimeDoubleSpinBox.setMaximum(dt * total_samples) 

579 self.frequencyLinesSpinBox.setMaximum(total_samples // 2 + 1) 

580 self.frequencySpacingDoubleSpinBox.setMinimum(1 / (dt * total_samples)) 

581 

582 def frameSizeChanged(self): 

583 print('Frame Size Changed') 

584 self.block_sampling_signals(True) 

585 frame_size = self.frameSizeSpinBox.value() 

586 # Make sure it stays as a factor of 2 

587 if frame_size % 2 == 1: 

588 self.frameSizeSpinBox.setValue(frame_size + 1) 

589 frame_size += 1 

590 self.frequencyLinesSpinBox.setValue(frame_size // 2 + 1) 

591 self.frameTimeDoubleSpinBox.setValue(frame_size / self.sample_rate) 

592 self.frequencySpacingDoubleSpinBox.setValue(self.sample_rate / frame_size) 

593 self.block_sampling_signals(False) 

594 if self.typeComboBox.currentIndex() == 0: 

595 self.overlapChanged() 

596 else: 

597 self.compute_triggers() 

598 

599 def frequencyLinesChanged(self): 

600 print('Frequency Lines Changed') 

601 self.block_sampling_signals(True) 

602 frame_size = (self.frequencyLinesSpinBox.value() - 1) * 2 

603 self.frameSizeSpinBox.setValue(frame_size) 

604 self.frameTimeDoubleSpinBox.setValue(frame_size / self.sample_rate) 

605 self.frequencySpacingDoubleSpinBox.setValue(self.sample_rate / frame_size) 

606 self.block_sampling_signals(False) 

607 self.overlapChanged() 

608 

609 def frameTimeChanged(self): 

610 print('Frame Time Changed') 

611 frame_size = int(np.round(self.frameTimeDoubleSpinBox.value() * self.sample_rate)) 

612 self.frameSizeSpinBox.setValue(frame_size) 

613 

614 def frequencySpacingChanged(self): 

615 print('Frequency Spacing Changed') 

616 frame_time = 1 / self.frequencySpacingDoubleSpinBox.value() 

617 self.frameTimeDoubleSpinBox.setValue(frame_time) 

618 

619 def windowChanged(self): 

620 print('Window Changed') 

621 window_text = self.windowComboBox.currentText().lower() 

622 self.windowParameter1DoubleSpinBox.setVisible(False) 

623 self.windowParameter2DoubleSpinBox.setVisible(False) 

624 self.windowParameter3DoubleSpinBox.setVisible(False) 

625 self.windowParameter1Label.setVisible(False) 

626 self.windowParameter2Label.setVisible(False) 

627 self.windowParameter3Label.setVisible(False) 

628 for parameter, label, spinbox in zip(self.window_parameters[window_text], 

629 [self.windowParameter1Label, 

630 self.windowParameter2Label, 

631 self.windowParameter3Label], 

632 [self.windowParameter1DoubleSpinBox, 

633 self.windowParameter2DoubleSpinBox, 

634 self.windowParameter3DoubleSpinBox]): 

635 label.setText(parameter) 

636 label.setVisible(True) 

637 spinbox.setVisible(True) 

638 

639 def typeChanged(self): 

640 print('Type Changed') 

641 try: 

642 trigger_run = self.typeComboBox.currentIndex() != 0 

643 self.framesSpinBox.setReadOnly(trigger_run) 

644 self.framesSpinBox.setButtonSymbols( 

645 self.framesSpinBox.NoButtons if trigger_run else self.framesSpinBox.UpDownArrows) 

646 for widget in [self.channelComboBox, 

647 self.slopeComboBox, 

648 self.levelDoubleSpinBox, 

649 self.pretriggerDoubleSpinBox, 

650 self.channelLabel, 

651 self.slopeLabel, 

652 self.levelLabel, 

653 self.pretriggerLabel, 

654 self.hysteresisLevelDoubleSpinBox, 

655 self.hysteresisLevelLabel 

656 ]: 

657 widget.setVisible(trigger_run) 

658 for widget in [self.overlapDoubleSpinBox, 

659 self.overlapSamplesSpinBox, 

660 self.overlapLabel, 

661 self.overlapSamplesLabel]: 

662 widget.setVisible(not trigger_run) 

663 if self.typeComboBox.currentIndex() == 0: 

664 self.overlapChanged() 

665 else: 

666 self.compute_triggers() 

667 self.referencesChanged() 

668 self.responsesChanged() 

669 except Exception as e: 

670 print(e) 

671 

672 def referencesChanged(self): 

673 print('References Changed') 

674 indices = [(item.data(Qt.UserRole), item.text()) 

675 for item in self.referencesSelector.selectedItems()] 

676 try: 

677 xrange = self.referencesPlot.getViewBox().viewRange()[0] 

678 except AttributeError: 

679 xrange = None 

680 self.referencesPlot.clear() 

681 for j, (index, text) in enumerate(indices): 

682 data_entry = self.time_history_data[index] 

683 pen = pqtg.mkPen(color=[int(255 * v) for v in self.cm(j % self.cm_mod)]) 

684 self.referencesPlot.plot(x=data_entry.abscissa, 

685 y=data_entry.ordinate, name=text, pen=pen) 

686 if xrange is not None: 

687 self.referencesPlot.setXRange(*xrange, padding=0.0) 

688 self.referencesPlot.addItem(self.time_selector_references) 

689 self.referencesPlot.addItem(self.trigger_level_selector_references) 

690 self.referencesPlot.addItem(self.hysteresis_level_selector_references) 

691 for roi in self.reference_rois: 

692 self.referencesPlot.addItem(roi, ignoreBounds=True) 

693 if self.typeComboBox.currentIndex() == 1 and self.channelComboBox.currentIndex() in [index[0] for index in indices]: 

694 for widget in [self.trigger_level_selector_references, 

695 self.hysteresis_level_selector_references]: 

696 widget.setVisible(True) 

697 else: 

698 for widget in [self.trigger_level_selector_references, 

699 self.hysteresis_level_selector_references]: 

700 widget.setVisible(False) 

701 

702 def responsesChanged(self): 

703 print('Responses Changed') 

704 indices = [(item.data(Qt.UserRole), item.text()) 

705 for item in self.responsesSelector.selectedItems()] 

706 # print(indices) 

707 try: 

708 xrange = self.responsesPlot.getViewBox().viewRange()[0] 

709 except AttributeError: 

710 xrange = None 

711 self.responsesPlot.clear() 

712 for j, (index, text) in enumerate(indices): 

713 data_entry = self.time_history_data[index] 

714 # print(data_entry.abscissa) 

715 # print(data_entry.ordinate) 

716 pen = pqtg.mkPen(color=[int(255 * v) for v in self.cm(j % self.cm_mod)]) 

717 # print(pen) 

718 self.responsesPlot.plot(x=data_entry.abscissa, 

719 y=data_entry.ordinate, name=text, pen=pen) 

720 if xrange is not None: 

721 self.responsesPlot.setXRange(*xrange, padding=0.0) 

722 self.responsesPlot.addItem(self.time_selector_responses) 

723 self.responsesPlot.addItem(self.trigger_level_selector_responses) 

724 self.responsesPlot.addItem(self.hysteresis_level_selector_responses) 

725 for roi in self.response_rois: 

726 self.responsesPlot.addItem(roi, ignoreBounds=True) 

727 if self.typeComboBox.currentIndex() == 1 and self.channelComboBox.currentIndex() in [index[0] for index in indices]: 

728 for widget in [self.trigger_level_selector_responses, 

729 self.hysteresis_level_selector_responses]: 

730 widget.setVisible(True) 

731 else: 

732 for widget in [self.trigger_level_selector_responses, 

733 self.hysteresis_level_selector_responses]: 

734 widget.setVisible(False) 

735 

736 def sendToResponse(self): 

737 print('Sent to Response') 

738 try: 

739 self.responsesSelector.clearSelection() 

740 for item in self.referencesSelector.selectedItems(): 

741 # Get the indices 

742 moved_item = self.referencesSelector.takeItem(self.referencesSelector.row(item)) 

743 # Get the responses data 

744 indices = np.array([self.responsesSelector.item(index).data(Qt.UserRole) 

745 for index in range(self.responsesSelector.count())]) 

746 # Find the row we want to put it in 

747 index_checks = indices > moved_item.data(Qt.UserRole) 

748 if len(index_checks) == 0: 

749 row = 0 

750 elif index_checks.sum() == 0: 

751 row = self.referencesSelector.count() 

752 else: 

753 row = np.argmax(index_checks) 

754 self.responsesSelector.insertItem(row, moved_item) 

755 item.setSelected(True) 

756 self.referencesSpinBox.setValue(self.referencesSelector.count()) 

757 self.responsesSpinBox.setValue(self.responsesSelector.count()) 

758 except Exception as e: 

759 print(e) 

760 

761 def sendToReference(self): 

762 print('Sent to Reference') 

763 try: 

764 self.referencesSelector.clearSelection() 

765 for item in self.responsesSelector.selectedItems(): 

766 # Get the indices 

767 moved_item = self.responsesSelector.takeItem(self.responsesSelector.row(item)) 

768 # Get the responses data 

769 indices = np.array([self.referencesSelector.item(index).data(Qt.UserRole) 

770 for index in range(self.referencesSelector.count())]) 

771 # Find the row we want to put it in 

772 index_checks = indices > moved_item.data(Qt.UserRole) 

773 if len(index_checks) == 0: 

774 row = 0 

775 elif index_checks.sum() == 0: 

776 row = self.referencesSelector.count() 

777 else: 

778 row = np.argmax(index_checks) 

779 self.referencesSelector.insertItem(row, moved_item) 

780 item.setSelected(True) 

781 self.referencesSpinBox.setValue(self.referencesSelector.count()) 

782 self.responsesSpinBox.setValue(self.responsesSelector.count()) 

783 except Exception as e: 

784 print(e) 

785 

786 def updateTriggerFromReference(self): 

787 print('Trigger Level Changed from Reference') 

788 self.block_averaging_signals(True) 

789 value = self.trigger_level_selector_references.value() 

790 hysteresis_value = self.hysteresis_level_selector_references.value() 

791 print('Value: {:}'.format(value)) 

792 slope_positive = self.slopeComboBox.currentIndex() == 0 

793 if slope_positive and value < hysteresis_value: 

794 value = hysteresis_value 

795 if (not slope_positive) and (value > hysteresis_value): 

796 value = hysteresis_value 

797 print('Value: {:}'.format(value)) 

798 self.levelDoubleSpinBox.setValue(value) 

799 self.trigger_level_selector_responses.setValue(value) 

800 self.trigger_level_selector_references.setValue(value) 

801 self.block_averaging_signals(False) 

802 self.compute_triggers() 

803 

804 def updateTriggerFromResponse(self): 

805 print('Trigger Level Changed from Response') 

806 self.block_averaging_signals(True) 

807 value = self.trigger_level_selector_responses.value() 

808 hysteresis_value = self.hysteresis_level_selector_responses.value() 

809 print('Value: {:}'.format(value)) 

810 slope_positive = self.slopeComboBox.currentIndex() == 0 

811 if slope_positive and value < hysteresis_value: 

812 value = hysteresis_value 

813 if (not slope_positive) and (value > hysteresis_value): 

814 value = hysteresis_value 

815 print('Value: {:}'.format(value)) 

816 self.levelDoubleSpinBox.setValue(value) 

817 self.trigger_level_selector_references.setValue(value) 

818 self.trigger_level_selector_responses.setValue(value) 

819 self.block_averaging_signals(False) 

820 self.compute_triggers() 

821 

822 def updateHysteresisFromReference(self): 

823 print('Hysteresis Level Changed from Reference') 

824 self.block_averaging_signals(True) 

825 value = self.trigger_level_selector_references.value() 

826 hysteresis_value = self.hysteresis_level_selector_references.value() 

827 print('Hysteresis Value: {:}'.format(hysteresis_value)) 

828 slope_positive = self.slopeComboBox.currentIndex() == 0 

829 if slope_positive and value < hysteresis_value: 

830 hysteresis_value = value 

831 if (not slope_positive) and (value > hysteresis_value): 

832 hysteresis_value = value 

833 print('Hysteresis Value: {:}'.format(hysteresis_value)) 

834 self.hysteresisLevelDoubleSpinBox.setValue(hysteresis_value) 

835 self.hysteresis_level_selector_responses.setValue(hysteresis_value) 

836 self.hysteresis_level_selector_references.setValue(hysteresis_value) 

837 self.block_averaging_signals(False) 

838 self.compute_triggers() 

839 

840 def updateHysteresisFromResponse(self): 

841 print('Hysteresis Level Changed from Response') 

842 self.block_averaging_signals(True) 

843 value = self.trigger_level_selector_responses.value() 

844 hysteresis_value = self.hysteresis_level_selector_responses.value() 

845 print('Hysteresis Value: {:}'.format(hysteresis_value)) 

846 slope_positive = self.slopeComboBox.currentIndex() == 0 

847 if slope_positive and value < hysteresis_value: 

848 hysteresis_value = value 

849 if (not slope_positive) and (value > hysteresis_value): 

850 hysteresis_value = value 

851 print('Hysteresis Value: {:}'.format(hysteresis_value)) 

852 self.hysteresisLevelDoubleSpinBox.setValue(hysteresis_value) 

853 self.hysteresis_level_selector_responses.setValue(hysteresis_value) 

854 self.hysteresis_level_selector_references.setValue(hysteresis_value) 

855 self.block_averaging_signals(False) 

856 self.compute_triggers() 

857 

858 def levelChanged(self): 

859 print('Trigger Level Changed') 

860 self.block_averaging_signals(True) 

861 slope_positive = self.slopeComboBox.currentIndex() == 0 

862 if slope_positive and self.levelDoubleSpinBox.value() < self.hysteresisLevelDoubleSpinBox.value(): 

863 self.levelDoubleSpinBox.setValue(self.hysteresisLevelDoubleSpinBox.value()) 

864 if (not slope_positive) and (self.levelDoubleSpinBox.value() > self.hysteresisLevelDoubleSpinBox.value()): 

865 self.levelDoubleSpinBox.setValue(self.hysteresisLevelDoubleSpinBox.value()) 

866 self.trigger_level_selector_references.setValue(self.levelDoubleSpinBox.value()) 

867 self.trigger_level_selector_responses.setValue(self.levelDoubleSpinBox.value()) 

868 self.block_averaging_signals(False) 

869 self.compute_triggers() 

870 

871 def hysteresisChanged(self): 

872 print('Hysteresis Level Changed') 

873 self.block_averaging_signals(True) 

874 slope_positive = self.slopeComboBox.currentIndex() == 0 

875 if slope_positive and self.levelDoubleSpinBox.value() < self.hysteresisLevelDoubleSpinBox.value(): 

876 self.hysteresisLevelDoubleSpinBox.setValue(self.levelDoubleSpinBox.value()) 

877 if (not slope_positive) and (self.levelDoubleSpinBox.value() > self.hysteresisLevelDoubleSpinBox.value()): 

878 self.hysteresisLevelDoubleSpinBox.setValue(self.levelDoubleSpinBox.value()) 

879 self.hysteresis_level_selector_references.setValue( 

880 self.hysteresisLevelDoubleSpinBox.value()) 

881 self.hysteresis_level_selector_responses.setValue(self.hysteresisLevelDoubleSpinBox.value()) 

882 self.block_averaging_signals(False) 

883 self.compute_triggers() 

884 

885 def pretriggerChanged(self): 

886 print('Pretrigger Changed') 

887 self.compute_triggers() 

888 

889 def compute_triggers(self): 

890 try: 

891 print('Computing Triggers') 

892 level = self.levelDoubleSpinBox.value() 

893 hysteresis = self.hysteresisLevelDoubleSpinBox.value() 

894 positive_slope = self.slopeComboBox.currentIndex() == 0 

895 channel_index = self.channelComboBox.currentIndex() 

896 pretrigger_samples = int(self.pretriggerDoubleSpinBox.value() / 

897 100 * self.frameSizeSpinBox.value()) 

898 min_abscissa_index, max_abscissa_index = self.get_abscissa_index_range() 

899 frame_size = self.frameSizeSpinBox.value() 

900 data = self.time_history_data[channel_index].ordinate 

901 if positive_slope: 

902 potential_triggers = np.where((data[:-1] <= level) & (data[1:] > level))[0] 

903 trigger_resets = data[:-1] < hysteresis 

904 else: 

905 potential_triggers = np.where((data[:-1] >= level) & (data[1:] < level))[0] 

906 trigger_resets = data[:-1] > hysteresis 

907 triggers = [] 

908 for trigger in potential_triggers: 

909 last_trigger = triggers[-1] if len(triggers) > 0 else 0 

910 if (np.any(trigger_resets[last_trigger:trigger]) 

911 and (trigger - pretrigger_samples >= min_abscissa_index) 

912 and (trigger - pretrigger_samples + frame_size < max_abscissa_index) 

913 and (len(triggers) == 0 or (trigger - pretrigger_samples - triggers[-1] >= frame_size))): 

914 triggers.append(trigger - pretrigger_samples) 

915 self.block_averaging_signals(True) 

916 self.framesSpinBox.setValue(len(triggers)) 

917 self.block_averaging_signals(False) 

918 self.ignore_frames = [] 

919 self.frame_start_indices = triggers 

920 self.create_rois() 

921 except Exception as e: 

922 print(e) 

923 

924 def compute(self): 

925 # Split up the measurement into frames 

926 

927 try: 

928 frequency_spacing = self.frequencySpacingDoubleSpinBox.value() 

929 frame_size = self.frameSizeSpinBox.value() 

930 frame_indices = np.array([start_index for frame_number, start_index in enumerate(self.frame_start_indices) 

931 if frame_number not in self.ignore_frames])[:, np.newaxis] + np.arange(frame_size) 

932 if frame_indices.shape[0] == 0: 

933 QMessageBox.critical(self, 'Invalid Frames', 

934 'At least one measurement frame must be selected') 

935 return 

936 reference_indices = np.array([self.referencesSelector.item(i).data( 

937 Qt.UserRole) for i in range(self.referencesSelector.count())]) 

938 if reference_indices.size == 0: 

939 QMessageBox.critical(self, 'Invalid References', 

940 'At least one reference must be selected') 

941 return 

942 response_indices = np.array([self.responsesSelector.item(i).data( 

943 Qt.UserRole) for i in range(self.responsesSelector.count())]) 

944 if response_indices.size == 0: 

945 QMessageBox.critical(self, 'Invalid Responses', 

946 'At least one response must be selected') 

947 return 

948 time_by_frames = self.time_history_data.ordinate[..., frame_indices] 

949 reference_data = time_by_frames[reference_indices].copy() 

950 response_data = time_by_frames[response_indices].copy() 

951 # print(reference_data.shape) 

952 # print(response_data.shape) 

953 # Get the window function 

954 window_function_string = self.windowComboBox.currentText().lower() 

955 num_parameters = len(self.window_parameters[window_function_string]) 

956 window_parameters = [widget.value() for widget in [self.windowParameter1DoubleSpinBox, 

957 self.windowParameter2DoubleSpinBox, 

958 self.windowParameter3DoubleSpinBox][:num_parameters]] 

959 if window_function_string == 'rectangle': 

960 window_function_string = 'boxcar' 

961 if window_function_string == 'exponential+force': 

962 window_function_string = 'exponential' 

963 response_window = get_window( 

964 tuple([window_function_string] + window_parameters[1:]), frame_size, fftbins=True) 

965 non_pulse_samples = np.arange(frame_size) / self.sample_rate > window_parameters[0] 

966 reference_window = response_window.copy() 

967 reference_window[non_pulse_samples] = 0 

968 # Need to adjust the reference signals so we don't create jumps when we apply the force window 

969 dc_offsets = np.mean(reference_data[..., non_pulse_samples], axis=-1, keepdims=True) 

970 reference_data -= dc_offsets 

971 else: 

972 response_window = get_window( 

973 tuple([window_function_string] + window_parameters), frame_size, fftbins=True) 

974 reference_window = response_window 

975 window_correction = 1 / np.mean(response_window**2) 

976 response_data *= response_window 

977 reference_data *= reference_window 

978 reference_coordinates = self.time_history_data[reference_indices].coordinate.flatten() 

979 response_coordinates = self.time_history_data[response_indices].coordinate.flatten() 

980 # Create time history array 

981 self.windowed_time_data = data_array(FunctionTypes.TIME_RESPONSE, 

982 np.arange(frame_size) / self.sample_rate, 

983 np.concatenate( 

984 (reference_data, response_data), axis=0), 

985 self.time_history_data.coordinate[np.concatenate((reference_indices, response_indices)), np.newaxis]) 

986 self.plotWindowedTimeHistoryButton.setEnabled(True) 

987 self.saveWindowedTimeHistoryButton.setEnabled(True) 

988 response_fft = np.fft.rfft(response_data, axis=-1) 

989 reference_fft = np.fft.rfft(reference_data, axis=-1) 

990 # print(reference_fft.shape) 

991 # print(response_fft.shape) 

992 freq = np.fft.rfftfreq(frame_size, 1 / self.sample_rate) 

993 # Compute FRFs 

994 if self.frfCheckBox.isChecked(): 

995 # Check the type of FRF 

996 frf_coordinate = outer_product(response_coordinates, 

997 reference_coordinates) 

998 success = False 

999 if self.frfComboBox.currentIndex() == 0: # H1 

1000 # We want to compute X*F^H = [X1;X2;X3][F1^H F2^H F3^H] 

1001 Gxf = np.sum(np.einsum('iaf,jaf->afij', response_fft, 

1002 np.conj(reference_fft)), axis=0) 

1003 Gff = np.sum(np.einsum('iaf,jaf->afij', reference_fft, 

1004 np.conj(reference_fft)), axis=0) 

1005 # Add small values to any matrices that are singular 

1006 singular_matrices = np.abs(np.linalg.det(Gff)) < 2 * np.finfo(Gff.dtype).eps 

1007 Gff[singular_matrices] += np.eye(Gff.shape[-1]) * np.finfo(Gff.dtype).eps 

1008 H = np.linalg.solve(Gff.transpose(0, 2, 1), 

1009 Gxf.transpose(0, 2, 1)).transpose(0, 2, 1) 

1010 # Create TransferFunctionArray 

1011 self.frf_data = data_array(FunctionTypes.FREQUENCY_RESPONSE_FUNCTION, 

1012 freq, np.moveaxis(H, 0, -1), frf_coordinate) 

1013 success = True 

1014 elif self.frfComboBox.currentIndex() == 1: # H2 

1015 if (response_fft.shape != reference_fft.shape): 

1016 QMessageBox.critical(self, 'Bad FRF Shape', 

1017 'For H2, Number of inputs must equal number of outputs') 

1018 success = False 

1019 else: 

1020 Gxx = np.einsum('iaf,jaf->fij', response_fft, np.conj(response_fft)) 

1021 Gfx = np.einsum('iaf,jaf->fij', reference_fft, np.conj(response_fft)) 

1022 singular_matrices = np.abs(np.linalg.det(Gfx)) < 2 * np.finfo(Gfx.dtype).eps 

1023 Gfx[singular_matrices] += np.eye(Gfx.shape[-1]) * np.finfo(Gfx.dtype).eps 

1024 H = np.moveaxis(np.linalg.solve(np.moveaxis(Gfx, -2, -1), np.moveaxis(Gxx, -2, -1)), -2, -1) 

1025 # Create TransferFunctionArray 

1026 self.frf_data = data_array(FunctionTypes.FREQUENCY_RESPONSE_FUNCTION, 

1027 freq, np.moveaxis(H, 0, -1), frf_coordinate) 

1028 success = True 

1029 elif self.frfComboBox.currentIndex() == 2: # H3 

1030 if (response_fft.shape != reference_fft.shape): 

1031 QMessageBox.critical(self, 'Bad FRF Shape', 

1032 'For H3, Number of inputs must equal number of outputs') 

1033 success = False 

1034 else: 

1035 Gxf = np.einsum('iaf,jaf->fij', response_fft, np.conj(reference_fft)) 

1036 Gff = np.einsum('iaf,jaf->fij', reference_fft, np.conj(reference_fft)) 

1037 # Add small values to any matrices that are singular 

1038 singular_matrices = np.abs(np.linalg.det(Gff)) < 2 * np.finfo(Gff.dtype).eps 

1039 Gff[singular_matrices] += np.eye(Gff.shape[-1]) * np.finfo(Gff.dtype).eps 

1040 Gxx = np.einsum('iaf,jaf->fij', response_fft, np.conj(response_fft)) 

1041 Gfx = np.einsum('iaf,jaf->fij', reference_fft, np.conj(response_fft)) 

1042 singular_matrices = np.abs(np.linalg.det(Gfx)) < 2 * np.finfo(Gfx.dtype).eps 

1043 Gfx[singular_matrices] += np.eye(Gfx.shape[-1]) * np.finfo(Gfx.dtype).eps 

1044 H = (np.moveaxis(np.linalg.solve(np.moveaxis(Gfx, -2, -1), np.moveaxis(Gxx, -2, -1)), -2, -1) + 

1045 np.moveaxis(np.linalg.solve(np.moveaxis(Gff, -2, -1), np.moveaxis(Gxf, -2, -1)), -2, -1)) / 2 

1046 self.frf_data = data_array(FunctionTypes.FREQUENCY_RESPONSE_FUNCTION, 

1047 freq, np.moveaxis(H, 0, -1), frf_coordinate) 

1048 success = True 

1049 elif self.frfComboBox.currentIndex() == 3: # Hv 

1050 Gxx = np.einsum('...iaf,...iaf->...if', response_fft, 

1051 np.conj(response_fft))[..., np.newaxis, np.newaxis] 

1052 Gxf = np.einsum('...iaf,...jaf->...ifj', response_fft, 

1053 np.conj(reference_fft))[..., np.newaxis, :] 

1054 Gff = np.einsum('...iaf,...jaf->...fij', reference_fft, 

1055 np.conj(reference_fft))[..., np.newaxis, :, :, :] 

1056 # print(Gxx.shape) 

1057 # print(Gxf.shape) 

1058 # print(Gff.shape) 

1059 # Broadcast over all responses 

1060 Gff = np.broadcast_to(Gff, Gxx.shape[:-2]+Gff.shape[-2:]) 

1061 Gffx = np.block([[Gff, np.conj(np.moveaxis(Gxf, -2, -1))], 

1062 [Gxf, Gxx]]) 

1063 # Compute eigenvalues 

1064 lam, evect = np.linalg.eigh(np.moveaxis(Gffx, -2, -1)) 

1065 # Get the evect corresponding to the minimum eigenvalue 

1066 evect = evect[..., 0] # Assumes evals are sorted ascending 

1067 H = np.moveaxis(-evect[..., :-1] / evect[..., -1:], # Scale so last value is -1 

1068 -3, -2) 

1069 self.frf_data = data_array(FunctionTypes.FREQUENCY_RESPONSE_FUNCTION, 

1070 freq, np.moveaxis(H, 0, -1), frf_coordinate) 

1071 success = True 

1072 else: 

1073 QMessageBox.critical(self, 'FRF Technique Not Implemented', 

1074 'FRF Technique {:} has not been implemented yet, sorry!'.format(self.frfComboBox.currentText())) 

1075 success = False 

1076 if success: 

1077 self.plotFRFButton.setEnabled(True) 

1078 self.saveFRFButton.setEnabled(True) 

1079 self.menuVisualize_with_DeflectionShapePlotter.setEnabled(True) 

1080 self.actionSend_to_SMAC.setEnabled(True) 

1081 self.actionSend_to_PolyPy.setEnabled(True) 

1082 # Add actions to a menu 

1083 self.menuVisualize_with_DeflectionShapePlotter.clear() 

1084 self.deflection_actions.clear() 

1085 for reference_coord in reference_coordinates: 

1086 action = QAction('Reference {:}'.format(str(reference_coord))) 

1087 action.triggered.connect(self.plotDeflection) 

1088 self.menuVisualize_with_DeflectionShapePlotter.addAction(action) 

1089 self.deflection_actions.append(action) 

1090 if self.autospectraCheckBox.isChecked(): 

1091 # Compute Autospectra 

1092 full_fft = np.concatenate((reference_fft, response_fft), axis=0) 

1093 full_coords = np.concatenate((reference_coordinates, response_coordinates)) 

1094 spectral_matrix = np.einsum('iaf,iaf->fi', full_fft, 

1095 np.conj(full_fft)) / full_fft.shape[1] 

1096 spectral_matrix *= (frequency_spacing * window_correction / 

1097 self.sample_rate**2) 

1098 spectral_matrix[1:-1] *= 2 

1099 self.autospectra_data = data_array(FunctionTypes.POWER_SPECTRAL_DENSITY, freq, 

1100 np.moveaxis(spectral_matrix, 0, -1), 

1101 full_coords[:, np.newaxis]) 

1102 self.plotAutospectraButton.setEnabled(True) 

1103 self.saveAutospectraButton.setEnabled(True) 

1104 if self.crossspectraCheckBox.isChecked(): 

1105 # Compute cross-spectra 

1106 full_fft = np.concatenate((reference_fft, response_fft), axis=0) 

1107 full_coords = np.concatenate((reference_coordinates, response_coordinates)) 

1108 spectral_matrix = np.einsum('iaf,jaf->fij', full_fft, 

1109 np.conj(full_fft)) / full_fft.shape[1] 

1110 spectral_matrix *= (frequency_spacing * window_correction / 

1111 self.sample_rate**2) 

1112 spectral_matrix[1:-1] *= 2 

1113 self.crossspectra_data = data_array(FunctionTypes.POWER_SPECTRAL_DENSITY, freq, 

1114 np.moveaxis(spectral_matrix, 0, -1), 

1115 outer_product(full_coords, full_coords)) 

1116 self.plotCrossspectraButton.setEnabled(True) 

1117 self.saveCrossspectraButton.setEnabled(True) 

1118 if self.coherenceCheckBox.isChecked(): 

1119 if reference_fft.shape[0] == 1: 

1120 # Ordinary Coherence 

1121 Gxf = np.einsum('iaf,jaf->fij', response_fft, 

1122 np.conj(reference_fft)) / frame_indices.shape[0] 

1123 Gxx = np.einsum('iaf,iaf->fi', response_fft, 

1124 np.conj(response_fft)) / frame_indices.shape[0] 

1125 Gff = np.einsum('iaf,iaf->fi', reference_fft, 

1126 np.conj(reference_fft)) / frame_indices.shape[0] 

1127 coh = np.abs(Gxf)**2 / (Gxx[:, :, np.newaxis] * Gff[:, np.newaxis, :]) 

1128 self.coherence_data = data_array(FunctionTypes.COHERENCE, freq, 

1129 np.moveaxis(coh, 0, -1), 

1130 outer_product(response_coordinates, 

1131 reference_coordinates)) 

1132 else: 

1133 # Multiple Coherence 

1134 Gxf = np.einsum('iaf,jaf->fij', response_fft, 

1135 np.conj(reference_fft)) / frame_indices.shape[0] 

1136 Gxx = np.einsum('iaf,iaf->fi', response_fft, 

1137 np.conj(response_fft)) / frame_indices.shape[0] 

1138 Gff = np.einsum('iaf,jaf->fij', reference_fft, 

1139 np.conj(reference_fft)) / frame_indices.shape[0] 

1140 Mcoh = (np.einsum('fij,fjk,fik->fi', Gxf, 

1141 np.linalg.inv(Gff), Gxf.conj()) / Gxx).real 

1142 self.coherence_data = data_array(FunctionTypes.MULTIPLE_COHERENCE, freq, 

1143 np.moveaxis(Mcoh, 0, -1), response_coordinates[:, np.newaxis]) 

1144 self.plotCoherenceButton.setEnabled(True) 

1145 self.saveCoherenceButton.setEnabled(True) 

1146 except Exception: 

1147 print(traceback.format_exc()) 

1148 

1149 def showWindowedTimeHistory(self): 

1150 self.plots['windowed'] = GUIPlot(self.windowed_time_data) 

1151 

1152 def saveWindowedTimeHistory(self): 

1153 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

1154 self, 'Select File to Save Windowed Time Data', filter='Numpy File (*.npz)') 

1155 if filename == '': 

1156 return 

1157 self.windowed_time_data.save(filename) 

1158 

1159 def showFRF(self): 

1160 self.plots['frf'] = GUIPlot(self.frf_data) 

1161 

1162 def saveFRF(self): 

1163 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

1164 self, 'Select File to Save FRF Data', filter='Numpy File (*.npz)') 

1165 if filename == '': 

1166 return 

1167 self.frf_data.save(filename) 

1168 

1169 def showCoherence(self): 

1170 self.plots['coherence'] = GUIPlot(self.coherence_data) 

1171 

1172 def saveCoherence(self): 

1173 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

1174 self, 'Select File to Save Coherence Data', filter='Numpy File (*.npz)') 

1175 if filename == '': 

1176 return 

1177 self.coherence_data.save(filename) 

1178 

1179 def showAutospectra(self): 

1180 self.plots['autospectra'] = GUIPlot(self.autospectra_data) 

1181 

1182 def saveAutospectra(self): 

1183 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

1184 self, 'Select File to Save Autospectra Data', filter='Numpy File (*.npz)') 

1185 if filename == '': 

1186 return 

1187 self.autospectra_data.save(filename) 

1188 

1189 def showCrossspectra(self): 

1190 self.plots['crossspectra'] = GUIPlot(self.crossspectra_data) 

1191 

1192 def saveCrossspectra(self): 

1193 filename, file_filter = QtWidgets.QFileDialog.getSaveFileName( 

1194 self, 'Select File to Save Crosspectra Data', filter='Numpy File (*.npz)') 

1195 if filename == '': 

1196 return 

1197 self.crossspectra_data.save(filename) 

1198 

1199 def analyzeSMAC(self): 

1200 self.plots['smac'] = SMAC_GUI(self.frf_data) 

1201 self.plots['smac'].geometry = self.geometry 

1202 

1203 def analyzePolyPy(self): 

1204 self.plots['polypy'] = PolyPy_GUI(self.frf_data) 

1205 self.plots['polypy'].geometry = self.geometry 

1206 

1207 def loadGeometry(self): 

1208 filename, file_filter = QFileDialog.getOpenFileName( 

1209 self, 'Select Geometry File', filter='Numpyz (*.npz);;Universal File Format (*.uff *.unv)') 

1210 if filename == '': 

1211 return 

1212 self.geometry = Geometry.load(filename) 

1213 

1214 def plotTransient(self): 

1215 response_indices = np.array([self.responsesSelector.item(i).data( 

1216 Qt.UserRole) for i in range(self.responsesSelector.count())]) 

1217 if response_indices.size == 0: 

1218 QMessageBox.critical(self, 'Invalid Responses', 

1219 'At least one response must be selected') 

1220 return 

1221 if self.geometry is None: 

1222 self.loadGeometry() 

1223 if self.geometry is None: 

1224 return 

1225 self.plots['transient'] = self.geometry.plot_transient( 

1226 self.time_history_data[response_indices].extract_elements(slice(*self.get_abscissa_index_range()))) 

1227 

1228 def plotDeflection(self): 

1229 try: 

1230 print('Plotting deflection:') 

1231 print('Action: {:}'.format(self.sender())) 

1232 for index, action in enumerate(self.menuVisualize_with_DeflectionShapePlotter.actions()): 

1233 if self.sender() is action: 

1234 print('Index {:}'.format(index)) 

1235 break 

1236 if self.geometry is None: 

1237 self.loadGeometry() 

1238 if self.geometry is None: 

1239 return 

1240 self.plots['deflection_shape'] = self.geometry.plot_deflection_shape( 

1241 self.frf_data[:, index]) 

1242 except Exception: 

1243 print(traceback.format_exc()) 

1244 

1245 def loadData(self): 

1246 try: 

1247 filename, file_filter = QFileDialog.getOpenFileName( 

1248 self, 'Select Time History File', filter='Numpy (*.npz);;Rattlesnake (*.nc4)') 

1249 if filename == '': 

1250 return 

1251 if file_filter == 'Numpy (*.npz)': 

1252 self.time_history_data = TimeHistoryArray.load(filename) 

1253 self.initialize_ui() 

1254 elif file_filter == 'Rattlesnake (*.nc4)': 

1255 self.time_history_data, channel_table = read_rattlesnake_output(filename) 

1256 self.initialize_ui() 

1257 # TODO Automatically select references and responses 

1258 else: 

1259 QMessageBox.critical(self, 'Invalid Data File Type', 

1260 'File must be a Rattlesnake or SDynPy Time History File') 

1261 except Exception: 

1262 print(traceback.format_exc())